├── .buildkite ├── config │ └── blockchain_limits.conf ├── env │ └── secrets.ejson ├── hooks │ ├── post-checkout │ └── pre-command ├── pipeline.yml └── scripts │ ├── make_deb.sh │ ├── make_docker.sh │ ├── packagecloud_upload.sh │ └── start.sh ├── .dockerignore ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── config ├── dev.config ├── dev_testnet.config ├── docker_node.config.src ├── grpc_server_gen.config ├── local.config ├── prod.config ├── sys.config ├── test.config └── vm.args ├── deb ├── after_install.sh ├── after_remove.sh ├── after_upgrade.sh ├── before_install.sh ├── before_upgrade.sh ├── blockchain-node.service └── node.config ├── docs ├── Makefile ├── blockchain_node-reference.html ├── blockchain_node-reference.md └── blockchain_node.schema.json ├── priv ├── genesis ├── genesis_devnet └── genesis_testnet ├── rebar.config ├── rebar.config.script ├── rebar.lock ├── rebar3 └── src ├── blockchain_node.app.src ├── blockchain_node_app.erl ├── bn_accounts.erl ├── bn_blocks.erl ├── bn_db.erl ├── bn_gateways.erl ├── bn_htlc.erl ├── bn_implicit_burn.erl ├── bn_jsonrpc.hrl ├── bn_jsonrpc_handler.erl ├── bn_oracle_price.erl ├── bn_peer.erl ├── bn_pending_txns.erl ├── bn_sup.erl ├── bn_txns.erl ├── bn_wallets.erl ├── helium_follower_service.erl ├── metrics ├── bn_metrics_exporter.erl ├── bn_metrics_server.erl └── metrics.hrl └── wallet.erl /.buildkite/config/blockchain_limits.conf: -------------------------------------------------------------------------------- 1 | * soft memlock unlimited 2 | * hard memlock unlimited 3 | * soft nofile 64000 4 | * hard nofile 64000 5 | * soft nproc 64000 6 | * hard nproc 64000 7 | -------------------------------------------------------------------------------- /.buildkite/env/secrets.ejson: -------------------------------------------------------------------------------- 1 | { 2 | "_public_key": "4da2b52855505e4af8c48d6dea31da6431ae7a19acef5d1ea0f2121cc8760334", 3 | "environment": { 4 | "PACKAGECLOUD_API_KEY": "EJ[1:hA6z/9BUNCsCGFAYYCidExwhTwdqgCik79GNsivcflY=:NqUGHDzae4LP9YN9bGA+j9t51L9a+uIZ:ZGMBwTiCQggE1Rog3ysjfmsFMiC+Zpn/Ux0Tw32DrHLpt8P2L8FJHfq/ibKy4YMj7HLlCb29c9ahqi39E0lmPw==]" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.buildkite/hooks/post-checkout: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The `post-checkout` hook will run after the bootstrap script has checked out 4 | # your pipelines source code. 5 | 6 | set -euo pipefail 7 | 8 | git fetch --tags --force # avoid "would clobber existing tags error" 9 | git tag --points-at $(git rev-list -n 1 $BUILDKITE_TAG) | xargs -n 1 git tag -d || true 10 | -------------------------------------------------------------------------------- /.buildkite/hooks/pre-command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo '--- :house_with_garden: Setting up the environment' 5 | 6 | . "$HOME/.asdf/asdf.sh" 7 | asdf local erlang 24.3.4 8 | asdf local python 3.7.3 9 | asdf local ruby 2.6.2 10 | 11 | export EJSON_KEYDIR="$HOME/.ejson" 12 | eval "$(ejson2env .buildkite/env/secrets.ejson)" 13 | -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - label: ":hammer: running tests" 3 | commands: 4 | - "make ci" 5 | #- "make test" #currently no ct defined, causes build error 6 | key: "tests" 7 | agents: 8 | queue: "erlang" 9 | 10 | - if: build.tag =~ /^testnet/ 11 | label: ":debian: build deb" 12 | env: 13 | BUILD_NET: "testnet" 14 | PKGSTEM: "testnet-blockchain-node" 15 | RELEASE_TARGET: "dev_testnet" 16 | VERSION_TAG: $BUILDKITE_TAG 17 | commands: 18 | - "git tag $BUILDKITE_TAG" 19 | - ".buildkite/scripts/make_deb.sh" 20 | key: "test-deb" 21 | artifact_paths: "*.deb" 22 | agents: 23 | queue: "erlang" 24 | 25 | - if: build.tag =~ /^testnet/ 26 | label: "upload" 27 | name: ":cloud: upload to packagecloud" 28 | env: 29 | BUILD_NET: "testnet" 30 | PKGSTEM: "testnet-blockchain-node" 31 | VERSION_TAG: $BUILDKITE_TAG 32 | commands: 33 | - ".buildkite/scripts/packagecloud_upload.sh" 34 | depends_on: "test-deb" 35 | agents: 36 | queue: "erlang" 37 | 38 | - if: build.tag =~ /^devnet/ 39 | label: ":debian: build deb" 40 | env: 41 | BUILD_NET: "devnet" 42 | RELEASE_TARGET: "devnet" 43 | PKGSTEM: "devnet-blockchain-node" 44 | VERSION_TAG: $BUILDKITE_TAG 45 | commands: 46 | - "git tag $BUILDKITE_TAG" 47 | - ".buildkite/scripts/make_deb.sh devnet" 48 | key: "dev-deb" 49 | artifact_paths: "*.deb" 50 | agents: 51 | queue: "erlang" 52 | 53 | - if: build.tag =~ /^devnet/ 54 | label: "upload" 55 | name: ":cloud: upload to packagecloud" 56 | env: 57 | BUILD_NET: "devnet" 58 | PKGSTEM: "devnet-blockchain-node" 59 | VERSION_TAG: $BUILDKITE_TAG 60 | commands: 61 | - ".buildkite/scripts/packagecloud_upload.sh" 62 | depends_on: "dev-deb" 63 | agents: 64 | queue: "erlang" 65 | 66 | - if: build.tag != null && build.tag !~ /^devnet/ && build.tag !~ /^testnet/ 67 | label: ":debian: build deb" 68 | env: 69 | VERSION_TAG: $BUILDKITE_TAG 70 | commands: 71 | - "git tag $BUILDKITE_TAG" 72 | - ".buildkite/scripts/make_deb.sh prod" 73 | key: "prod-deb" 74 | artifact_paths: "*.deb" 75 | agents: 76 | queue: "erlang" 77 | 78 | - if: build.tag != null && build.tag !~ /^devnet/ && build.tag !~ /^testnet/ 79 | label: "upload" 80 | name: ":cloud: upload to packagecloud" 81 | env: 82 | VERSION_TAG: $BUILDKITE_TAG 83 | commands: 84 | - ".buildkite/scripts/packagecloud_upload.sh" 85 | depends_on: "prod-deb" 86 | agents: 87 | queue: "erlang" 88 | 89 | - if: build.tag != null && build.tag !~ /^devnet/ && build.tag !~ /^testnet/ 90 | label: "prod docker" 91 | name: ":whale: build docker prod image" 92 | env: 93 | VERSION_TAG: $BUILDKITE_TAG 94 | commands: 95 | - "git tag $BUILDKITE_TAG" 96 | - ".buildkite/scripts/make_docker.sh docker_node" 97 | agents: 98 | queue: "erlang" 99 | -------------------------------------------------------------------------------- /.buildkite/scripts/make_deb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | BUILD_NET="${BUILD_NET:-mainnet}" 6 | RELEASE_TARGET="${RELEASE_TARGET:-prod}" 7 | PKGSTEM="${PKGSTEM:-blockchain-node}" 8 | 9 | VERSION=$( echo $VERSION_TAG | sed -e "s,${BUILD_NET},," ) 10 | 11 | DIAGNOSTIC=1 ./rebar3 as ${RELEASE_TARGET} release -v ${VERSION} -n blockchain_node 12 | 13 | wget -O /tmp/genesis https://snapshots.helium.wtf/genesis.${BUILD_NET} 14 | 15 | if [ ! -d /opt/blockchain_node/etc ]; then 16 | mkdir -p /opt/blockchain_node/etc 17 | fi 18 | 19 | if [ ! -f /opt/blockchain_node/etc/node.config ]; then 20 | touch /opt/blockchain_node/etc/node.config 21 | fi 22 | 23 | fpm -n ${PKGSTEM} \ 24 | -v "${VERSION}" \ 25 | -s dir \ 26 | -t deb \ 27 | --depends libssl1.1 \ 28 | --depends libsodium23 \ 29 | --depends libncurses5 \ 30 | --depends dbus \ 31 | --depends libstdc++6 \ 32 | --deb-systemd deb/blockchain-node.service \ 33 | --before-install deb/before_install.sh \ 34 | --after-install deb/after_install.sh \ 35 | --after-remove deb/after_remove.sh \ 36 | --before-upgrade deb/before_upgrade.sh \ 37 | --after-upgrade deb/after_upgrade.sh \ 38 | --deb-no-default-config-files \ 39 | --deb-systemd-enable \ 40 | --deb-systemd-auto-start \ 41 | --deb-systemd-restart-after-upgrade \ 42 | --deb-user helium \ 43 | --deb-group helium \ 44 | --config-files /opt/blockchain_node/etc/node.config \ 45 | _build/${RELEASE_TARGET}/rel/=/opt \ 46 | /tmp/genesis=/opt/miner/update/genesis 47 | 48 | rm -f /tmp/genesis 49 | -------------------------------------------------------------------------------- /.buildkite/scripts/make_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | REGISTRY_HOST="quay.io/team-helium/blockchain-node" 6 | BUILDER_IMAGE="erlang:24-alpine" 7 | RUNNER_IMAGE="alpine:3.17" 8 | 9 | BUILD_TARGET=$1 10 | VERSION=$VERSION_TAG 11 | DOCKER_BUILD_ARGS="--build-arg VERSION=$VERSION --build-arg BUILD_TARGET=$BUILD_TARGET" 12 | 13 | case "$BUILD_TARGET" in 14 | "docker_node") 15 | echo "Building docker node" 16 | DOCKER_BUILD_ARGS="--build-arg BUILDER_IMAGE=$BUILDER_IMAGE --build-arg RUNNER_IMAGE=$RUNNER_IMAGE $DOCKER_BUILD_ARGS" 17 | DOCKER_NAME="blockchain-node-$VERSION" 18 | DOCKERFILE="./Dockerfile" 19 | ;; 20 | *) 21 | echo "I don't know how to build $BUILD_TARGET" 22 | exit 1 23 | ;; 24 | esac 25 | 26 | docker build $DOCKER_BUILD_ARGS -t "helium:${DOCKER_NAME}" -f "$DOCKERFILE" . 27 | docker tag "helium:$DOCKER_NAME" "$REGISTRY_HOST:$DOCKER_NAME" 28 | docker login -u="team-helium+buildkite" -p="${QUAY_BUILDKITE_PASSWORD}" quay.io 29 | docker push "$REGISTRY_HOST:$DOCKER_NAME" 30 | -------------------------------------------------------------------------------- /.buildkite/scripts/packagecloud_upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | BUILD_NET="${BUILD_NET:-mainnet}" 6 | PKGSTEM="${PKGSTEM:-blockchain-node}" 7 | 8 | VERSION=$( echo $VERSION_TAG | sed -e "s,${BUILD_NET},," ) 9 | 10 | PKGNAME="${PKGSTEM}_${VERSION}_amd64.deb" 11 | REPO=$( echo $PKGSTEM | sed -e "s,-,_,g" ) 12 | 13 | buildkite-agent artifact download ${PKGNAME} . 14 | 15 | curl -u "${PACKAGECLOUD_API_KEY}:" \ 16 | -F "package[distro_version_id]=210" \ 17 | -F "package[package_file]=@${PKGNAME}" \ 18 | https://packagecloud.io/api/v1/repos/helium/${REPO}/packages.json 19 | -------------------------------------------------------------------------------- /.buildkite/scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | /var/helium/blockchain_node/bin/blockchain_node foreground 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | .buildkite/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | *.iml 18 | rebar3.crashdump 19 | .DS_Store 20 | data/ 21 | .env 22 | .envrc 23 | 24 | # gpb auto-generatd erl modules 25 | src/grpc/ 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute to this repository # 2 | 3 | We value contributions from the community and will do everything we 4 | can go get them reviewed in a timely fashion. If you have code to send 5 | our way or a bug to report: 6 | 7 | * **Contributing Code**: If you have new code or a bug fix, fork this 8 | repo, create a logically-named branch, and [submit a PR against this 9 | repo](https://github.com/helium/blockchain-node/issues). Include a 10 | write up of the PR with details on what it does. 11 | 12 | * **Reporting Bugs**: Open an issue [against this 13 | repo](https://github.com/helium/blockchain-node/issues) with as much 14 | detail as you can. At the very least you'll include steps to 15 | reproduce the problem. 16 | 17 | This project is intended to be a safe, welcoming space for 18 | collaboration, and contributors are expected to adhere to the 19 | [Contributor Covenant Code of 20 | Conduct](http://contributor-covenant.org/). 21 | 22 | Above all, thank you for taking the time to be a part of the Helium community. 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILDER_IMAGE 2 | ARG RUNNER_IMAGE 3 | 4 | FROM ${BUILDER_IMAGE} as deps-compiler 5 | 6 | ARG BUILD_TARGET=docker_node 7 | 8 | RUN apk add --no-cache --update \ 9 | git tar build-base linux-headers autoconf automake libtool pkgconfig \ 10 | dbus-dev bzip2 bison flex gmp-dev cmake lz4 libsodium-dev openssl-dev \ 11 | sed curl 12 | 13 | # Install Rust toolchain 14 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 15 | 16 | WORKDIR /usr/src/blockchain_node 17 | 18 | ENV CC=gcc CXX=g++ CFLAGS="-U__sun__" \ 19 | ERLANG_ROCKSDB_OPTS="-DWITH_BUNDLE_SNAPPY=ON -DWITH_BUNDLE_LZ4=ON" \ 20 | ERL_COMPILER_OPTIONS="[deterministic]" \ 21 | PATH="/root/.cargo/bin:$PATH" \ 22 | RUSTFLAGS="-C target-feature=-crt-static" 23 | 24 | # Add and compile the dependencies to cache 25 | COPY ./rebar* ./Makefile ./ 26 | COPY ./config/grpc_server_gen.config ./config/ 27 | 28 | RUN ./rebar3 compile 29 | 30 | FROM deps-compiler as builder 31 | 32 | ARG VERSION 33 | ARG BUILD_TARGET=docker_node 34 | 35 | # Now add our code 36 | COPY . . 37 | 38 | RUN DIAGNOSTIC=1 ./rebar3 as ${BUILD_TARGET} tar -v ${VERSION} -n blockchain_node \ 39 | && mkdir -p /opt/docker \ 40 | && tar -zxvf _build/${BUILD_TARGET}/rel/*/*.tar.gz -C /opt/docker 41 | 42 | FROM ${RUNNER_IMAGE} as runner 43 | 44 | ARG VERSION 45 | 46 | RUN apk add --no-cache --update ncurses dbus libsodium libgcc libstdc++ 47 | RUN ulimit -n 128000 48 | 49 | WORKDIR /opt/blockchain_node 50 | 51 | ENV COOKIE=node \ 52 | # Write files generated during startup to /tmp 53 | RELX_OUT_FILE_PATH=/tmp \ 54 | # add blockchain_node to path, for easy interactions 55 | PATH=/sbin:/bin:/usr/bin:/usr/local/bin:/opt/blockchain_node/bin:$PATH 56 | 57 | COPY --from=builder /opt/docker ./ 58 | 59 | RUN ln -sf /opt/blockchain_node/releases/$VERSION /config 60 | 61 | ENTRYPOINT ["bin/blockchain_node"] 62 | CMD ["foreground"] 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2018, Helium Systems Inc. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | 192 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: compile rel cover test typecheck doc ci start stop reset 2 | 3 | REBAR=./rebar3 4 | BUILDER_IMAGE=erlang:24-alpine 5 | RUNNER_IMAGE=alpine:3.17 6 | APP_VERSION=$$(git tag --points-at HEAD) 7 | 8 | OS_NAME=$(shell uname -s) 9 | PROFILE ?= dev 10 | 11 | grpc_services_directory=src/grpc/autogen 12 | 13 | ifeq (${OS_NAME},FreeBSD) 14 | make="gmake" 15 | else 16 | MAKE="make" 17 | endif 18 | 19 | compile: 20 | $(REBAR) compile 21 | 22 | shell: 23 | $(REBAR) shell 24 | 25 | clean: 26 | rm -rf $(grpc_services_directory) 27 | $(REBAR) clean 28 | 29 | cover: 30 | $(REBAR) cover 31 | 32 | test: 33 | $(REBAR) as test do eunit,ct 34 | 35 | ci: 36 | $(REBAR) do xref, dialyzer 37 | 38 | typecheck: 39 | $(REBAR) dialyzer 40 | 41 | doc: 42 | $(REBAR) edoc 43 | 44 | grpc: | $(grpc_services_directory) 45 | @echo "generating grpc services" 46 | REBAR_CONFIG="config/grpc_server_gen.config" $(REBAR) grpc gen 47 | 48 | clean_grpc: 49 | @echo "cleaning grpc services" 50 | rm -rf $(grpc_services_directory) 51 | 52 | $(grpc_services_directory): 53 | @echo "grpc service directory $(directory) does not exist" 54 | $(REBAR) get-deps 55 | 56 | release: 57 | $(REBAR) as $(PROFILE) do release 58 | 59 | .PHONY: docs 60 | 61 | start: 62 | ./_build/$(PROFILE)/rel/blockchain_node/bin/blockchain_node start 63 | 64 | stop: 65 | -./_build/$(PROFILE)/rel/blockchain_node/bin/blockchain_node stop 66 | 67 | console: 68 | ./_build/$(PROFILE)/rel/blockchain_node/bin/blockchain_node remote_console 69 | 70 | docker-build: 71 | docker build \ 72 | --build-arg VERSION=$(APP_VERSION) \ 73 | --build-arg BUILDER_IMAGE=$(BUILDER_IMAGE) \ 74 | --build-arg RUNNER_IMAGE=$(RUNNER_IMAGE) \ 75 | -t helium/node . 76 | 77 | docker-clean: docker-stop 78 | docker rm node 79 | 80 | docker-start: 81 | mkdir -p $(HOME)/node_data 82 | docker run -d --init \ 83 | --publish 44158:44158/tcp \ 84 | --publish 4467:4467/tcp \ 85 | --name node \ 86 | --mount type=bind,source=$(HOME)/node_data,target=/var/data \ 87 | helium/node 88 | 89 | docker-stop: 90 | docker stop node 91 | 92 | docs: 93 | $(MAKE) -C docs 94 | 95 | update-genesis: 96 | curl -o priv/genesis https://snapshots.helium.wtf/genesis.mainnet 97 | curl -o priv/genesis_testnet https://snapshots.helium.wtf/genesis.testnet 98 | curl -o priv/genesis_devnet https://snapshots.helium.wtf/genesis.devnet 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Archived 2 | 3 | This repository is no longer applicable after the migration to Solana 4 | 5 | # blockchain-node 6 | 7 | [![Build status](https://badge.buildkite.com/8f80e5ba2dd64290fb11c5126477a023b0ea75d35f08783085.svg?branch=master)](https://buildkite.com/helium/blockchain-node) 8 | 9 | This is an Erlang application that is a Helium Blockchain node. It 10 | follows the blockchain and exposes functionality using a JSONRPC 2.0 API. 11 | 12 | ## Documentation 13 | 14 | See [API endpoint 15 | documentation](https://helium.github.io/blockchain-node/blockchain_node-reference.html) 16 | (in [markdown format](docs/blockchain_node-reference.md)). 17 | 18 | ## Developer Usage 19 | 20 | - Clone this repository 21 | 22 | - Run `make && make release` in the top level folder 23 | 24 | - Run `make start` to start the application. Logs will be at 25 | `_build/dev/rel/blockchain_node/log/*`. 26 | 27 | Once started the application will start syncing the blockchain and 28 | loading blocks. If this is done from scratch it can take a number of 29 | days to download all blocks from the network and aobsorb them in the 30 | local ledger. 31 | 32 | ### File Descriptors 33 | 34 | The application uses a lot of file descriptors for network 35 | communication and local storage. If you see errors related to too many 36 | open files or `nofile`, stop the application and increase the file 37 | descriptor limit. 38 | 39 | #### macOS 40 | 41 | You may see an error similar to the following: 42 | 43 | `{error,"IO error: While open a file for appending: data/blockchain.db/020311.sst: Too many open files"}` 44 | 45 | Check [this](https://superuser.com/a/443168) Superuser answer for a workaround. 46 | 47 | #### Linux 48 | 49 | Update your `/etc/security/limits.conf` to increase your file limits. An 50 | example of what to add can be seen 51 | [here](https://github.com/helium/blockchain-node/blob/master/.buildkite/config/blockchain_limits.conf). 52 | 53 | ### Installing Ubuntu Required Packages 54 | 55 | If running on Ubuntu, you will need the following packages installed 56 | before running `make release`: 57 | 58 | ```bash 59 | wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb 60 | sudo dpkg -i erlang-solutions_2.0_all.deb 61 | sudo apt-get update 62 | sudo apt install esl-erlang=1:24.3.3-1 cmake libsodium-dev libssl-dev build-essential 63 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 64 | ``` 65 | 66 | If you already have rust installed, please ensure it is at least at verion `1.51` or upgrade to the latest stable using `rustup update stable` 67 | 68 | ## Using Docker 69 | 70 | ### Building the Docker Image 71 | 72 | `make docker-build` 73 | 74 | ### Running the Docker Container 75 | 76 | `make docker-start` 77 | 78 | ### Updating Docker 79 | 80 | Navigate to your copy of the `blockchain-node` repository. 81 | 82 | `cd /path/to/blockchain-node` 83 | 84 | Stop the Node. 85 | 86 | `make docker-stop` 87 | 88 | Update the repository. 89 | 90 | `git pull` 91 | 92 | Remove the existing Docker container. 93 | 94 | `make docker-clean` 95 | 96 | Rebuild the Docker image. 97 | 98 | `make docker-build` 99 | 100 | Run the updated Docker container. 101 | 102 | `make docker-start` 103 | 104 | Log the Node output. 105 | 106 | `tail -f $HOME/node_data/log/console.log` 107 | -------------------------------------------------------------------------------- /config/dev.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | [ 3 | "config/sys.config", 4 | {lager, 5 | [ 6 | {log_root, "log"} 7 | ]}, 8 | {blockchain, 9 | [ 10 | {base_dir, "data"} 11 | ]} 12 | ]. 13 | -------------------------------------------------------------------------------- /config/dev_testnet.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | [ 3 | "config/sys.config", 4 | {lager, 5 | [ 6 | {log_root, "log"} 7 | ]}, 8 | {blockchain, 9 | [ 10 | {base_dir, "data"}, 11 | {network, testnet}, 12 | {seed_nodes, "/ip4/54.244.119.55/tcp/2154,/ip4/3.22.146.211/tcp/443"}, 13 | {seed_node_dns, ""}, 14 | {honor_quick_sync, false}, 15 | {quick_sync_mode, assumed_valid} 16 | ]}, 17 | "etc/node.config" 18 | ]. 19 | -------------------------------------------------------------------------------- /config/docker_node.config.src: -------------------------------------------------------------------------------- 1 | [ 2 | "config/sys.config", 3 | {lager, 4 | [ 5 | {log_root, "/var/data/log"}, 6 | {handlers, 7 | [ 8 | {lager_file_backend, [{file, "console.log"}, {size, 10485760}, {date, "$D0"}, {count, 5}, {level, info}]}, 9 | {lager_file_backend, [{file, "error.log"}, {size, 10485760}, {date, "$D0"}, {count, 5}, {level, error}]} 10 | ]} 11 | ]}, 12 | {libp2p, 13 | [ 14 | {nat_map, #{ {"${NAT_INTERNAL_IP}", "${NAT_INTERNAL_PORT}"} => {"${NAT_EXTERNAL_IP}", "${NAT_EXTERNAL_PORT}"}}} 15 | ]}, 16 | {blockchain, 17 | [ 18 | {base_dir, "/var/data"} 19 | ]}, 20 | "etc/node.config" 21 | ]. 22 | -------------------------------------------------------------------------------- /config/grpc_server_gen.config: -------------------------------------------------------------------------------- 1 | {plugins, [ 2 | {grpcbox_plugin, 3 | {git, "https://github.com/novalabsxyz/grpcbox_plugin.git", 4 | {branch, "andymck/ts-master/combined-opts-and-template-changes"}}} 5 | ]}. 6 | 7 | {grpc, [ 8 | {proto_files, [ 9 | "_build/default/lib/helium_proto/src/service/follower.proto" 10 | ]}, 11 | {beam_out_dir, "src/grpc/autogen/server"}, 12 | {out_dir, "src/grpc/autogen/server"}, 13 | {keep_beams, false}, 14 | {create_services, true}, 15 | {type, server}, 16 | {override_gpb_defaults, true}, 17 | {gpb_opts, [ 18 | {rename, {msg_fqname, base_name}}, 19 | use_packages, 20 | {defs_as_proplists, true}, 21 | {report_errors, false}, 22 | {descriptor, false}, 23 | {recursive, false}, 24 | {i, "_build/default/lib/helium_proto/src"}, 25 | {o, "src/grpc/autogen/server"}, 26 | {module_name_prefix, ""}, 27 | {module_name_suffix, "_pb"}, 28 | {rename, {msg_name, {suffix, "_pb"}}}, 29 | {strings_as_binaries, false}, 30 | type_specs 31 | ]} 32 | ]}. 33 | -------------------------------------------------------------------------------- /config/local.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | [ 3 | "config/sys.config", 4 | {blockchain, 5 | [ 6 | {base_dir, "data"}, 7 | {honor_assumed_valid, false}, 8 | {port, 0}, 9 | {key, undefined}, 10 | {num_consensus_members, 7}, 11 | {seed_nodes, ""}, 12 | {seed_node_dns, ""}, 13 | {peerbook_update_interval, 60000}, 14 | {peerbook_allow_rfc1918, true}, 15 | {peer_cache_timeout, 20000} 16 | ]} 17 | ]. 18 | -------------------------------------------------------------------------------- /config/prod.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | [ 3 | "config/sys.config", 4 | {lager, 5 | [ 6 | {log_root, "log"} 7 | ]}, 8 | {blockchain, 9 | [ 10 | {base_dir, "data"} 11 | ]}, 12 | "etc/node.config" 13 | ]. 14 | -------------------------------------------------------------------------------- /config/sys.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | [ 3 | {pg_types, [{json_config, {jsone, [], [{keys, atom}]}}]}, 4 | {blockchain_node, [ 5 | {jsonrpc_port, 4467}, 6 | {metrics, [block_metrics, txn_metrics, grpc_metrics]}, 7 | {metrics_port, 29090} 8 | ]}, 9 | {kernel, [ 10 | %% force distributed erlang to only run on localhost 11 | {inet_dist_use_interface, {127, 0, 0, 1}} 12 | ]}, 13 | {lager, [ 14 | {log_root, "log"}, 15 | {suppress_supervisor_start_stop, true}, 16 | {metadata_whitelist, [poc_id]}, 17 | {crash_log, "crash.log"}, 18 | {colored, true}, 19 | {handlers, [ 20 | {lager_file_backend, [{file, "console.log"}, {level, info}]}, 21 | {lager_file_backend, [{file, "error.log"}, {level, error}]} 22 | ]} 23 | ]}, 24 | {libp2p, [ 25 | {use_dns_for_seeds, true}, 26 | {seed_dns_cname, "seed.helium.io"}, 27 | {seed_config_dns_name, "_seed_config.helium.io"}, 28 | {similarity_time_diff_mins, 30}, 29 | {random_peer_pred, fun bn_sup:random_val_predicate/1} 30 | ]}, 31 | {blockchain, [ 32 | {snap_source_base_url, "https://snapshots.helium.wtf/mainnet"}, 33 | {fetch_latest_from_snap_source, false}, 34 | {block_sync_batch_size, 10}, 35 | {block_sync_batch_limit, 100}, 36 | {honor_quick_sync, true}, 37 | {quick_sync_mode, blessed_snapshot}, 38 | {blessed_snapshot_block_height, 1471681}, 39 | {blessed_snapshot_block_hash, 40 | <<136, 139, 194, 2, 69, 194, 236, 22, 135, 91, 228, 52, 218, 201, 55, 195, 37, 173, 212, 41 | 164, 174, 245, 94, 219, 126, 252, 225, 126, 134, 90, 217, 137>>}, 42 | {listen_addresses, ["/ip4/0.0.0.0/tcp/44158"]}, 43 | {store_json, false}, 44 | {store_htlc_receipts, false}, 45 | {store_implicit_burns, false}, 46 | {store_historic_balances, false}, 47 | {key, undefined}, 48 | {base_dir, "data"}, 49 | {autoload, false}, 50 | {num_consensus_members, 16}, 51 | {seed_nodes, "/ip4/18.217.27.26/tcp/2154,/ip4/99.80.158.114/tcp/443,/ip4/54.207.252.240/tcp/2154,/ip4/3.34.10.207/tcp/443"}, 52 | {disable_gateway_cache, true}, 53 | {sync_timeout_mins, 5}, 54 | {max_inbound_connections, 32}, 55 | {snapshot_memory_limit, 2048}, 56 | {outbound_gossip_connections, 4}, 57 | {peerbook_update_interval, 180000}, 58 | {peerbook_allow_rfc1918, false}, 59 | {relay_limit, 50} 60 | ]}, 61 | {grpcbox, [ 62 | {servers, [ 63 | #{ 64 | grpc_opts => 65 | #{ 66 | service_protos => [follower_pb, transaction_pb], 67 | services => #{ 68 | 'helium.follower.follower' => helium_follower_service, 69 | 'helium.transaction.transaction' => helium_transaction_service 70 | } 71 | }, 72 | transport_opts => #{ssl => false}, 73 | listen_opts => #{port => 8080, ip => {0, 0, 0, 0}}, 74 | port_opts => #{size => 100}, 75 | server_opts => 76 | #{ 77 | header_table_size => 4096, 78 | enable_push => 1, 79 | max_concurrent_streams => unlimited, 80 | initial_window_size => 65535, 81 | max_frame_size => 16384, 82 | max_header_list_size => unlimited 83 | } 84 | } 85 | ]} 86 | ]}, 87 | {relcast, [ 88 | {db_open_opts, [ 89 | {total_threads, 4}, 90 | {max_background_jobs, 2}, 91 | {max_background_compactions, 2} 92 | %% {max_background_flushes, 2}, % not sure if needed 93 | ]}, 94 | {defer_count_threshold, 30}, 95 | {defer_time_threshold, 1000} 96 | ]}, 97 | {rocksdb, [ 98 | {global_opts, [ 99 | {max_open_files, 128}, 100 | {compaction_style, universal}, 101 | {block_based_table_options, [{cache_index_and_filter_blocks, true}]}, 102 | % 8MB 103 | {memtable_memory_budget, 8388608}, 104 | % 256kB 105 | {arena_block_size, 262144}, 106 | % 256kB 107 | {write_buffer_size, 262144}, 108 | % 8MB 109 | {db_write_buffer_size, 8388608}, 110 | {max_write_buffer_number, 8}, 111 | {keep_log_file_num, 5} 112 | ]} 113 | ]} 114 | ]. 115 | -------------------------------------------------------------------------------- /config/test.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | [ 3 | "config/sys.config", 4 | {blockchain, 5 | [ 6 | {base_dir, "data"}, 7 | {honor_assumed_valid, false}, 8 | {port, 0}, 9 | {key, undefined}, 10 | {num_consensus_members, 7}, 11 | {seed_nodes, ""}, 12 | {seed_node_dns, ""}, 13 | {peerbook_update_interval, 60000}, 14 | {peerbook_allow_rfc1918, true}, 15 | {peer_cache_timeout, 20000} 16 | ]} 17 | ]. -------------------------------------------------------------------------------- /config/vm.args: -------------------------------------------------------------------------------- 1 | -name {{release_name}}@127.0.0.1 2 | -setcookie {{release_name}} 3 | 4 | +c true 5 | +C multi_time_warp 6 | +sbwt very_short 7 | +stbt db 8 | +sub true 9 | +swt very_low -------------------------------------------------------------------------------- /deb/after_install.sh: -------------------------------------------------------------------------------- 1 | # add miner to /usr/local/bin it appears in path 2 | ln -s /opt/miner/bin/miner /usr/local/bin/miner 3 | 4 | # if upgrading from old version with different file location, move miner data files to the new location 5 | if [ -e /var/helium/blockchain_node/data/blockchain_node/swarm_key ] && [ ! -e /opt/blockchain_node/data/blockchain_node/swarm_key ]; then 6 | echo "Found existing swarm_key, moving data to /opt/miner/" 7 | mv /var/helium/miner/data /opt/miner/data 8 | chown -R helium:helium /opt/miner/data 9 | elif [ -e /var/data/blockchain_node/swarm_key ] && [ ! -e /opt/blockchain_node/data/blockchain_node/swarm_key ]; then 10 | echo "Found existing swarm_key, moving data to /opt/miner/" 11 | mv /var/data/miner /opt/miner/data 12 | chown -R helium:helium /opt/miner/data 13 | fi 14 | -------------------------------------------------------------------------------- /deb/after_remove.sh: -------------------------------------------------------------------------------- 1 | rm -f /usr/local/bin/blockchain_node 2 | -------------------------------------------------------------------------------- /deb/after_upgrade.sh: -------------------------------------------------------------------------------- 1 | # if upgrading from old version with different file location, move blockchain_node date files to the new location 2 | if [ -e /var/helium/blockchain_node/data/blockchain_node/swarm_key ] && [ ! -e /opt/blockchain_node/data/blockchain_node/swarm_key ]; then 3 | echo "Found existing swarm_key, moving data to /opt/blockchain_node/" 4 | mv /var/helium/blockchain_node/data /opt/blockchain_node/data 5 | chown -R helium:helium /opt/blockchain_node/data 6 | elif [ -e /var/data/blockchain_node/blockchain_node/swarm_key ] && [ ! -e /opt/blockchain_node/data/blockchain_node/swarm_key ]; then 7 | echo "Found existing swarm_key, moving data to /opt/blockchain_node/" 8 | mv /var/data/blockchain_node /opt/blockchain_node/data 9 | chown -R helium:helium /opt/blockchain_node/data 10 | fi 11 | 12 | # add blockchain_node to /usr/local/bin so it appears in path, if it does not already exist 13 | ln -s /opt/blockchain_node/bin/blockchain_node /usr/local/bin/blockchain_node || true 14 | -------------------------------------------------------------------------------- /deb/before_install.sh: -------------------------------------------------------------------------------- 1 | # add system user for file ownership and systemd user, if not exists 2 | useradd --system --home-dir /opt/miner --create-home helium || true 3 | 4 | # make the hotfix directory 5 | if [ ! -d /opt/miner/hotfix ]; then 6 | mkdir -p /opt/miner/hotfix 7 | chown helium:helium /opt/miner/hotfix 8 | fi 9 | -------------------------------------------------------------------------------- /deb/before_upgrade.sh: -------------------------------------------------------------------------------- 1 | # add system user for file ownership and systemd user, if not exists 2 | useradd --system --home-dir /opt/miner --create-home helium || true 3 | -------------------------------------------------------------------------------- /deb/blockchain-node.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=blockchain-node instance 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=/opt/blockchain_node/bin/blockchain_node foreground 8 | ExecStop=/opt/blockchain_node/bin/blockchain_node stop 9 | User=helium 10 | PIDFile=/var/blockchain_node/blockchain_node.pid 11 | Environment=HOME="/opt/blockchain_node" 12 | Environment=RUNNER_LOG_DIR="/opt/blockchain_node/log/blockchain_node" 13 | Environment=ERL_CRASH_DUMP="/opt/blockchain_node/log/blockchain_node" 14 | LimitNOFILE=200000 15 | LimitNPROC=200000 16 | Restart=always 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /deb/node.config: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | all: blockchain_node.md blockchain_node.html 2 | 3 | blockchain_node.md: blockchain_node.schema.json 4 | jrgen docs-md blockchain_node.schema.json 5 | 6 | blockchain_node.html: blockchain_node.schema.json 7 | jrgen docs-html blockchain_node.schema.json 8 | -------------------------------------------------------------------------------- /docs/blockchain_node.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://rawgit.com/helium/blockchain-node/master/docs/blockchain_node.schema.json", 3 | "jrgen": "1.1", 4 | "jsonrpc": "2.0", 5 | "info": { 6 | "title": "blockchain_node", 7 | "description": [ 8 | "An API to the Helium blockchain-node.", 9 | "This api follows the json-rpc 2.0 specification. More information available at http://www.jsonrpc.org/specification." 10 | ], 11 | "version": "1.0" 12 | }, 13 | "definitions": { 14 | "height": { 15 | "type": "number", 16 | "description": "Block height" 17 | }, 18 | "block": { 19 | "type": "object", 20 | "description": "Block details", 21 | "properties": { 22 | "hash": { 23 | "type": "string", 24 | "description": "Hash of block", 25 | "example": "vX_PzD2DvIQZPlpM_LiDCCewpWuZkwcdAhjnJXeg5Gk" 26 | }, 27 | "height": { 28 | "type": "number", 29 | "description": "Height of block", 30 | "example": 318492 31 | }, 32 | "prev_hash": { 33 | "type": "string", 34 | "description": "Hash of previous block", 35 | "example": "OLv5ah-94zg3ySJK5x50-W6Kw4gd510ikhpbByq37ZU" 36 | }, 37 | "time": { 38 | "type": "number", 39 | "description": "Time of block in seconds from epoch", 40 | "example": 1588558709 41 | }, 42 | "transactions": { 43 | "type": "array", 44 | "description": "Block transaction descriptions", 45 | "items": { 46 | "description": "Transaction hash", 47 | "type": "object", 48 | "properties": { 49 | "hash": { 50 | "type": "string", 51 | "description": "Transaction hash" 52 | }, 53 | "type": { 54 | "type": "string", 55 | "description": "Transaction type" 56 | } 57 | } 58 | }, 59 | "example": [ 60 | { 61 | "hash": "UOVRPuEO2IE8y9fxiuO9JBcBLrqP0Hbh7cUqt-n_8QE", 62 | "type": "poc_request_v1" 63 | }, 64 | { 65 | "hash": "67NdSWYjdE8LaR0DE_NNWqMr4XVK8hwrFJ616c9BPmE", 66 | "type": "poc_request_v1" 67 | }, 68 | { 69 | "hash": "KfHpj8ytLV6bqNaMS8wbWXeqXkHxjS-G_U_AAUrFvSQ", 70 | "type": "poc_request_v1" 71 | }, 72 | { 73 | "hash": "r4mgtbBnrY0v6_m01-akrUtZ7KSsLIF4XTJBIUWiaZs", 74 | "type": "poc_request_v1" 75 | }, 76 | { 77 | "hash": "KMFPXYw9QYdW3mtciOuitcWm1qVknm5IDluckN7IcaY", 78 | "type": "poc_request_v1" 79 | }, 80 | { 81 | "hash": "1cpS1AnemprqCmm8SHq9_S-eiCE6zjzf2QsOIaV4GgI", 82 | "type": "poc_request_v1" 83 | }, 84 | { 85 | "hash": "1Rh4iR3eBQIIywqSQ0TCO04tdl2Dl7dW4qWng5q65Es", 86 | "type": "poc_request_v1" 87 | } 88 | ] 89 | } 90 | }, 91 | "required": ["hash", "height", "prev_hash", "time", "transactions"] 92 | }, 93 | "transaction": { 94 | "type": "object", 95 | "description": "Transaction details. The exact fields returned depend on the transaction type returned in the result.", 96 | "properties": { 97 | "hash": { 98 | "type": "string", 99 | "description": "B64 hash of the transaction" 100 | }, 101 | "type": { 102 | "type": "string", 103 | "description": "The type of the transaction" 104 | }, 105 | "implicit_burn": { 106 | "$ref": "#/definitions/implicit_burn" 107 | } 108 | }, 109 | "required": ["type", "hash"] 110 | }, 111 | "pending_transaction": { 112 | "type": "object", 113 | "description": "Pending transaction details. The exact fields returned depend on the transaction type returned in the result. The transaction will be absent if status is cleared or failed", 114 | "properties": { 115 | "txn": { 116 | "$ref": "#/definitions/transaction" 117 | }, 118 | "status": { 119 | "type": "string", 120 | "description": "One of pending, cleared or failed" 121 | }, 122 | "failed_reason": { 123 | "type": "string", 124 | "description": "Present during failed status" 125 | } 126 | }, 127 | "required": ["status"] 128 | }, 129 | "backup_info": { 130 | "type": "object", 131 | "properties": { 132 | "backup_id": { 133 | "type": "integer", 134 | "description": "ID of the backup", 135 | "example": 2 136 | }, 137 | "number_files": { 138 | "type": "integer", 139 | "description": "Number of files in the backup", 140 | "example": 3 141 | }, 142 | "size": { 143 | "type": "integer", 144 | "description": "Size of backup, in bytes" 145 | }, 146 | "timestamp": { 147 | "type": "integer", 148 | "description": "Timestamp (seconds since epoch) of backup" 149 | } 150 | }, 151 | "required": ["backup_id", "number_files", "size", "timestamp"] 152 | }, 153 | "oracle_price": { 154 | "type": "object", 155 | "description": "Oracle Price", 156 | "properties": { 157 | "price": { 158 | "type": "number", 159 | "description": "The oracle price at the indicated height", 160 | "example": 131069500 161 | }, 162 | "height": { 163 | "type": "number", 164 | "description": "The block height of the oracle price", 165 | "example": 633936 166 | } 167 | }, 168 | "required": ["price", "height"] 169 | }, 170 | "implicit_burn": { 171 | "type": "object", 172 | "description": "Implicit burn details", 173 | "properties": { 174 | "fee": { 175 | "type": "number", 176 | "description": "Amount of HNT (in bones) burned for the fee of the corresponding transaction", 177 | "example": 1401125 178 | }, 179 | "payer": { 180 | "type": "string", 181 | "description": "Address of the account that paid the fee", 182 | "example": "1b93cMbumsxd2qgahdn7dZ19rzNJ7KxEHsLfT4zQXiS9YnbR39F" 183 | } 184 | }, 185 | "required": ["fee", "payer"] 186 | }, 187 | "htlc_receipt": { 188 | "type": "object", 189 | "description": "HTLC details", 190 | "properties": { 191 | "address": { 192 | "type": "string", 193 | "description": "B58 address of the HTLC", 194 | "example": "13BnsQ6rZVHXHxT8tgYX6njGxppkVEEcAxDdHV51Vwikrh8XBP9" 195 | }, 196 | "balance": { 197 | "type": "number", 198 | "description": "Amount of HNT locked", 199 | "example": 10 200 | }, 201 | "hashlock": { 202 | "type": "string", 203 | "description": "Hash to unlock HTLC", 204 | "example": "AQEFmiouhIzFHBeCyW4J3sBKvBD3m2yuktTxUf14cIo" 205 | }, 206 | "payee": { 207 | "type": "string", 208 | "description": "Address of the payee", 209 | "example": "14zemQxLLimdTkHnpBU8f6o3DMmU9QfrreqsR1rYUF4tLveyc62" 210 | }, 211 | "payer": { 212 | "type": "string", 213 | "description": "Address of the payer", 214 | "example": "13udMhCkD4RmVCKKtprt96UAEBMppT55fs1z9viS6Uha8EWSWGe" 215 | }, 216 | "redeemed_at": { 217 | "type": "number", 218 | "description": "Block height at which HTLC was redeemed", 219 | "example": 930213 220 | }, 221 | "timelock": { 222 | "type": "number", 223 | "description": "Number of blocks HTLC is locked for until payer can reclaim", 224 | "example": 100 225 | } 226 | }, 227 | "required": [ 228 | "address", 229 | "balance", 230 | "hashlock", 231 | "payee", 232 | "payer", 233 | "timelock" 234 | ] 235 | } 236 | }, 237 | "methods": { 238 | "block_height": { 239 | "summary": "Gets the stored height of the blockchain.", 240 | "description": "Gets the stored height of the blockchain.", 241 | "tags": ["blocks"], 242 | "result": { 243 | "type": "number", 244 | "description": "Block height", 245 | "example": 318492 246 | } 247 | }, 248 | "block_get": { 249 | "summary": "Get a block by height or hash.", 250 | "description": "Gets a block with it's transaction hashes given a block height or block hash.", 251 | "tags": ["blocks"], 252 | "params": { 253 | "type": "object", 254 | "properties": { 255 | "height": { 256 | "description": "Block height to fetch", 257 | "type": "number", 258 | "example": 318492 259 | }, 260 | "hash": { 261 | "description": "Block hash to fetch", 262 | "type": "number" 263 | } 264 | } 265 | }, 266 | "result": { 267 | "$ref": "#/definitions/block" 268 | }, 269 | "errors": [ 270 | { 271 | "code": -100, 272 | "description": "Block not found" 273 | }, 274 | { 275 | "code": -150, 276 | "description": "Failed to get block" 277 | }, 278 | { 279 | "code": -3602, 280 | "description": "Invalid parameter" 281 | } 282 | ] 283 | }, 284 | "account_get": { 285 | "summary": "Get account details.", 286 | "description": "Get account details for a given account address.", 287 | "tags": ["accounts"], 288 | "params": { 289 | "type": "object", 290 | "properties": { 291 | "address": { 292 | "type": "string", 293 | "description": "B58 address of the account to fetch" 294 | } 295 | }, 296 | "required": ["address"] 297 | }, 298 | "result": { 299 | "type": "object", 300 | "description": "Account", 301 | "properties": { 302 | "address": { 303 | "type": "string", 304 | "description": "Address of the account", 305 | "example": "13Ya3s4k8dsbd1dey6dmiYbwk4Dk1MRFCi3RBQ7nwKnSZqnYoW5" 306 | }, 307 | "balance": { 308 | "type": "number", 309 | "description": "HNT balance of the account in bones", 310 | "example": 1000 311 | }, 312 | "mobile_balance": { 313 | "type": "number", 314 | "description": "MOBILE balance of the account in bones", 315 | "example": 1000 316 | }, 317 | "iot_balance": { 318 | "type": "number", 319 | "description": "IOT balance of the account in bones", 320 | "example": 1000 321 | }, 322 | "nonce": { 323 | "type": "number", 324 | "description": "The current nonce for the account", 325 | "example": 3 326 | }, 327 | "speculative_nonce": { 328 | "type": "number", 329 | "description": "The larger of the maximum pending balance nonce or the current nonce", 330 | "example": 12 331 | }, 332 | "dc_balance": { 333 | "type": "number", 334 | "description": "Data credit balance of the account", 335 | "example": 0 336 | }, 337 | "dc_nonce": { 338 | "type": "number", 339 | "description": "The current data credit nonce for the account", 340 | "example": 0 341 | }, 342 | "sec_balance": { 343 | "type": "number", 344 | "description": "Security token balance of the account", 345 | "example": 0 346 | }, 347 | "sec_nonce": { 348 | "type": "number", 349 | "description": "The current security token nonce for the account (deprecated).", 350 | "example": 0 351 | }, 352 | "sec_speculative_nonce": { 353 | "type": "number", 354 | "description": "The larger of the maximum pending security nonce or the current security token nonce for the account (deprecated)", 355 | "example": 0 356 | }, 357 | "staked_balance": { 358 | "type": "number", 359 | "description": "Staked HNT balance of the account", 360 | "example": 0 361 | }, 362 | "cooldown_balance": { 363 | "type": "number", 364 | "description": "Staked HNT balance of the account currently in cooldown", 365 | "example": 0 366 | } 367 | }, 368 | "required": [ 369 | "address", 370 | "balance", 371 | "speculative_nonce", 372 | "nonce", 373 | "dc_balance", 374 | "dc_nonce", 375 | "sec_balance", 376 | "iot_balance", 377 | "mobile_balance", 378 | "staked_balance", 379 | "cooldown_balance" 380 | ] 381 | } 382 | }, 383 | "transaction_get": { 384 | "summary": "Get transaction details.", 385 | "description": "Get details for a given transaction hash.", 386 | "tags": ["transactions"], 387 | "params": { 388 | "type": "object", 389 | "properties": { 390 | "hash": { 391 | "type": "string", 392 | "description": "B64 hash of the transaction to fetch" 393 | } 394 | }, 395 | "required": ["hash"] 396 | }, 397 | "result": { 398 | "$ref": "#/definitions/transaction" 399 | }, 400 | "errors": [ 401 | { 402 | "code": -100, 403 | "description": "Transaction not found" 404 | }, 405 | { 406 | "code": -150, 407 | "description": "Failed to get transaction" 408 | } 409 | ] 410 | }, 411 | "oracle_price_current": { 412 | "summary": "Gets the current oracle price.", 413 | "description": "Gets the oracle price at the current height of the blockchain.", 414 | "tags": ["oracles"], 415 | "result": { 416 | "$ref": "#/definitions/oracle_price" 417 | } 418 | }, 419 | "oracle_price_get": { 420 | "summary": "Gets an oracle price at a height.", 421 | "description": "Gets the oracle price at the given height of the blockchain (if known).", 422 | "tags": ["oracles"], 423 | "params": { 424 | "type": "object", 425 | "properties": { 426 | "height": { 427 | "type": "number", 428 | "description": "Block height to get the oracle price for." 429 | } 430 | }, 431 | "required": ["height"] 432 | }, 433 | "result": { 434 | "$ref": "#/definitions/oracle_price" 435 | } 436 | }, 437 | "pending_transaction_get": { 438 | "summary": "Get a pending transaction.", 439 | "description": "Get the previously submitted transaction with status.", 440 | "tags": ["pending transactions"], 441 | "params": { 442 | "type": "object", 443 | "properties": { 444 | "hash": { 445 | "type": "string", 446 | "description": "B64 hash of the pending transaction to fetch", 447 | "example": "xG-KdomBEdp4gTiJO1Riif92DoMd5hPxadcSci05pIs" 448 | } 449 | }, 450 | "required": ["hash"] 451 | }, 452 | "result": { 453 | "$ref": "#/definitions/pending_transaction" 454 | }, 455 | "errors": [ 456 | { 457 | "code": -100, 458 | "description": "Pending transaction not found" 459 | } 460 | ] 461 | }, 462 | "pending_transaction_status": { 463 | "summary": "Get pending transaction status.", 464 | "description": "Get the status a previously submitted transaction.", 465 | "tags": ["pending transactions"], 466 | "params": { 467 | "type": "object", 468 | "properties": { 469 | "hash": { 470 | "type": "string", 471 | "description": "B64 hash of the pending transaction to fetch", 472 | "example": "xG-KdomBEdp4gTiJO1Riif92DoMd5hPxadcSci05pIs" 473 | } 474 | }, 475 | "required": ["hash"] 476 | }, 477 | "result": { 478 | "type": "string", 479 | "description": "One of 'pending', 'cleared', 'not_found' or a failure reason", 480 | "example": "cleared" 481 | }, 482 | "errors": [ 483 | { 484 | "code": -100, 485 | "description": "Pending transaction not found" 486 | } 487 | ] 488 | }, 489 | "pending_transaction_submit": { 490 | "summary": "Submit a transaction to the pending queue.", 491 | "description": "Submits a pending transaction to the pending queue. The transactions needs to be in a blockchain_txn envelope and base64 encoded", 492 | "tags": ["pending transactions"], 493 | "params": { 494 | "type": "object", 495 | "properties": { 496 | "txn": { 497 | "type": "string", 498 | "description": "B64 encoded transaction", 499 | "example": "QoWBCIe..." 500 | } 501 | }, 502 | "required": ["txn"] 503 | }, 504 | "result": { 505 | "$ref": "#/definitions/transaction" 506 | }, 507 | "errors": [ 508 | { 509 | "code": -3602, 510 | "description": "Invalid parameter" 511 | } 512 | ] 513 | }, 514 | "pending_transaction_verify": { 515 | "summary": "Verify a transaction prior to submitting to the pending queue.", 516 | "description": "Verifies a transaction prior to submitting to the pending queue. The transactions needs to be in a blockchain_txn envelope and base64 encoded. Result returns \"valid\" if the transaction is valid; otherwise, the error message is present.", 517 | "tags": ["pending transactions"], 518 | "params": { 519 | "type": "object", 520 | "properties": { 521 | "txn": { 522 | "type": "string", 523 | "description": "B64 encoded transaction", 524 | "example": "QoWBCIe..." 525 | } 526 | }, 527 | "required": ["txn"] 528 | }, 529 | "result": "string", 530 | "errors": [ 531 | { 532 | "code": -3602, 533 | "description": "Invalid parameter" 534 | } 535 | ] 536 | }, 537 | "implicit_burn_get": { 538 | "summary": "Gets an implicit burn for a transaction hash.", 539 | "description": "Gets an implicit burn for a transaction hash. Returns amount of HNT burned for a DC fee.", 540 | "tags": ["transactions"], 541 | "params": { 542 | "type": "object", 543 | "properties": { 544 | "hash": { 545 | "type": "string", 546 | "description": "Transaction hash to get implicit burn for.", 547 | "example": "13BnsQ6rZVHXHxT8tgYX6njGxppkVEEcAxDdHV51Vwikrh8XBP9" 548 | } 549 | }, 550 | "required": ["hash"] 551 | }, 552 | "result": { 553 | "$ref": "#/definitions/implicit_burn" 554 | }, 555 | "errors": [ 556 | { 557 | "code": -100, 558 | "description": "Implicit burn not found for transaction" 559 | } 560 | ] 561 | }, 562 | "htlc_get": { 563 | "summary": "Gets HTLC details for an HTLC address.", 564 | "description": "Gets HTLC details for an HTLC address. If an HTLC was redeemed, it will also show the redemption height.", 565 | "tags": ["htlc"], 566 | "params": { 567 | "type": "object", 568 | "properties": { 569 | "address": { 570 | "type": "string", 571 | "description": "HTLC address", 572 | "example": "13BnsQ6rZVHXHxT8tgYX6njGxppkVEEcAxDdHV51Vwikrh8XBP9" 573 | } 574 | }, 575 | "required": ["address"] 576 | }, 577 | "result": { 578 | "$ref": "#/definitions/htlc_receipt" 579 | }, 580 | "errors": [ 581 | { 582 | "code": -100, 583 | "description": "HTLC not found" 584 | } 585 | ] 586 | }, 587 | "wallet_create": { 588 | "summary": "Create a new wallet.", 589 | "description": "Creates a new wallet, encrypted with the given password. The wallet is locked after creation.", 590 | "tags": ["wallets"], 591 | "params": { 592 | "type": "object", 593 | "properties": { 594 | "password": { 595 | "type": "string", 596 | "description": "Password used to encrypt the wallet", 597 | "example": "a password" 598 | } 599 | }, 600 | "required": ["password"] 601 | }, 602 | "result": { 603 | "type": "string", 604 | "description": "The B58 encoded public address of the wallet", 605 | "example": "13Ya3s4k8dsbd1dey6dmiYbwk4Dk1MRFCi3RBQ7nwKnSZqnYoW5" 606 | } 607 | }, 608 | "wallet_delete": { 609 | "summary": "Delets a wallet.", 610 | "description": "Permanently removes the wallet from the database.", 611 | "tags": ["wallets"], 612 | "params": { 613 | "type": "object", 614 | "properties": { 615 | "address": { 616 | "type": "string", 617 | "description": "B58 address of the wallet to delete", 618 | "example": "13Ya3s4k8dsbd1dey6dmiYbwk4Dk1MRFCi3RBQ7nwKnSZqnYoW5" 619 | } 620 | }, 621 | "required": ["address"] 622 | }, 623 | "result": { 624 | "type": "boolean", 625 | "description": "Returns true if the wallet was deleted", 626 | "example": true 627 | } 628 | }, 629 | "wallet_list": { 630 | "summary": "List all wallets.", 631 | "description": "Lists the public keys of all wallets.", 632 | "tags": ["wallets"], 633 | "result": { 634 | "type": "array", 635 | "items": { 636 | "type": "string", 637 | "description": "The B58 encoded public address of a wallet" 638 | }, 639 | "example": ["13Ya3s4k8dsbd1dey6dmiYbwk4Dk1MRFCi3RBQ7nwKnSZqnYoW5"] 640 | } 641 | }, 642 | "wallet_unlock": { 643 | "summary": "Unlock a wallet for signing.", 644 | "description": "Unlock a wallet for signing. The wallet will be unlocked for 60 seonds.", 645 | "tags": ["wallets"], 646 | "params": { 647 | "type": "object", 648 | "properties": { 649 | "address": { 650 | "type": "string", 651 | "description": "B58 address of the wallet to unlock", 652 | "example": "13Ya3s4k8dsbd1dey6dmiYbwk4Dk1MRFCi3RBQ7nwKnSZqnYoW5" 653 | }, 654 | "password": { 655 | "type": "string", 656 | "description": "Password used to decrypt the wallet", 657 | "example": "a password" 658 | } 659 | }, 660 | "required": ["address", "password"] 661 | }, 662 | "result": { 663 | "type": "boolean", 664 | "description": "Returns true if the wallet is unlocked", 665 | "example": true 666 | }, 667 | "errors": [ 668 | { 669 | "code": -100, 670 | "description": "Wallet not found" 671 | } 672 | ] 673 | }, 674 | "wallet_lock": { 675 | "summary": "Lock a wallet.", 676 | "description": "Locks a previously unlocked wallet.", 677 | "tags": ["wallets"], 678 | "params": { 679 | "type": "object", 680 | "properties": { 681 | "address": { 682 | "type": "string", 683 | "description": "B58 address of the wallet to lock", 684 | "example": "13Ya3s4k8dsbd1dey6dmiYbwk4Dk1MRFCi3RBQ7nwKnSZqnYoW5" 685 | } 686 | }, 687 | "required": ["address"] 688 | }, 689 | "result": { 690 | "type": "boolean", 691 | "description": "Returns true regardless of whether the wallet is found or not", 692 | "example": true 693 | } 694 | }, 695 | "wallet_is_locked": { 696 | "summary": "Checks if a wallet is locked.", 697 | "description": "Checks if a wallet is unlocked.", 698 | "tags": ["wallets"], 699 | "params": { 700 | "type": "object", 701 | "properties": { 702 | "address": { 703 | "type": "string", 704 | "description": "B58 address of the wallet to check", 705 | "example": "13Ya3s4k8dsbd1dey6dmiYbwk4Dk1MRFCi3RBQ7nwKnSZqnYoW5" 706 | } 707 | }, 708 | "required": ["address"] 709 | }, 710 | "result": { 711 | "type": "boolean", 712 | "description": "Returns true if the wallet is locked or uknown", 713 | "example": true 714 | } 715 | }, 716 | "wallet_pay": { 717 | "summary": "Send a payment to another account.", 718 | "description": "Sends a single payment in bones to a given account address. Note that 1 HNT it 100_000_000 bones", 719 | "tags": ["wallets"], 720 | "params": { 721 | "type": "object", 722 | "properties": { 723 | "address": { 724 | "type": "string", 725 | "description": "B58 address of the payer wallet", 726 | "example": "13Ya3s4k8dsbd1dey6dmiYbwk4Dk1MRFCi3RBQ7nwKnSZqnYoW5" 727 | }, 728 | "payee": { 729 | "type": "string", 730 | "description": "B58 address of the payee account", 731 | "example": "13buBykFQf5VaQtv7mWj2PBY9Lq4i1DeXhg7C4Vbu3ppzqqNkTH" 732 | }, 733 | "bones": { 734 | "type": "integer", 735 | "description": "Amount in bones to send. Must be specified if \"max\" = false.", 736 | "example": 1000 737 | }, 738 | "token_type": { 739 | "type": "string", 740 | "description": "Token type to send. [hnt, mobile, iot, hst. Default: hnt]", 741 | "example": "hnt" 742 | }, 743 | "max": { 744 | "type": "boolean", 745 | "description": "If true, send entire wallet balance rather than specific amount.", 746 | "example": "false" 747 | }, 748 | "nonce": { 749 | "type": "integer", 750 | "description": "Nonce to use for transaction", 751 | "example": 422 752 | } 753 | }, 754 | "required": ["address", "payee"] 755 | }, 756 | "result": { 757 | "$ref": "#/definitions/transaction" 758 | }, 759 | "errors": [ 760 | { 761 | "code": -100, 762 | "description": "Wallet not found or locked" 763 | } 764 | ] 765 | }, 766 | "wallet_pay_multi": { 767 | "summary": "Send multiple paymens in a single transation.", 768 | "description": "Sends multiple payments in bones to one or more payees. Note that 1 HNT it 100_000_000 bones", 769 | "tags": ["wallets"], 770 | "params": { 771 | "type": "object", 772 | "properties": { 773 | "address": { 774 | "type": "string", 775 | "description": "B58 address of the payer wallet" 776 | }, 777 | "payments": { 778 | "type": "array", 779 | "items": { 780 | "type": "object", 781 | "properties": { 782 | "payee": { 783 | "type": "string", 784 | "description": "B58 address of the payee account" 785 | }, 786 | "bones": { 787 | "type": "integer", 788 | "description": "Amount in bones to send. Must be specified if \"max\" = false" 789 | }, 790 | "token_type": { 791 | "type": "string", 792 | "description": "Token type to send. [hnt, mobile, iot, hst. Default: hnt]", 793 | "example": "hnt" 794 | }, 795 | "max": { 796 | "type": "boolean", 797 | "description": "If true, send entire wallet balance rather than specific amount. Only one payment entry per token type can have \"max\" set to true.", 798 | "example": "false" 799 | } 800 | } 801 | } 802 | } 803 | }, 804 | "required": ["address", "payments"] 805 | }, 806 | "result": { 807 | "$ref": "#/definitions/transaction" 808 | }, 809 | "errors": [ 810 | { 811 | "code": -100, 812 | "description": "Wallet not found or locked" 813 | } 814 | ] 815 | }, 816 | "wallet_import": { 817 | "summary": "Import an encrypted wallet.", 818 | "description": "Import an encrypted wallet into the wallet database. The password is only used to verify that the wallet can be unlocked and is not stored.", 819 | "tags": ["wallets"], 820 | "params": { 821 | "type": "object", 822 | "properties": { 823 | "password": { 824 | "type": "string", 825 | "description": "Password used to decrypt the wallet", 826 | "example": "a password" 827 | }, 828 | "path": { 829 | "type": "string", 830 | "description": "Path to the file to import the wallet from" 831 | } 832 | }, 833 | "required": ["password", "path"] 834 | }, 835 | "result": { 836 | "type": "string", 837 | "description": "The public key of the wallet" 838 | }, 839 | "errors": [ 840 | { 841 | "code": -100, 842 | "description": "Wallet file not found" 843 | }, 844 | { 845 | "code": -110, 846 | "description": "Invalid password for wallet" 847 | } 848 | ] 849 | }, 850 | "wallet_export": { 851 | "summary": "Export an encrypted wallet to a given path.", 852 | "description": "Exports an encrypted wallet to the given path.", 853 | "tags": ["wallets"], 854 | "params": { 855 | "type": "object", 856 | "properties": { 857 | "address": { 858 | "type": "string", 859 | "description": "B58 address of the payer wallet" 860 | }, 861 | "path": { 862 | "type": "string", 863 | "description": "Path to the file to save the wallet to" 864 | } 865 | }, 866 | "required": ["address", "path"] 867 | }, 868 | "errors": [ 869 | { 870 | "code": -100, 871 | "description": "Wallet not found" 872 | } 873 | ] 874 | }, 875 | "wallet_export_secret": { 876 | "summary": "Export the secret key bytes for a wallet to a given path.", 877 | "description": "Exports the secret keybytes of a given unlocked wallet to the given path.", 878 | "tags": ["wallets"], 879 | "params": { 880 | "type": "object", 881 | "properties": { 882 | "address": { 883 | "type": "string", 884 | "description": "B58 address of the wallet" 885 | }, 886 | "path": { 887 | "type": "string", 888 | "description": "Path to the file to save the export to" 889 | } 890 | }, 891 | "required": ["address", "path"] 892 | }, 893 | "errors": [ 894 | { 895 | "code": -100, 896 | "description": "Wallet not found" 897 | } 898 | ] 899 | }, 900 | "wallet_backup_list": { 901 | "summary": "Lists information on the list of backups in the given path.", 902 | "description": "Backup list information includes the backup ID, size, and the time the backup was created.", 903 | "tags": ["wallets", "backups"], 904 | "params": { 905 | "type": "object", 906 | "properties": { 907 | "path": { 908 | "type": "string", 909 | "description": "Path to the backup folder" 910 | } 911 | }, 912 | "required": ["path"] 913 | }, 914 | "result": { 915 | "type": "array", 916 | "items": { 917 | "$ref": "#/definitions/backup_info" 918 | } 919 | } 920 | }, 921 | "wallet_backup_create": { 922 | "summary": "Creates a backup of the wallet database.", 923 | "description": "Creates a backup of the backup database in the given path.", 924 | "tags": ["wallets", "backups"], 925 | "params": { 926 | "type": "object", 927 | "properties": { 928 | "path": { 929 | "type": "string", 930 | "description": "Path to the backup folder" 931 | }, 932 | "max_backups": { 933 | "type": "integer", 934 | "description": "Maximum number of backups to maintain in the folder" 935 | } 936 | }, 937 | "required": ["path", "max_backups"] 938 | }, 939 | "result": { 940 | "$ref": "#/definitions/backup_info" 941 | } 942 | }, 943 | "wallet_backup_delete": { 944 | "summary": "Delete a backup.", 945 | "description": "Delete the backup with the given ID from the given backup path.", 946 | "tags": ["wallets", "backups"], 947 | "params": { 948 | "type": "object", 949 | "properties": { 950 | "path": { 951 | "type": "string", 952 | "description": "Path to the backup folder" 953 | }, 954 | "backup_id": { 955 | "type": "integer", 956 | "description": "Backup ID to delete" 957 | } 958 | }, 959 | "required": ["path", "backup_id"] 960 | }, 961 | "result": { 962 | "type": "boolean", 963 | "description": "True if the backup was deleted succesfully" 964 | }, 965 | "errors": [ 966 | { 967 | "code": -100, 968 | "description": "Backup not found" 969 | } 970 | ] 971 | }, 972 | "wallet_backup_restore": { 973 | "summary": "Restore the wallet database.", 974 | "description": "Restores the wallet database from the backup ID in the given backup folder.", 975 | "tags": ["wallets", "backups"], 976 | "params": { 977 | "type": "object", 978 | "properties": { 979 | "path": { 980 | "type": "string", 981 | "description": "Path to the backup folder" 982 | }, 983 | "backup_id": { 984 | "type": "integer", 985 | "description": "Backup ID to restore from" 986 | } 987 | }, 988 | "required": ["path", "backup_id"] 989 | }, 990 | "result": { 991 | "type": "boolean", 992 | "description": "True if the backup was restored succesfully" 993 | }, 994 | "errors": [ 995 | { 996 | "code": -100, 997 | "description": "Backup not found" 998 | } 999 | ] 1000 | } 1001 | } 1002 | } 1003 | -------------------------------------------------------------------------------- /priv/genesis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/blockchain-node/5be28488f24ffd09832c5e57e93c22a3dc244e9c/priv/genesis -------------------------------------------------------------------------------- /priv/genesis_devnet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/blockchain-node/5be28488f24ffd09832c5e57e93c22a3dc244e9c/priv/genesis_devnet -------------------------------------------------------------------------------- /priv/genesis_testnet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/blockchain-node/5be28488f24ffd09832c5e57e93c22a3dc244e9c/priv/genesis_testnet -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | {erl_opts, [ 3 | debug_info, 4 | {parse_transform, lager_transform} 5 | %% warnings_as_errors 6 | ]}. 7 | 8 | {plugins, [ 9 | {grpcbox_plugin, {git, "https://github.com/novalabsxyz/grpcbox_plugin.git", {branch, "andymck/ts-master/combined-opts-and-template-changes"}}}, 10 | erlfmt 11 | ]}. 12 | 13 | {cover_export_enabled, true}. 14 | 15 | {cover_enabled, true}. 16 | 17 | {covertool, [ 18 | {coverdata_files, [ 19 | "ct.coverdata", 20 | "eunit.coverdata" 21 | ]} 22 | ]}. 23 | 24 | {deps, [ 25 | {helium_proto, {git, "https://github.com/helium/proto.git", {branch, "master"}}}, 26 | {blockchain, {git, "https://github.com/helium/blockchain-core.git", {branch, "master"}}}, 27 | {elli, "3.3.0"}, 28 | jsone, 29 | {jsonrpc2, {git, "https://github.com/novalabsxyz/jsonrpc2-erlang.git", {branch, "master"}}}, 30 | {observer_cli, "1.7.1"}, 31 | {telemetry, "1.1.0"}, 32 | {prometheus, "4.8.2"}, 33 | pbkdf2 34 | ]}. 35 | 36 | {xref_checks, [ 37 | undefined_function_calls, 38 | undefined_functions 39 | ]}. 40 | 41 | {shell, [ 42 | {apps, [lager, grpcbox]} 43 | ]}. 44 | 45 | {pre_hooks, [ 46 | {"(linux|darwin|solaris)", compile, "make grpc"}, 47 | {"(freebsd|openbsd|netbsd)", compile, "gmake grpc"}, 48 | {"(linux|darwin|solaris)", clean, "make clean_grpc"}, 49 | {"(freebsd|openbsd|netbsd)", clean, "gmake clean_grpc"} 50 | ]}. 51 | 52 | {relx, [ 53 | {release, {blockchain_node, git}, [ 54 | blockchain_node 55 | ]}, 56 | {vm_args, "./config/vm.args"}, 57 | {sys_config, "./config/dev.config"}, 58 | {extended_start_script, true}, 59 | {include_src, true}, 60 | {extended_start_script_hooks, [ 61 | {post_start, [ 62 | {wait_for_process, blockchain_worker} 63 | ]} 64 | ]}, 65 | {extended_start_script_extensions, [ 66 | {genesis, "extensions/genesis"}, 67 | {info, "extensions/info"}, 68 | {peer, "extensions/peer"}, 69 | {ledger, "extensions/ledger"}, 70 | {trace, "extensions/trace"}, 71 | {snapshot, "extensions/snapshot"}, 72 | {repair, "extensions/repair"}, 73 | {txn, "extensions/txn"} 74 | ]}, 75 | {overlay, [ 76 | {copy, "config/sys.config", "config/sys.config"}, 77 | {copy, "priv/genesis", "update/genesis"}, 78 | {copy, "./_build/default/lib/blockchain/scripts/extensions/peer", "bin/extensions/peer"}, 79 | {copy, "./_build/default/lib/blockchain/scripts/extensions/ledger", 80 | "bin/extensions/ledger"}, 81 | {copy, "./_build/default/lib/blockchain/scripts/extensions/trace", "bin/extensions/trace"}, 82 | {copy, "./_build/default/lib/blockchain/scripts/extensions/txn", "bin/extensions/txn"}, 83 | {copy, "./_build/default/lib/blockchain/scripts/extensions/snapshot", 84 | "bin/extensions/snapshot"}, 85 | {copy, "./_build/default/lib/blockchain/scripts/extensions/repair", 86 | "bin/extensions/repair"}, 87 | {copy, "deb/node.config", "etc/node.config"}, 88 | {template, "config/vm.args", "{{output_dir}}/releases/{{release_version}}/vm.args"} 89 | ]} 90 | ]}. 91 | 92 | {profiles, [ 93 | {test, [ 94 | {relx, [ 95 | {sys_config, "./config/test.config"}, 96 | {dev_mode, false}, 97 | {include_erts, false}, 98 | {include_src, false}, 99 | {overrides, [{add, blockchain, [{erl_opts, [{d, 'TEST'}]}]}]}, 100 | {deps, [ 101 | {miner_test, {git, "https://github.com/helium/miner-test.git", {branch, "master"}}} 102 | ]} 103 | ]} 104 | ]}, 105 | {dev, [ 106 | {relx, [ 107 | {sys_config, "./config/dev.config"}, 108 | {dev_mode, true}, 109 | {include_src, false}, 110 | {include_erts, false} 111 | ]} 112 | ]}, 113 | {dev_testnet, [ 114 | {relx, [ 115 | {release, {blockchain_node, {semver, "testnet"}}, [blockchain_node]}, 116 | {sys_config, "./config/dev_testnet.config"}, 117 | {dev_mode, false}, 118 | {include_src, false}, 119 | {include_erts, true}, 120 | {overlay, [ 121 | {copy, "priv/genesis_testnet", "update/genesis"} 122 | ]} 123 | ]} 124 | ]}, 125 | {devnet, [ 126 | {relx, [ 127 | {release, {blockchain_node, {semver, "devnet"}}, [blockchain_node]}, 128 | {sys_config, "./config/dev_testnet.config"}, 129 | {dev_mode, false}, 130 | {include_src, false}, 131 | {include_erts, true}, 132 | {overlay, [ 133 | {copy, "priv/genesis_testnet", "update/genesis"} 134 | ]} 135 | ]} 136 | ]}, 137 | {prod, [ 138 | {relx, [ 139 | {release, {blockchain_node, semver}, [blockchain_node]}, 140 | {sys_config, "./config/prod.config"}, 141 | {dev_mode, false}, 142 | {debug_info, keep}, 143 | {include_src, false}, 144 | {include_erts, true} 145 | ]} 146 | ]}, 147 | {docker_node, [ 148 | {relx, [ 149 | {sys_config_src, "./config/docker_node.config.src"}, 150 | {dev_mode, false}, 151 | {include_erts, true} 152 | ]} 153 | ]}, 154 | {local, [ 155 | {relx, [ 156 | {sys_config, "./config/local.config"}, 157 | {dev_mode, false}, 158 | {include_src, false}, 159 | {include_erts, false} 160 | ]} 161 | ]} 162 | ]}. 163 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | os:putenv("ERLANG_ROCKSDB_OPTS", "-DWITH_BUNDLE_SNAPPY=ON -DWITH_BUNDLE_LZ4=ON"), 2 | CONFIG. -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"acceptor_pool">>, 3 | {git,"https://github.com/novalabsxyz/acceptor_pool", 4 | {ref,"7e53c5cec2c867838576237c6d016e1eb5501f03"}}, 5 | 2}, 6 | {<<"backoff">>,{pkg,<<"backoff">>,<<"1.1.6">>},2}, 7 | {<<"base32">>,{pkg,<<"base32">>,<<"0.1.0">>},4}, 8 | {<<"base64url">>,{pkg,<<"base64url">>,<<"1.0.1">>},1}, 9 | {<<"blockchain">>, 10 | {git,"https://github.com/helium/blockchain-core.git", 11 | {ref,"e3abf5a5eb459836d0aad7b047a1d175a4096581"}}, 12 | 0}, 13 | {<<"certifi">>,{pkg,<<"certifi">>,<<"2.9.0">>},2}, 14 | {<<"chatterbox">>, 15 | {git,"https://github.com/novalabsxyz/chatterbox", 16 | {ref,"cbfe6e46b273f1552b57685c9f6daf710473c609"}}, 17 | 2}, 18 | {<<"clique">>, 19 | {git,"https://github.com/helium/clique.git", 20 | {ref,"e4be0dae150061bec080502ed530091672880867"}}, 21 | 1}, 22 | {<<"cream">>, 23 | {git,"https://github.com/helium/cream", 24 | {ref,"9fd0ff78ab4d30f8ea16212b504c817290dfaf64"}}, 25 | 1}, 26 | {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, 27 | {<<"cuttlefish">>, 28 | {git,"https://github.com/helium/cuttlefish.git", 29 | {ref,"8672838e8f4ef61602aee6e4ff97ec9be54031dc"}}, 30 | 2}, 31 | {<<"ecc_compact">>,{pkg,<<"ecc_compact">>,<<"1.1.1">>},3}, 32 | {<<"elli">>,{pkg,<<"elli">>,<<"3.3.0">>},0}, 33 | {<<"enacl">>, 34 | {git,"https://github.com/helium/enacl", 35 | {ref,"efb74b1af1df7f46f6ade488f16ec365af3ce47f"}}, 36 | 3}, 37 | {<<"erbloom">>, 38 | {git,"https://github.com/Vagabond/erbloom", 39 | {ref,"8fc2a5aa2454bca3b57047c0b3f79f8d9d219483"}}, 40 | 2}, 41 | {<<"erl_angry_purple_tiger">>, 42 | {git,"https://github.com/helium/erl_angry_purple_tiger.git", 43 | {ref,"c5476b6639314a75a99400c9dfa7603b24a6d18a"}}, 44 | 1}, 45 | {<<"erl_base58">>,{pkg,<<"erl_base58">>,<<"0.0.1">>},1}, 46 | {<<"erlang_lorawan">>, 47 | {git,"https://github.com/helium/erlang-lorawan.git", 48 | {ref,"32d1c580c26ba41ad5230d080d4c0e23cf273d86"}}, 49 | 1}, 50 | {<<"erlang_stats">>, 51 | {git,"https://github.com/helium/erlang-stats.git", 52 | {ref,"ba2d545f8e559bd4d46146a5f5277ce4f907c03f"}}, 53 | 1}, 54 | {<<"exor_filter">>, 55 | {git,"https://github.com/mpope9/exor_filter", 56 | {ref,"36a15cf1c12eac7563cc7d50328f2bc53ceb2687"}}, 57 | 1}, 58 | {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},3}, 59 | {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},2}, 60 | {<<"gproc">>,{pkg,<<"gproc">>,<<"0.8.0">>},2}, 61 | {<<"grpcbox">>, 62 | {git,"https://github.com/novalabsxyz/grpcbox.git", 63 | {ref,"e1f0bdbb5408c5d5bb68b5c848c19b89bce90c84"}}, 64 | 1}, 65 | {<<"h3">>, 66 | {git,"https://github.com/helium/erlang-h3.git", 67 | {ref,"90e1b6ebf93f88702ce8d24d9142833a8401e3ab"}}, 68 | 1}, 69 | {<<"hackney">>,{pkg,<<"hackney">>,<<"1.18.1">>},1}, 70 | {<<"helium_proto">>, 71 | {git,"https://github.com/helium/proto.git", 72 | {ref,"8f6218e04cce79cd9cc012147e638b4ba1a6b27e"}}, 73 | 0}, 74 | {<<"hpack">>,{pkg,<<"hpack_erl">>,<<"0.2.3">>},3}, 75 | {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},2}, 76 | {<<"inert">>,{pkg,<<"inert">>,<<"1.0.4">>},3}, 77 | {<<"inet_cidr">>,{pkg,<<"erl_cidr">>,<<"1.0.2">>},3}, 78 | {<<"inet_ext">>, 79 | {git,"https://github.com/benoitc/inet_ext", 80 | {ref,"e30b65d32711a4b7033fd4ac9b33b3c1c8be8bed"}}, 81 | 2}, 82 | {<<"intercept">>,{pkg,<<"intercept">>,<<"1.0.0">>},3}, 83 | {<<"jsone">>,{pkg,<<"jsone">>,<<"1.7.0">>},0}, 84 | {<<"jsonrpc2">>, 85 | {git,"https://github.com/novalabsxyz/jsonrpc2-erlang.git", 86 | {ref,"e37bb4560339415f18529364f26bc404a5b3408f"}}, 87 | 0}, 88 | {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1}, 89 | {<<"lager">>,{pkg,<<"lager">>,<<"3.9.2">>},1}, 90 | {<<"libp2p">>, 91 | {git,"https://github.com/helium/erlang-libp2p.git", 92 | {ref,"bd8c6363a0a414e64f31511e126b7d0b986d8ba4"}}, 93 | 1}, 94 | {<<"libp2p_crypto">>, 95 | {git,"https://github.com/helium/libp2p-crypto.git", 96 | {ref,"a969bb7affc7d3fa1472aef80d736c467d3f6d45"}}, 97 | 2}, 98 | {<<"merkerl">>, 99 | {git,"https://github.com/helium/merkerl.git", 100 | {ref,"26ddcaf7f3c2c76eebf6f9258822f923ce69cb75"}}, 101 | 1}, 102 | {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2}, 103 | {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},2}, 104 | {<<"multiaddr">>,{pkg,<<"multiaddr">>,<<"1.1.3">>},3}, 105 | {<<"multihash">>,{pkg,<<"multihash">>,<<"2.1.0">>},3}, 106 | {<<"nat">>, 107 | {git,"https://github.com/benoitc/erlang-nat", 108 | {ref,"6136102c176814dd26c11b93ca0ce852b66c4195"}}, 109 | 2}, 110 | {<<"observer_cli">>,{pkg,<<"observer_cli">>,<<"1.7.1">>},0}, 111 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2}, 112 | {<<"pbkdf2">>,{pkg,<<"pbkdf2">>,<<"2.0.0">>},0}, 113 | {<<"procket">>,{pkg,<<"procket">>,<<"0.9.7">>},3}, 114 | {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.8.2">>},0}, 115 | {<<"quantile_estimator">>,{pkg,<<"quantile_estimator">>,<<"0.2.1">>},1}, 116 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},2}, 117 | {<<"rand_compat">>,{pkg,<<"rand_compat">>,<<"0.0.3">>},3}, 118 | {<<"recon">>,{pkg,<<"recon">>,<<"2.5.2">>},1}, 119 | {<<"relcast">>, 120 | {git,"https://github.com/helium/relcast.git", 121 | {ref,"613b3e8ca0586faf3bf682e861620885f9b82a76"}}, 122 | 2}, 123 | {<<"rocksdb">>, 124 | {git,"https://gitlab.com/vagabond1/erlang-rocksdb", 125 | {ref,"e8a9a3907a2ecab225122b1cd7c7cb7973a14aee"}}, 126 | 3}, 127 | {<<"sidejob">>,{pkg,<<"sidejob">>,<<"2.1.0">>},2}, 128 | {<<"small_ints">>,{pkg,<<"small_ints">>,<<"0.1.0">>},4}, 129 | {<<"splicer">>,{pkg,<<"splicer">>,<<"0.5.5">>},2}, 130 | {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},2}, 131 | {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.1.0">>},0}, 132 | {<<"throttle">>,{pkg,<<"lambda_throttle">>,<<"0.2.0">>},2}, 133 | {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2}, 134 | {<<"vincenty">>, 135 | {git,"https://github.com/helium/vincenty", 136 | {ref,"e33268c2a497f218d65e068ccec42e59ff5be996"}}, 137 | 1}, 138 | {<<"xxhash">>, 139 | {git,"https://github.com/pierreis/erlang-xxhash", 140 | {ref,"92fcfe57a74d3ea1fe794ccdfdf62ce4a036c01b"}}, 141 | 1}]}. 142 | [ 143 | {pkg_hash,[ 144 | {<<"backoff">>, <<"83B72ED2108BA1EE8F7D1C22E0B4A00CFE3593A67DBC792799E8CCE9F42F796B">>}, 145 | {<<"base32">>, <<"044F6DC95709727CA2176F3E97A41DDAA76B5BC690D3536908618C0CB32616A2">>}, 146 | {<<"base64url">>, <<"F8C7F2DA04CA9A5D0F5F50258F055E1D699F0E8BF4CFDB30B750865368403CF6">>}, 147 | {<<"certifi">>, <<"6F2A475689DD47F19FB74334859D460A2DC4E3252A3324BD2111B8F0429E7E21">>}, 148 | {<<"ctx">>, <<"8FF88B70E6400C4DF90142E7F130625B82086077A45364A78D208ED3ED53C7FE">>}, 149 | {<<"ecc_compact">>, <<"D45197DB76EEBD22B144552795C1950DD07B013D591EA2C3A8DE22F8EE6C132B">>}, 150 | {<<"elli">>, <<"089218762A7FF3D20AE81C8E911BD0F73EE4EE0ED85454226D1FC6B4FFF3B4F6">>}, 151 | {<<"erl_base58">>, <<"37710854461D71DF338E73C65776302DB41C4BAB4674D2EC134ED7BCFC7B5552">>}, 152 | {<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}, 153 | {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}, 154 | {<<"gproc">>, <<"CEA02C578589C61E5341FCE149EA36CCEF236CC2ECAC8691FBA408E7EA77EC2F">>}, 155 | {<<"hackney">>, <<"F48BF88F521F2A229FC7BAE88CF4F85ADC9CD9BCF23B5DC8EB6A1788C662C4F6">>}, 156 | {<<"hpack">>, <<"17670F83FF984AE6CD74B1C456EDDE906D27FF013740EE4D9EFAA4F1BF999633">>}, 157 | {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, 158 | {<<"inert">>, <<"51586E4E77E3C5ABC00E3A12AEE829FB6E44D51E4C7957F7B97C4378A15985DD">>}, 159 | {<<"inet_cidr">>, <<"4814A5B78B969A5E069B0CECBB102622AB0C459B690053ED94543CD529915A43">>}, 160 | {<<"intercept">>, <<"1F6C725E6FC070720643BD4D97EE53B1209365C80E520E1F5A1ACB36712A7EB5">>}, 161 | {<<"jsone">>, <<"1E3BD7D5DD44BB2EB0797DDDEA1CBF2DDAB8D9F29E499A467CA171C23F5984EA">>}, 162 | {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, 163 | {<<"lager">>, <<"4CAB289120EB24964E3886BD22323CB5FEFE4510C076992A23AD18CF85413D8C">>}, 164 | {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, 165 | {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, 166 | {<<"multiaddr">>, <<"978E58E28F6FACAF428C87AF933612B1E2F3F2775F1794EDA5E831A4EACD2984">>}, 167 | {<<"multihash">>, <<"F084F7C6BEC062F0C0E82AE18CFDC8DAEC8F4FAA4C8E1ACE0B9C676A9323162F">>}, 168 | {<<"observer_cli">>, <<"C9CA1F623A3EF0158283A3C37CD7B7235BFE85927AD6E26396DD247E2057F5A1">>}, 169 | {<<"parse_trans">>, <<"16328AB840CC09919BD10DAB29E431DA3AF9E9E7E7E6F0089DD5A2D2820011D8">>}, 170 | {<<"pbkdf2">>, <<"11C23279FDED5C0027AB3996CFAE77805521D7EF4BABDE2BD7EC04A9086CF499">>}, 171 | {<<"procket">>, <<"9F0D5F3D4CCCE98DD01EA2FAE50C360219CBFF6F71545B966DAEEEBF171C49FB">>}, 172 | {<<"prometheus">>, <<"B88F24279DD7A1F512CB090595FF6C88B50AAD0A6B394A4C4983725723DCD834">>}, 173 | {<<"quantile_estimator">>, <<"EF50A361F11B5F26B5F16D0696E46A9E4661756492C981F7B2229EF42FF1CD15">>}, 174 | {<<"ranch">>, <<"F04166F456790FEE2AC1AA05A02745CC75783C2BFB26D39FAF6AEFC9A3D3A58A">>}, 175 | {<<"rand_compat">>, <<"011646BC1F0B0C432FE101B816F25B9BBB74A085713CEE1DAFD2D62E9415EAD3">>}, 176 | {<<"recon">>, <<"CBA53FA8DB83AD968C9A652E09C3ED7DDCC4DA434F27C3EAA9CA47FFB2B1FF03">>}, 177 | {<<"sidejob">>, <<"5D6A7C9C620778CB1908E46B552D767DF2ED4D77070BB7B5B8773D4FF18D1D37">>}, 178 | {<<"small_ints">>, <<"82A824C8794A2DDC73CB5CD00EAD11331DB296521AD16A619C13D668572B868A">>}, 179 | {<<"splicer">>, <<"FD56E168D0F3BFC879858850E4F4572E9E1BA16AC3DC524D573085CA1157EC37">>}, 180 | {<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>}, 181 | {<<"telemetry">>, <<"A589817034A27EAB11144AD24D5C0F9FAB1F58173274B1E9BAE7074AF9CBEE51">>}, 182 | {<<"throttle">>, <<"E881B46D9836AFB70F3E2FA3BE9B0140805BA324ED26AA734FF6C5C1568C6CA7">>}, 183 | {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]}, 184 | {pkg_hash_ext,[ 185 | {<<"backoff">>, <<"CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39">>}, 186 | {<<"base32">>, <<"10A73951D857D8CB1ECEEA8EB96C6941F6A76E105947AD09C2B73977DEE07638">>}, 187 | {<<"base64url">>, <<"F9B3ADD4731A02A9B0410398B475B33E7566A695365237A6BDEE1BB447719F5C">>}, 188 | {<<"certifi">>, <<"266DA46BDB06D6C6D35FDE799BCB28D36D985D424AD7C08B5BB48F5B5CDD4641">>}, 189 | {<<"ctx">>, <<"A14ED2D1B67723DBEBBE423B28D7615EB0BDCBA6FF28F2D1F1B0A7E1D4AA5FC2">>}, 190 | {<<"ecc_compact">>, <<"86F8A8A33141C5390D72BDC2CE4D79B8CC74463A80D263A44FC988D353269504">>}, 191 | {<<"elli">>, <<"698B13B33D05661DB9FE7EFCBA41B84825A379CCE86E486CF6AFF9285BE0CCF8">>}, 192 | {<<"erl_base58">>, <<"41E8EC356C5C5558A45682F61F80725789AE9A11BD1CC7D5C73CDE1E3B546DD2">>}, 193 | {<<"getopt">>, <<"53E1AB83B9CEB65C9672D3E7A35B8092E9BDC9B3EE80721471A161C10C59959C">>}, 194 | {<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>}, 195 | {<<"gproc">>, <<"580ADAFA56463B75263EF5A5DF4C86AF321F68694E7786CB057FD805D1E2A7DE">>}, 196 | {<<"hackney">>, <<"A4ECDAFF44297E9B5894AE499E9A070EA1888C84AFDD1FD9B7B2BC384950128E">>}, 197 | {<<"hpack">>, <<"06F580167C4B8B8A6429040DF36CC93BBA6D571FAEAEC1B28816523379CBB23A">>}, 198 | {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, 199 | {<<"inert">>, <<"1C45F2043FD80B18D7C00410D8DF9B7CA463A3F7479CB4E3A2745C60C81EDDE7">>}, 200 | {<<"inet_cidr">>, <<"97046492E5C5BE0D8B92CD275980D667A8D28D9E79B2305828E358CC7D30A935">>}, 201 | {<<"intercept">>, <<"FC5F5E26A571637B0B80119FE2F1E83DDDEAC8078BD76279C015474AF432B32E">>}, 202 | {<<"jsone">>, <<"A3A33712EE6BC8BE10CFA21C7C425A299DE4C5A8533F9F931E577A6D0E8F5DBD">>}, 203 | {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, 204 | {<<"lager">>, <<"7F904D9E87A8CB7E66156ED31768D1C8E26EBA1D54F4BC85B1AA4AC1F6340C28">>}, 205 | {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>}, 206 | {<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>}, 207 | {<<"multiaddr">>, <<"980D3EA5EB0EB2EFC51E3D10953F17447D417C49BD2FCD7FC6A2A42D1F66D5EE">>}, 208 | {<<"multihash">>, <<"E73AD5D0099DBFFB4EE429A78436B605AFE2530ED684AB36BB86733AB65707C8">>}, 209 | {<<"observer_cli">>, <<"4CCAFAAA2CE01B85DDD14591F4D5F6731B4E13B610A70FB841F0701178478280">>}, 210 | {<<"parse_trans">>, <<"07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B">>}, 211 | {<<"pbkdf2">>, <<"1E793CE6FDB0576613115714DEAE9DFC1D1537EABA74F07EFB36DE139774488D">>}, 212 | {<<"procket">>, <<"262E9C7541AA04BE7A50AC785185AFDF733C38790CAE8EC740B7E9EE8286DB2B">>}, 213 | {<<"prometheus">>, <<"C3ABD6521E52CEC4F0D8ECA603CF214DFC84D8A27AA85946639F1424B8554D98">>}, 214 | {<<"quantile_estimator">>, <<"282A8A323CA2A845C9E6F787D166348F776C1D4A41EDE63046D72D422E3DA946">>}, 215 | {<<"ranch">>, <<"86D40FC42AA47BCB6952DDF1DBFD3DA04B5BA69AFB65C322C99845913250B11F">>}, 216 | {<<"rand_compat">>, <<"CDF7BE2B17308EC245B912C45FE55741F93B6E4F1A24BA6074F7137B0CC09BF4">>}, 217 | {<<"recon">>, <<"2C7523C8DEE91DFF41F6B3D63CBA2BD49EB6D2FE5BF1EEC0DF7F87EB5E230E1C">>}, 218 | {<<"sidejob">>, <<"6DC3DAC041C8C07C64401ECD22684730DA1497F5F14377B3CA9C5B2B9A135181">>}, 219 | {<<"small_ints">>, <<"00B3BFF6C446711F8EA4EA942056F375E0F13C7983CC3950C6EA1DE014C7C416">>}, 220 | {<<"splicer">>, <<"4F99B98309D3017377A417D250BC4E90AC5B408F6639665F0108F9AC888B4BEE">>}, 221 | {<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>}, 222 | {<<"telemetry">>, <<"B727B2A1F75614774CFF2D7565B64D0DFA5BD52BA517F16543E6FC7EFCC0DF48">>}, 223 | {<<"throttle">>, <<"3EACFAAC1C2EBD0F17D77D9E96B1029BF07DED4AC233BA38883D70CDF1FFF740">>}, 224 | {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]} 225 | ]. 226 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/blockchain-node/5be28488f24ffd09832c5e57e93c22a3dc244e9c/rebar3 -------------------------------------------------------------------------------- /src/blockchain_node.app.src: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | {application, blockchain_node, 3 | [ 4 | {description, "A Helium Blockchain Node"}, 5 | {vsn, "0.1.0"}, 6 | {registered, []}, 7 | {mod, {blockchain_node_app, []}}, 8 | { 9 | applications, 10 | [ 11 | kernel, 12 | stdlib, 13 | syntax_tools, 14 | compiler, 15 | lager, 16 | jsone, 17 | jsonrpc2, 18 | base64url, 19 | pbkdf2, 20 | observer_cli, 21 | clique, 22 | recon, 23 | grpcbox, 24 | elli, 25 | telemetry, 26 | prometheus 27 | ] 28 | }, 29 | {included_applications, [blockchain]}, 30 | {env, []}, 31 | {modules, []}, 32 | {licenses, ["Apache 2.0"]}, 33 | {links, []} 34 | ] 35 | }. 36 | -------------------------------------------------------------------------------- /src/blockchain_node_app.erl: -------------------------------------------------------------------------------- 1 | -module(blockchain_node_app). 2 | 3 | -behaviour(application). 4 | -export([start/2, stop/1]). 5 | 6 | start(_StartType, _StartArgs) -> 7 | bn_sup:start_link(). 8 | 9 | stop(_State) -> 10 | ok. 11 | -------------------------------------------------------------------------------- /src/bn_accounts.erl: -------------------------------------------------------------------------------- 1 | -module(bn_accounts). 2 | 3 | -include("bn_jsonrpc.hrl"). 4 | -include_lib("blockchain/include/blockchain_vars.hrl"). 5 | 6 | -behavior(bn_jsonrpc_handler). 7 | 8 | %% jsonrpc_handler 9 | -export([handle_rpc/2]). 10 | %% API 11 | -export([get_speculative_nonce/3]). 12 | 13 | %% 14 | %% jsonrpc_handler 15 | %% 16 | 17 | handle_rpc(<<"account_get">>, {Param}) -> 18 | Chain = blockchain_worker:blockchain(), 19 | {Height, Ledger} = {bn_txns:follower_height(), blockchain:ledger(Chain)}, 20 | Address = ?jsonrpc_b58_to_bin(<<"address">>, Param), 21 | BalanceMap = 22 | case get_token_version(Ledger) of 23 | 1 -> get_balance_entry_v1(Address, Ledger); 24 | 2 -> get_balance_entry_v2(Address, Ledger) 25 | end, 26 | {StakedBalance, CooldownBalance} = blockchain_ledger_v1:fold_validators(fun(Val, {SAcc, CAcc} = Acc) -> 27 | case blockchain_ledger_validator_v1:owner_address(Val) of 28 | Address -> 29 | case blockchain_ledger_validator_v1:status(Val) of 30 | staked -> 31 | {SAcc + blockchain_ledger_validator_v1:stake(Val), CAcc}; 32 | cooldown -> 33 | {SAcc, CAcc + blockchain_ledger_validator_v1:stake(Val)}; 34 | _ -> 35 | Acc 36 | end; 37 | _ -> 38 | Acc 39 | end 40 | end, {0, 0}, Ledger), 41 | InfoMap = 42 | #{ 43 | address => ?BIN_TO_B58(Address), 44 | block => Height, 45 | staked_balance => StakedBalance, 46 | cooldown_balance => CooldownBalance 47 | }, 48 | maps:merge(BalanceMap, InfoMap); 49 | handle_rpc(_, _) -> 50 | ?jsonrpc_error(method_not_found). 51 | 52 | -spec get_balance_entry_v1( 53 | Address :: libp2p_crypto:pubkey_bin(), 54 | Ledger :: blockchain:ledger() 55 | ) -> map(). 56 | get_balance_entry_v1(Address, Ledger) -> 57 | GetBalance = fun() -> 58 | case blockchain_ledger_v1:find_entry(Address, Ledger) of 59 | {ok, Entry} -> 60 | #{ 61 | balance => blockchain_ledger_entry_v1:balance(Entry), 62 | nonce => blockchain_ledger_entry_v1:nonce(Entry), 63 | speculative_nonce => get_speculative_nonce(Address, balance, Ledger, 1) 64 | }; 65 | _ -> 66 | #{ 67 | balance => 0, 68 | nonce => 0, 69 | speculative_nonce => 0 70 | } 71 | end 72 | end, 73 | GetSecurities = fun() -> 74 | case blockchain_ledger_v1:find_security_entry(Address, Ledger) of 75 | {ok, Entry} -> 76 | #{ 77 | sec_balance => blockchain_ledger_security_entry_v1:balance(Entry), 78 | sec_nonce => blockchain_ledger_security_entry_v1:nonce(Entry), 79 | sec_speculative_nonce => get_speculative_nonce( 80 | Address, 81 | security, 82 | Ledger, 83 | 1 84 | ) 85 | }; 86 | _ -> 87 | #{ 88 | sec_balance => 0, 89 | sec_nonce => 0, 90 | sec_speculative_nonce => 0 91 | } 92 | end 93 | end, 94 | GetDCs = fun() -> 95 | case blockchain_ledger_v1:find_dc_entry(Address, Ledger) of 96 | {ok, Entry} -> 97 | #{ 98 | dc_balance => blockchain_ledger_data_credits_entry_v1:balance(Entry), 99 | dc_nonce => blockchain_ledger_data_credits_entry_v1:nonce(Entry) 100 | }; 101 | _ -> 102 | #{ 103 | dc_balance => 0, 104 | dc_nonce => 0 105 | } 106 | end 107 | end, 108 | lists:foldl( 109 | fun(Fun, Map) -> 110 | maps:merge(Map, Fun()) 111 | end, 112 | #{}, 113 | [GetBalance, GetSecurities, GetDCs] 114 | ). 115 | 116 | -spec get_balance_entry_v2( 117 | Address :: libp2p_crypto:pubkey_bin(), 118 | Ledger :: blockchain:ledger() 119 | ) -> map(). 120 | get_balance_entry_v2(Address, Ledger) -> 121 | BalanceMap = 122 | case blockchain_ledger_v1:find_entry(Address, Ledger) of 123 | {ok, Entry} -> 124 | #{ 125 | mobile_balance => blockchain_ledger_entry_v2:balance(Entry, mobile), 126 | iot_balance => blockchain_ledger_entry_v2:balance(Entry, iot), 127 | sec_balance => blockchain_ledger_entry_v2:balance(Entry, hst), 128 | balance => blockchain_ledger_entry_v2:balance(Entry), 129 | nonce => blockchain_ledger_entry_v2:nonce(Entry), 130 | speculative_nonce => get_speculative_nonce(Address, balance, Ledger, 2) 131 | }; 132 | _ -> 133 | #{ 134 | mobile_balance => 0, 135 | iot_balance => 0, 136 | sec_balance => 0, 137 | balance => 0, 138 | nonce => 0, 139 | speculative_nonce => 0 140 | } 141 | end, 142 | DCMap = 143 | case blockchain_ledger_v1:find_dc_entry(Address, Ledger) of 144 | {ok, DCEntry} -> 145 | #{ 146 | dc_balance => blockchain_ledger_data_credits_entry_v1:balance(DCEntry), 147 | dc_nonce => blockchain_ledger_data_credits_entry_v1:nonce(DCEntry) 148 | }; 149 | _ -> 150 | #{ 151 | dc_balance => 0, 152 | dc_nonce => 0 153 | } 154 | end, 155 | maps:merge(BalanceMap, DCMap). 156 | 157 | -spec get_token_version( 158 | Ledger :: blockchain:ledger() 159 | ) -> pos_integer(). 160 | get_token_version(Ledger) -> 161 | case blockchain_ledger_v1:config(?token_version, Ledger) of 162 | {ok, N} -> N; 163 | _ -> 1 164 | end. 165 | 166 | -spec get_speculative_nonce( 167 | Address :: libp2p_crypto:pubkey_bin(), 168 | bn_pending_txns:nonce_type(), 169 | blockchain:ledger() 170 | ) -> non_neg_integer(). 171 | get_speculative_nonce(Address, NonceType, Ledger) -> 172 | get_speculative_nonce(Address, NonceType, Ledger, get_token_version(Ledger)). 173 | 174 | -spec get_speculative_nonce( 175 | Address :: libp2p_crypto:pubkey_bin(), 176 | bn_pending_txns:nonce_type(), 177 | blockchain:ledger(), 178 | LedgerEntryVersion :: pos_integer() 179 | ) -> 180 | non_neg_integer(). 181 | get_speculative_nonce(Address, NonceType, Ledger, LedgerEntryVersion) -> 182 | EntryMod = 183 | case LedgerEntryVersion of 184 | 1 -> blockchain_ledger_entry_v1; 185 | 2 -> blockchain_ledger_entry_v2 186 | end, 187 | case blockchain_ledger_v1:find_entry(Address, Ledger) of 188 | {ok, Entry} -> 189 | LedgerNonce = EntryMod:nonce(Entry), 190 | PendingNonce = bn_pending_txns:get_max_nonce(Address, NonceType), 191 | max(LedgerNonce, PendingNonce); 192 | {error, _} -> 193 | 0 194 | end. 195 | -------------------------------------------------------------------------------- /src/bn_blocks.erl: -------------------------------------------------------------------------------- 1 | -module(bn_blocks). 2 | 3 | -include("bn_jsonrpc.hrl"). 4 | -behavior(bn_jsonrpc_handler). 5 | 6 | -export([handle_rpc/2]). 7 | 8 | handle_rpc(<<"block_height">>, _Params) -> 9 | bn_txns:follower_height(); 10 | 11 | handle_rpc(<<"block_get">>, {Param}) -> 12 | HeightOrHash = 13 | case ?jsonrpc_get_param(<<"height">>, Param, false) of 14 | false -> ?jsonrpc_b64_to_bin(<<"hash">>, Param); 15 | V when is_integer(V) -> V; 16 | _ -> ?jsonrpc_error({invalid_params, Param}) 17 | end, 18 | case blockchain:get_block(HeightOrHash, blockchain_worker:blockchain()) of 19 | {ok, Block} -> 20 | blockchain_block:to_json(Block, []); 21 | {error, not_found} -> 22 | ?jsonrpc_error({not_found, "Block not found: ~p", [Param]}); 23 | {error, _}=Error -> 24 | ?jsonrpc_error(Error) 25 | end; 26 | 27 | handle_rpc(_, _) -> 28 | ?jsonrpc_error(method_not_found). 29 | -------------------------------------------------------------------------------- /src/bn_db.erl: -------------------------------------------------------------------------------- 1 | -module(bn_db). 2 | 3 | -export([open_db/2, open_db/3, clean_db/1]). 4 | -export([get_state/1]). 5 | -export([get_follower_height/2, put_follower_height/3, batch_put_follower_height/3]). 6 | 7 | -spec open_db(Dir :: file:filename_all(), CFNames :: [string()]) -> 8 | {ok, rocksdb:db_handle(), [rocksdb:cf_handle()]} | {error, any()}. 9 | open_db(Dir, CFNames) -> 10 | open_db(Dir, CFNames, []). 11 | -spec open_db(Dir :: file:filename_all(), CFNames :: [string()], AdditionalCFOpts :: [term()]) -> 12 | {ok, rocksdb:db_handle(), [rocksdb:cf_handle()]} | {error, any()}. 13 | open_db(Dir, CFNames, AdditionalCFOpts) -> 14 | ok = filelib:ensure_dir(Dir), 15 | GlobalOpts = application:get_env(rocksdb, global_opts, []), 16 | DBOptions = [{create_if_missing, true}, {atomic_flush, true}] ++ GlobalOpts, 17 | ExistingCFs = 18 | case rocksdb:list_column_families(Dir, DBOptions) of 19 | {ok, CFs0} -> 20 | CFs0; 21 | {error, _} -> 22 | ["default"] 23 | end, 24 | 25 | CFOpts = GlobalOpts ++ AdditionalCFOpts, 26 | case rocksdb:open_with_cf(Dir, DBOptions, [{CF, CFOpts} || CF <- ExistingCFs]) of 27 | {error, _Reason} = Error -> 28 | Error; 29 | {ok, DB, OpenedCFs} -> 30 | L1 = lists:zip(ExistingCFs, OpenedCFs), 31 | L2 = lists:map( 32 | fun (CF) -> 33 | {ok, CF1} = rocksdb:create_column_family(DB, CF, CFOpts), 34 | {CF, CF1} 35 | end, 36 | CFNames -- ExistingCFs 37 | ), 38 | L3 = L1 ++ L2, 39 | {ok, DB, [proplists:get_value(X, L3) || X <- CFNames]} 40 | end. 41 | 42 | clean_db(Dir) when is_list(Dir) -> 43 | ok = rocksdb:destroy(Dir, []). 44 | 45 | -spec get_state(Module :: atom()) -> {ok, any()} | {error, term()}. 46 | get_state(Module) -> 47 | case persistent_term:get(Module, false) of 48 | false -> 49 | {error, {no_database, Module}}; 50 | State -> 51 | {ok, State} 52 | end. 53 | 54 | -define(HEIGHT_KEY, <<"height">>). 55 | 56 | -spec get_follower_height(rocksdb:db_handle(), rocksdb:cf_handle()) -> 57 | {ok, non_neg_integer()} | {error, term()}. 58 | get_follower_height(DB, CF) -> 59 | case rocksdb:get(DB, CF, ?HEIGHT_KEY, []) of 60 | {ok, <>} -> 61 | {ok, blockchain:snapshot_height(Height)}; 62 | not_found -> 63 | {ok, blockchain:snapshot_height(0)}; 64 | {error, _} = Error -> 65 | Error 66 | end. 67 | 68 | -spec put_follower_height(rocksdb:db_handle(), rocksdb:cf_handle(), non_neg_integer()) -> 69 | ok | {error, term()}. 70 | put_follower_height(DB, CF, BlockHeight) -> 71 | rocksdb:put(DB, CF, ?HEIGHT_KEY, <>, []). 72 | 73 | -spec batch_put_follower_height( 74 | rocksdb:batch_handle(), 75 | rocksdb:cf_handle(), 76 | non_neg_integer() 77 | ) -> 78 | ok | {error, term()}. 79 | batch_put_follower_height(Batch, CF, BlockHeight) -> 80 | rocksdb:batch_put(Batch, CF, ?HEIGHT_KEY, <>). 81 | -------------------------------------------------------------------------------- /src/bn_gateways.erl: -------------------------------------------------------------------------------- 1 | -module(bn_gateways). 2 | 3 | -include("bn_jsonrpc.hrl"). 4 | 5 | -behavior(bn_jsonrpc_handler). 6 | 7 | %% jsonrpc_handler 8 | -export([handle_rpc/2]). 9 | 10 | %% 11 | %% jsonrpc_handler 12 | %% 13 | 14 | handle_rpc(<<"gateway_info_get">>, {Param}) -> 15 | Chain = blockchain_worker:blockchain(), 16 | {Height, Ledger} = {bn_txns:follower_height(), blockchain:ledger(Chain)}, 17 | Address = ?jsonrpc_b58_to_bin(<<"address">>, Param), 18 | case blockchain_ledger_v1:find_gateway_info(Address, Ledger) of 19 | {ok, GWInfo} -> 20 | #{ 21 | owner_address => ?BIN_TO_B58(blockchain_ledger_gateway_v2:owner_address(GWInfo)), 22 | location => ?MAYBE_H3(blockchain_ledger_gateway_v2:location(GWInfo)), 23 | alpha => blockchain_ledger_gateway_v2:alpha(GWInfo), 24 | beta => blockchain_ledger_gateway_v2:beta(GWInfo), 25 | delta => blockchain_ledger_gateway_v2:delta(GWInfo), 26 | last_poc_challenge => blockchain_ledger_gateway_v2:last_poc_challenge(GWInfo), 27 | last_poc_onion_key_hash => ?BIN_TO_B64( 28 | blockchain_ledger_gateway_v2:last_poc_onion_key_hash(GWInfo) 29 | ), 30 | nonce => blockchain_ledger_gateway_v2:nonce(GWInfo), 31 | version => blockchain_ledger_gateway_v2:version(GWInfo), 32 | neighbors => blockchain_ledger_gateway_v2:neighbors(GWInfo), 33 | witnesses => blockchain_ledger_gateway_v2:witnesses(GWInfo), 34 | oui => blockchain_ledger_gateway_v2:oui(GWInfo), 35 | gain => blockchain_ledger_gateway_v2:gain(GWInfo), 36 | elevation => blockchain_ledger_gateway_v2:elevation(GWInfo), 37 | mode => blockchain_ledger_gateway_v2:mode(GWInfo), 38 | last_location_nonce => blockchain_ledger_gateway_v2:last_location_nonce(GWInfo) 39 | }; 40 | {error, E} -> 41 | ?jsonrpc_error( 42 | {error, "unable to retrieve account details for ~p at height ~p due to error: ~p", [ 43 | ?BIN_TO_B58(Address), Height, E 44 | ]} 45 | ); 46 | _ -> 47 | ?jsonrpc_error( 48 | {error, 49 | "unable to retrieve account details for ~p at height ~p due to unknown error.", 50 | [?BIN_TO_B58(Address), Height]} 51 | ) 52 | end; 53 | handle_rpc(_, _) -> 54 | ?jsonrpc_error(method_not_found). 55 | -------------------------------------------------------------------------------- /src/bn_htlc.erl: -------------------------------------------------------------------------------- 1 | -module(bn_htlc). 2 | 3 | -include("bn_jsonrpc.hrl"). 4 | -behavior(bn_jsonrpc_handler). 5 | 6 | -export([handle_rpc/2]). 7 | 8 | %% 9 | %% jsonrpc_handler 10 | %% 11 | 12 | handle_rpc(<<"htlc_get">>, {Param}) -> 13 | Address = ?jsonrpc_b58_to_bin(<<"address">>, Param), 14 | Chain = blockchain_worker:blockchain(), 15 | Ledger = blockchain:ledger(Chain), 16 | case blockchain_ledger_v1:find_htlc(Address, Ledger) of 17 | {ok, HTLC} -> 18 | #{ 19 | address => Address, 20 | balance => blockchain_ledger_htlc_v1:balance(HTLC), 21 | hashlock => ?BIN_TO_B64(blockchain_ledger_htlc_v1:hashlock(HTLC)), 22 | payee => ?BIN_TO_B58(blockchain_ledger_htlc_v1:payee(HTLC)), 23 | payer => ?BIN_TO_B58(blockchain_ledger_htlc_v1:payer(HTLC)), 24 | timelock => blockchain_ledger_htlc_v1:timelock(HTLC) 25 | }; 26 | {error, not_found} -> 27 | case blockchain:get_htlc_receipt(Address, Chain) of 28 | {ok, HTLCReceipt} -> 29 | blockchain_htlc_receipt:to_json(HTLCReceipt, []); 30 | {error, _}=Error -> 31 | ?jsonrpc_error(Error) 32 | end; 33 | {error, _}=Error -> 34 | ?jsonrpc_error(Error) 35 | end; 36 | handle_rpc(_, _) -> 37 | ?jsonrpc_error(method_not_found). 38 | -------------------------------------------------------------------------------- /src/bn_implicit_burn.erl: -------------------------------------------------------------------------------- 1 | -module(bn_implicit_burn). 2 | 3 | -include("bn_jsonrpc.hrl"). 4 | -behavior(bn_jsonrpc_handler). 5 | 6 | -export([handle_rpc/2]). 7 | 8 | %% 9 | %% jsonrpc_handler 10 | %% 11 | 12 | handle_rpc(<<"implicit_burn_get">>, {Param}) -> 13 | Hash = ?jsonrpc_b64_to_bin(<<"hash">>, Param), 14 | Chain = blockchain_worker:blockchain(), 15 | case blockchain:get_implicit_burn(Hash, Chain) of 16 | {ok, ImplicitBurn} -> 17 | blockchain_implicit_burn:to_json(ImplicitBurn, []); 18 | {error, not_found} -> 19 | ?jsonrpc_error({not_found, "Implicit burn not found for transaction hash: ~p", [Param]}); 20 | {error, _}=Error -> 21 | ?jsonrpc_error(Error) 22 | end; 23 | handle_rpc(_, _) -> 24 | ?jsonrpc_error(method_not_found). 25 | -------------------------------------------------------------------------------- /src/bn_jsonrpc.hrl: -------------------------------------------------------------------------------- 1 | -define (jsonrpc_get_param(K,P), bn_jsonrpc_handler:jsonrpc_get_param((K),(P))). 2 | -define (jsonrpc_get_param(K,P,D), bn_jsonrpc_handler:jsonrpc_get_param((K),(P),(D))). 3 | -define (jsonrpc_b58_to_bin(K,P), bn_jsonrpc_handler:jsonrpc_b58_to_bin((K),(P))). 4 | -define (jsonrpc_b64_to_bin(K,P), bn_jsonrpc_handler:jsonrpc_b64_to_bin((K),(P))). 5 | -define(jsonrpc_error(E), bn_jsonrpc_handler:jsonrpc_error((E))). 6 | 7 | -define (BIN_TO_B58(B), list_to_binary(libp2p_crypto:bin_to_b58((B)))). 8 | -define (B58_TO_BIN(B), libp2p_crypto:b58_to_bin(binary_to_list((B)))). 9 | 10 | -define (BIN_TO_B64(B), base64url:encode((B))). 11 | -define (B64_TO_BIN(B), base64url:decode((B))). 12 | 13 | -define(TO_KEY(K), bn_jsonrpc_handler:to_key(K)). 14 | -define(TO_VALUE(V), bn_jsonrpc_handler:to_value(V)). 15 | -define(B58_TO_ANIMAL(V), iolist_to_binary(element(2, erl_angry_purple_tiger:animal_name(V)))). 16 | -define(BIN_TO_ANIMAL(V), 17 | iolist_to_binary( 18 | element(2, erl_angry_purple_tiger:animal_name(?BIN_TO_B58(V))) 19 | ) 20 | ). 21 | 22 | -define(MAYBE_H3(B), blockchain_json:maybe_h3((B))). 23 | -------------------------------------------------------------------------------- /src/bn_jsonrpc_handler.erl: -------------------------------------------------------------------------------- 1 | -module(bn_jsonrpc_handler). 2 | 3 | -callback handle_rpc(Method :: binary(), Params :: term()) -> jsone:json(). 4 | 5 | -export([handle/2, handle_event/3]). 6 | -export([ 7 | jsonrpc_b58_to_bin/2, 8 | jsonrpc_b64_to_bin/2, 9 | jsonrpc_get_param/2, 10 | jsonrpc_get_param/3, 11 | jsonrpc_error/1, 12 | to_key/1, 13 | to_value/1 14 | ]). 15 | 16 | -include("bn_jsonrpc.hrl"). 17 | 18 | -include_lib("elli/include/elli.hrl"). 19 | 20 | -behaviour(elli_handler). 21 | 22 | handle(Req, _Args) -> 23 | %% Delegate to our handler function 24 | handle(Req#req.method, elli_request:path(Req), Req). 25 | 26 | handle('POST', _, Req) -> 27 | Json = elli_request:body(Req), 28 | {reply, Reply} = 29 | jsonrpc2:handle(Json, fun handle_rpc/2, fun decode_helper/1, fun encode_helper/1), 30 | {ok, [], Reply}; 31 | handle(_, _, _Req) -> 32 | {404, [], <<"Not Found">>}. 33 | 34 | handle_rpc(<<"block_", _/binary>> = Method, Params) -> 35 | bn_blocks:handle_rpc(Method, Params); 36 | handle_rpc(<<"transaction_", _/binary>> = Method, Params) -> 37 | bn_txns:handle_rpc(Method, Params); 38 | handle_rpc(<<"gateway_info_", _/binary>> = Method, Params) -> 39 | bn_gateways:handle_rpc(Method, Params); 40 | handle_rpc(<<"htlc_", _/binary>> = Method, Params) -> 41 | bn_htlc:handle_rpc(Method, Params); 42 | handle_rpc(<<"implicit_burn_", _/binary>> = Method, Params) -> 43 | bn_implicit_burn:handle_rpc(Method, Params); 44 | handle_rpc(<<"peer_", _/binary>> = Method, Params) -> 45 | bn_peer:handle_rpc(Method, Params); 46 | handle_rpc(<<"pending_transaction_", _/binary>> = Method, Params) -> 47 | bn_pending_txns:handle_rpc(Method, Params); 48 | handle_rpc(<<"account_", _/binary>> = Method, Params) -> 49 | bn_accounts:handle_rpc(Method, Params); 50 | handle_rpc(<<"wallet_", _/binary>> = Method, Params) -> 51 | bn_wallets:handle_rpc(Method, Params); 52 | handle_rpc(<<"oracle_price_", _/binary>> = Method, Params) -> 53 | bn_oracle_price:handle_rpc(Method, Params); 54 | handle_rpc(_, _) -> 55 | ?jsonrpc_error(method_not_found). 56 | 57 | %% @doc Handle request events, like request completed, exception 58 | %% thrown, client timeout, etc. Must return `ok'. 59 | handle_event(request_throw, [Req, Exception, Stack], _Config) -> 60 | lager:error("exception: ~p~nstack: ~p~nrequest: ~p~n", [ 61 | Exception, 62 | Stack, 63 | elli_request:to_proplist(Req) 64 | ]), 65 | ok; 66 | handle_event(request_exit, [Req, Exit, Stack], _Config) -> 67 | lager:error("exit: ~p~nstack: ~p~nrequest: ~p~n", [ 68 | Exit, 69 | Stack, 70 | elli_request:to_proplist(Req) 71 | ]), 72 | ok; 73 | handle_event(request_error, [Req, Error, Stack], _Config) -> 74 | lager:error("error: ~p~nstack: ~p~nrequest: ~p~n", [ 75 | Error, 76 | Stack, 77 | elli_request:to_proplist(Req) 78 | ]), 79 | ok; 80 | handle_event(_, _, _) -> 81 | ok. 82 | 83 | %% 84 | %% Param conversion 85 | %% 86 | jsonrpc_get_param(Key, PropList) -> 87 | case proplists:lookup(Key, PropList) of 88 | none -> ?jsonrpc_error(invalid_params); 89 | {Key, V} -> V 90 | end. 91 | 92 | jsonrpc_get_param(Key, PropList, Default) -> 93 | proplists:get_value(Key, PropList, Default). 94 | 95 | jsonrpc_b58_to_bin(Key, PropList) -> 96 | B58 = jsonrpc_get_param(Key, PropList), 97 | try ?B58_TO_BIN(B58) 98 | catch 99 | _:_ -> ?jsonrpc_error(invalid_params) 100 | end. 101 | 102 | jsonrpc_b64_to_bin(Key, PropList) -> 103 | B64 = jsonrpc_get_param(Key, PropList), 104 | try ?B64_TO_BIN(B64) 105 | catch 106 | _:_ -> ?jsonrpc_error(invalid_params) 107 | end. 108 | 109 | %% 110 | %% Errors 111 | %% 112 | -define(throw_error(C, L), throw({jsonrpc2, C, iolist_to_binary((L))})). 113 | -define(throw_error(C, F, A), 114 | throw({jsonrpc2, C, iolist_to_binary(io_lib:format((F), (A)))}) 115 | ). 116 | 117 | -define(ERR_NOT_FOUND, -100). 118 | -define(ERR_INVALID_PASSWORD, -110). 119 | -define(ERR_ERROR, -150). 120 | 121 | -spec jsonrpc_error(term()) -> no_return(). 122 | jsonrpc_error(method_not_found = E) -> 123 | throw(E); 124 | jsonrpc_error(invalid_params = E) -> 125 | throw(E); 126 | jsonrpc_error({invalid_params, _} = E) -> 127 | throw(E); 128 | jsonrpc_error({not_found, F, A}) -> 129 | ?throw_error(?ERR_NOT_FOUND, F, A); 130 | jsonrpc_error({not_found, M}) -> 131 | ?throw_error(?ERR_NOT_FOUND, M); 132 | jsonrpc_error(invalid_password) -> 133 | ?throw_error(?ERR_INVALID_PASSWORD, "Invalid password"); 134 | jsonrpc_error({error, F, A}) -> 135 | ?throw_error(?ERR_ERROR, F, A); 136 | jsonrpc_error({error, E}) -> 137 | jsonrpc_error({error, "~p", E}). 138 | 139 | %% 140 | %% Internal 141 | %% 142 | 143 | decode_helper(Bin) -> 144 | jsone:decode(Bin, [{object_format, tuple}]). 145 | 146 | encode_helper(Json) -> 147 | jsone:encode(Json, [undefined_as_null]). 148 | 149 | to_key(X) when is_atom(X) -> atom_to_binary(X, utf8); 150 | to_key(X) when is_list(X) -> iolist_to_binary(X); 151 | to_key(X) when is_binary(X) -> X. 152 | 153 | %% don't want these atoms stringified 154 | to_value(true) -> true; 155 | to_value(false) -> false; 156 | to_value(undefined) -> null; 157 | %% lightly format floats, but pass through integers as-is 158 | to_value(X) when is_float(X) -> float_to_binary(blockchain_utils:normalize_float(X), [{decimals, 3}, compact]); 159 | to_value(X) when is_integer(X) -> X; 160 | %% make sure we have valid representations of other types which may show up in values 161 | to_value(X) when is_list(X) -> iolist_to_binary(X); 162 | to_value(X) when is_atom(X) -> atom_to_binary(X, utf8); 163 | to_value(X) when is_binary(X) -> X; 164 | to_value(X) when is_map(X) -> ensure_binary_map(X); 165 | to_value(X) -> iolist_to_binary(io_lib:format("~p", [X])). 166 | 167 | ensure_binary_map(M) -> 168 | maps:fold(fun(K, V, Acc) -> 169 | BinK = to_key(K), 170 | BinV = to_value(V), 171 | Acc#{BinK => BinV} 172 | end, #{}, M). 173 | -------------------------------------------------------------------------------- /src/bn_oracle_price.erl: -------------------------------------------------------------------------------- 1 | -module(bn_oracle_price). 2 | 3 | % -behavior(blockchain_follower). 4 | 5 | -include("bn_jsonrpc.hrl"). 6 | 7 | %% blockchain_follower 8 | -export([ 9 | requires_sync/0, 10 | requires_ledger/0, 11 | init/1, 12 | follower_height/1, 13 | load_chain/2, 14 | load_block/5, 15 | terminate/2 16 | ]). 17 | 18 | %% jsonrpc 19 | -export([handle_rpc/2]). 20 | %% api 21 | -export([get_oracle_price/1]). 22 | 23 | -define(DB_FILE, "oracle_price.db"). 24 | 25 | -record(state, { 26 | db :: rocksdb:db_handle(), 27 | default :: rocksdb:cf_handle(), 28 | prices :: rocksdb:cf_handle() 29 | }). 30 | 31 | %% 32 | %% blockchain_follower 33 | %% 34 | requires_sync() -> false. 35 | 36 | requires_ledger() -> false. 37 | 38 | init(Args) -> 39 | Dir = filename:join(proplists:get_value(base_dir, Args, "data"), ?DB_FILE), 40 | case load_db(Dir) of 41 | {error, {db_open, "Corruption:" ++ _Reason}} -> 42 | lager:error("DB could not be opened corrupted ~p, cleaning up", [_Reason]), 43 | ok = bn_db:clean_db(Dir), 44 | init(Args); 45 | {ok, State} -> 46 | persistent_term:put(?MODULE, State), 47 | {ok, State} 48 | end. 49 | 50 | follower_height(#state{db = DB, default = DefaultCF}) -> 51 | case bn_db:get_follower_height(DB, DefaultCF) of 52 | {ok, Height} -> Height; 53 | {error, _} = Error -> ?jsonrpc_error(Error) 54 | end. 55 | 56 | load_chain(_Chain, State = #state{}) -> 57 | {ok, State}. 58 | 59 | load_block(_Hash, Block, _Sync, Ledger, State = #state{db = DB, default = DefaultCF}) -> 60 | Height = blockchain_block:height(Block), 61 | ok = 62 | case Ledger of 63 | undefined -> 64 | ok; 65 | _ -> 66 | {ok, Price} = blockchain_ledger_v1:current_oracle_price(Ledger), 67 | save_oracle_price(Height, Price, State) 68 | end, 69 | ok = bn_db:put_follower_height(DB, DefaultCF, Height), 70 | {ok, State}. 71 | 72 | terminate(_Reason, #state{db = DB}) -> 73 | rocksdb:close(DB). 74 | 75 | %% 76 | %% jsonrpc_handler 77 | %% 78 | 79 | handle_rpc(<<"oracle_price_current">>, _Params) -> 80 | case get_oracle_price(current) of 81 | {ok, {Height, Price}} -> #{height => Height, price => Price}; 82 | {error, _} = Error -> ?jsonrpc_error(Error) 83 | end; 84 | handle_rpc(<<"oracle_price_get">>, {Param}) -> 85 | Height = 86 | case ?jsonrpc_get_param(<<"height">>, Param, false) of 87 | V when is_integer(V) -> V; 88 | _ -> ?jsonrpc_error({invalid_params, Param}) 89 | end, 90 | case get_oracle_price(Height) of 91 | {ok, {_, Price}} -> 92 | #{height => Height, price => Price}; 93 | {error, not_found} -> 94 | ?jsonrpc_error({not_found, "Price not found for: ~p", [Height]}); 95 | {error, _} = Error -> 96 | ?jsonrpc_error(Error) 97 | end; 98 | handle_rpc(_, _) -> 99 | ?jsonrpc_error(method_not_found). 100 | 101 | %% 102 | %% API 103 | %% 104 | 105 | -spec get_oracle_price(Height :: current | non_neg_integer()) -> 106 | {ok, {Height :: non_neg_integer(), Price :: non_neg_integer()}} | {error, term()}. 107 | get_oracle_price(current) -> 108 | {ok, Height} = blockchain:height(blockchain_worker:blockchain()), 109 | case blockchain_ledger_v1:current_oracle_price(blockchain:ledger()) of 110 | {ok, Price} -> {ok, {Height, Price}}; 111 | {error, _} = Error -> Error 112 | end; 113 | get_oracle_price(Height) when is_integer(Height) -> 114 | {ok, State} = get_state(), 115 | case get_oracle_price(Height, State) of 116 | {ok, Price} -> {ok, {Height, Price}}; 117 | {error, _} = Error -> Error 118 | end. 119 | 120 | %% 121 | %% internal 122 | %% 123 | get_state() -> 124 | bn_db:get_state(?MODULE). 125 | 126 | -spec get_oracle_price(Height :: non_neg_integer(), #state{}) -> 127 | {ok, Price :: non_neg_integer()} | {error, term()}. 128 | get_oracle_price(Height, #state{db = DB, prices = PricesCF}) -> 129 | case rocksdb:get(DB, PricesCF, <>, []) of 130 | {ok, <>} -> {ok, Price}; 131 | not_found -> {error, not_found}; 132 | Error -> Error 133 | end. 134 | 135 | -spec save_oracle_price(Height :: non_neg_integer(), Price :: non_neg_integer(), #state{}) -> 136 | ok. 137 | save_oracle_price(Height, Price, #state{db = DB, prices = PricesCF}) -> 138 | {ok, Batch} = rocksdb:batch(), 139 | ok = rocksdb:batch_put( 140 | Batch, 141 | PricesCF, 142 | <>, 143 | <> 144 | ), 145 | rocksdb:write_batch(DB, Batch, [{sync, true}]). 146 | 147 | -spec load_db(Dir :: file:filename_all()) -> {ok, #state{}} | {error, any()}. 148 | load_db(Dir) -> 149 | case bn_db:open_db(Dir, ["default", "prices"]) of 150 | {error, _Reason} = Error -> 151 | Error; 152 | {ok, DB, [DefaultCF, PricesCF]} -> 153 | State = #state{db = DB, default = DefaultCF, prices = PricesCF}, 154 | compact_db(State), 155 | {ok, State} 156 | end. 157 | 158 | compact_db(#state{db = DB, default = Default, prices = PricesCF}) -> 159 | rocksdb:compact_range(DB, Default, undefined, undefined, []), 160 | rocksdb:compact_range(DB, PricesCF, undefined, undefined, []), 161 | ok. 162 | -------------------------------------------------------------------------------- /src/bn_peer.erl: -------------------------------------------------------------------------------- 1 | -module(bn_peer). 2 | 3 | -include("bn_jsonrpc.hrl"). 4 | -behavior(bn_jsonrpc_handler). 5 | 6 | -export([handle_rpc/2]). 7 | 8 | %% 9 | %% jsonrpc_handler 10 | %% 11 | handle_rpc(<<"peer_book_self">>, []) -> 12 | peer_book_response(self); 13 | handle_rpc(_, _) -> 14 | ?jsonrpc_error(method_not_found). 15 | 16 | %% 17 | %% Internal 18 | %% 19 | peer_book_response(self) -> 20 | TID = blockchain_swarm:tid(), 21 | Peerbook = libp2p_swarm:peerbook(TID), 22 | 23 | {ok, Peer} = libp2p_peerbook:get(Peerbook, blockchain_swarm:pubkey_bin()), 24 | [ lists:foldl(fun(M, Acc) -> maps:merge(Acc, M) end, 25 | format_peer(Peer), 26 | [format_listen_addrs(TID, libp2p_peer:listen_addrs(Peer)), 27 | format_peer_sessions(TID)] 28 | ) ]. 29 | 30 | format_peer(Peer) -> 31 | ListenAddrs = libp2p_peer:listen_addrs(Peer), 32 | ConnectedTo = libp2p_peer:connected_peers(Peer), 33 | NatType = libp2p_peer:nat_type(Peer), 34 | Timestamp = libp2p_peer:timestamp(Peer), 35 | Bin = libp2p_peer:pubkey_bin(Peer), 36 | M = #{ 37 | <<"address">> => libp2p_crypto:pubkey_bin_to_p2p(Bin), 38 | <<"name">> => ?BIN_TO_ANIMAL(Bin), 39 | <<"listen_addr_count">> => length(ListenAddrs), 40 | <<"connection_count">> => length(ConnectedTo), 41 | <<"nat">> => NatType, 42 | <<"last_updated">> => (erlang:system_time(millisecond) - Timestamp) / 1000 43 | }, 44 | maps:map(fun(_K, V) -> ?TO_VALUE(V) end, M). 45 | 46 | format_listen_addrs(TID, Addrs) -> 47 | libp2p_transport:sort_addrs(TID, Addrs), 48 | #{<<"listen_addresses">> => [?TO_VALUE(A) || A <- Addrs]}. 49 | 50 | format_peer_sessions(Swarm) -> 51 | SessionInfos = libp2p_swarm:sessions(Swarm), 52 | Rs = lists:filtermap( 53 | fun({A, S}) -> 54 | case multiaddr:protocols(A) of 55 | [{"p2p", B58}] -> 56 | {true, {A, libp2p_session:addr_info(libp2p_swarm:tid(Swarm), S), B58}}; 57 | _ -> 58 | false 59 | end 60 | end, 61 | SessionInfos 62 | ), 63 | 64 | FormatEntry = fun({MA, {SockAddr, PeerAddr}, B58}) -> 65 | M = #{ 66 | <<"local">> => SockAddr, 67 | <<"remote">> => PeerAddr, 68 | <<"p2p">> => MA, 69 | <<"name">> => ?B58_TO_ANIMAL(B58) 70 | }, 71 | maps:map(fun(_K, V) -> ?TO_VALUE(V) end, M) 72 | end, 73 | #{ <<"sessions">> => [FormatEntry(E) || E <- Rs] }. 74 | -------------------------------------------------------------------------------- /src/bn_pending_txns.erl: -------------------------------------------------------------------------------- 1 | -module(bn_pending_txns). 2 | 3 | % -behavior(blockchain_follower). 4 | 5 | -include("bn_jsonrpc.hrl"). 6 | 7 | -include_lib("helium_proto/include/blockchain_txn_pb.hrl"). 8 | 9 | -type supported_txn() :: 10 | #blockchain_txn_oui_v1_pb{} 11 | | #blockchain_txn_routing_v1_pb{} 12 | | #blockchain_txn_vars_v1_pb{} 13 | | #blockchain_txn_add_gateway_v1_pb{} 14 | | #blockchain_txn_assert_location_v1_pb{} 15 | | #blockchain_txn_assert_location_v2_pb{} 16 | | #blockchain_txn_payment_v1_pb{} 17 | | #blockchain_txn_payment_v2_pb{} 18 | | #blockchain_txn_create_htlc_v1_pb{} 19 | | #blockchain_txn_redeem_htlc_v1_pb{} 20 | | #blockchain_txn_price_oracle_v1_pb{} 21 | | #blockchain_txn_token_burn_v1_pb{} 22 | | #blockchain_txn_transfer_hotspot_v1_pb{} 23 | | #blockchain_txn_transfer_hotspot_v2_pb{} 24 | | #blockchain_txn_security_exchange_v1_pb{} 25 | | #blockchain_txn_stake_validator_v1_pb{} 26 | | #blockchain_txn_unstake_validator_v1_pb{} 27 | | #blockchain_txn_transfer_validator_stake_v1_pb{} 28 | | #blockchain_txn_state_channel_open_v1_pb{}. 29 | 30 | -type nonce_type() :: none | balance | gateway | security | dc. 31 | -type nonce_address() :: libp2p_crypto:pubkey_bin() | undefined. 32 | -type nonce() :: non_neg_integer(). 33 | 34 | -export_type([nonce_type/0, nonce_address/0, nonce/0]). 35 | 36 | %% blockchain_follower 37 | -export([ 38 | requires_sync/0, 39 | requires_ledger/0, 40 | init/1, 41 | follower_height/1, 42 | load_chain/2, 43 | load_block/5, 44 | terminate/2 45 | ]). 46 | 47 | %% jsonrpc 48 | -export([handle_rpc/2]). 49 | %% API 50 | -export([submit_txn/1, get_txn_status/1, get_max_nonce/2]). 51 | 52 | -define(DB_FILE, "pendning_transactions.db"). 53 | -define(TXN_STATUS_CLEARED, 0). 54 | -define(TXN_STATUS_FAILED, 1). 55 | 56 | -record(state, { 57 | db :: rocksdb:db_handle(), 58 | default :: rocksdb:cf_handle(), 59 | pending :: rocksdb:cf_handle(), 60 | status :: rocksdb:cf_handle() 61 | }). 62 | 63 | %% 64 | %% blockchain_follower 65 | %% 66 | requires_sync() -> false. 67 | 68 | requires_ledger() -> false. 69 | 70 | init(Args) -> 71 | Dir = filename:join(proplists:get_value(base_dir, Args, "data"), ?DB_FILE), 72 | case load_db(Dir) of 73 | {error, {db_open, "Corruption:" ++ _Reason}} -> 74 | lager:error("DB could not be opened corrupted ~p, cleaning up", [_Reason]), 75 | ok = bn_db:clean_db(Dir), 76 | init(Args); 77 | {ok, State} -> 78 | persistent_term:put(?MODULE, State), 79 | {ok, State} 80 | end. 81 | 82 | follower_height(#state{db = DB, default = DefaultCF}) -> 83 | case bn_db:get_follower_height(DB, DefaultCF) of 84 | {ok, Height} -> Height; 85 | {error, _} = Error -> ?jsonrpc_error(Error) 86 | end. 87 | 88 | load_chain(_Chain, State = #state{}) -> 89 | Submitted = submit_pending_txns(State), 90 | lager:info("Submitted ~p pending transactions", [Submitted]), 91 | {ok, State}. 92 | 93 | load_block(_Hash, Block, _Sync, _Ledger, State = #state{db = DB, default = DefaultCF}) -> 94 | ok = bn_db:put_follower_height(DB, DefaultCF, blockchain_block:height(Block)), 95 | {ok, State}. 96 | 97 | terminate(_Reason, #state{db = DB}) -> 98 | rocksdb:close(DB). 99 | 100 | %% 101 | %% jsonrpc_handler 102 | %% 103 | 104 | handle_rpc(<<"pending_transaction_get">>, {Param}) -> 105 | Hash = ?jsonrpc_b64_to_bin(<<"hash">>, Param), 106 | {ok, State} = get_state(), 107 | case get_txn_status(Hash, State) of 108 | {error, not_found} -> 109 | ?jsonrpc_error({not_found, "Pending transaction not found"}); 110 | {ok, Status} -> 111 | Json = 112 | case get_txn(Hash, State) of 113 | {error, not_found} -> 114 | %% transaction was cleared or failed, leave the txn 115 | %% details out 116 | #{}; 117 | {ok, Txn} -> 118 | #{<<"txn">> => blockchain_txn:to_json(Txn, [])} 119 | end, 120 | case Status of 121 | pending -> 122 | Json#{ 123 | <<"status">> => <<"pending">> 124 | }; 125 | {cleared, Height} -> 126 | Json#{ 127 | <<"status">> => <<"cleared">>, 128 | <<"block">> => Height 129 | }; 130 | {failed, Reason} -> 131 | Json#{ 132 | <<"status">> => <<"failed">>, 133 | <<"failed_reason">> => Reason 134 | } 135 | end 136 | end; 137 | handle_rpc(<<"pending_transaction_status">>, {Param}) -> 138 | Hash = ?jsonrpc_b64_to_bin(<<"hash">>, Param), 139 | {ok, State} = get_state(), 140 | case get_txn_status(Hash, State) of 141 | {ok, pending} -> 142 | <<"pending">>; 143 | {ok, {cleared, _}} -> 144 | <<"cleared">>; 145 | {ok, {failed, Reason}} -> 146 | Reason; 147 | {error, not_found} -> 148 | ?jsonrpc_error({not_found, "Pending transaction not found"}) 149 | end; 150 | handle_rpc(<<"pending_transaction_submit">>, {Param}) -> 151 | BinTxn = ?jsonrpc_b64_to_bin(<<"txn">>, Param), 152 | try 153 | Txn = blockchain_txn:deserialize(BinTxn), 154 | {ok, _} = submit_txn(Txn), 155 | blockchain_txn:to_json(Txn, []) 156 | catch 157 | _:_ -> ?jsonrpc_error(invalid_params) 158 | end; 159 | handle_rpc(<<"pending_transaction_verify">>, {Param}) -> 160 | BinTxn = ?jsonrpc_b64_to_bin(<<"txn">>, Param), 161 | try 162 | Txn = blockchain_txn:deserialize(BinTxn), 163 | Valid = blockchain_txn:is_valid(Txn, blockchain_worker:blockchain()), 164 | case Valid of 165 | ok -> <<"valid">>; 166 | {error, Reason} -> 167 | Reason 168 | end 169 | catch 170 | _:_ -> ?jsonrpc_error(invalid_params) 171 | end; 172 | handle_rpc(_, _) -> 173 | ?jsonrpc_error(method_not_found). 174 | 175 | %% 176 | %% API 177 | %% 178 | -spec submit_txn(blockchain_txn:txn()) -> {ok, Hash :: binary()} | {error, term()}. 179 | submit_txn(Txn) -> 180 | {ok, State} = get_state(), 181 | submit_txn(Txn, State). 182 | 183 | -type txn_status() :: pending | {failed, Reason :: binary()}. 184 | 185 | -spec get_txn_status(Hash :: binary()) -> {ok, txn_status()} | {error, term()}. 186 | get_txn_status(Hash) -> 187 | {ok, State} = get_state(), 188 | get_txn_status(Hash, State). 189 | 190 | %% 191 | %% Internal 192 | %% 193 | 194 | get_state() -> 195 | bn_db:get_state(?MODULE). 196 | 197 | -spec get_txn_status(Hash :: binary(), #state{}) -> 198 | {ok, {failed, Reason :: binary()} | {cleared, Block :: pos_integer()} | pending} 199 | | {error, not_found}. 200 | get_txn_status(Hash, #state{db = DB, pending = PendingCF, status = StatusCF}) -> 201 | case rocksdb:get(DB, StatusCF, Hash, []) of 202 | {ok, <<(?TXN_STATUS_CLEARED):8, Block:64/integer-unsigned-little>>} -> 203 | {ok, {cleared, Block}}; 204 | {ok, <<(?TXN_STATUS_FAILED):8, Reason/binary>>} -> 205 | {ok, {failed, Reason}}; 206 | not_found -> 207 | case rocksdb:get(DB, PendingCF, Hash, []) of 208 | not_found -> 209 | {error, not_found}; 210 | {ok, _} -> 211 | {ok, pending} 212 | end 213 | end. 214 | 215 | -spec submit_pending_txns(#state{}) -> non_neg_integer(). 216 | submit_pending_txns(State = #state{db = DB, pending = PendingCF}) -> 217 | %% iterate over the transactions and submit each one of them 218 | {ok, Itr} = rocksdb:iterator(DB, PendingCF, []), 219 | Submitted = submit_pending_txns(Itr, rocksdb:iterator_move(Itr, first), State, 0), 220 | catch rocksdb:iterator_close(Itr), 221 | Submitted. 222 | 223 | submit_pending_txns(_Itr, {error, _Error}, #state{}, Acc) -> 224 | Acc; 225 | submit_pending_txns(Itr, {ok, Hash, BinTxn}, State = #state{}, Acc) -> 226 | try blockchain_txn:deserialize(BinTxn) of 227 | Txn -> 228 | blockchain_txn_mgr:submit(Txn, fun(Result) -> 229 | finalize_txn(Hash, Result, State) 230 | end) 231 | catch 232 | What:Why -> 233 | lager:warning("Error while fetching pending transaction: ~p: ~p ~p", [ 234 | Hash, 235 | What, 236 | Why 237 | ]) 238 | end, 239 | submit_pending_txns(Itr, rocksdb:iterator_move(Itr, next), State, Acc + 1). 240 | 241 | finalize_txn(Hash, Status, #state{db = DB, pending = PendingCF, status = StatusCF}) -> 242 | {ok, Batch} = rocksdb:batch(), 243 | %% Set cleared or failed status 244 | case Status of 245 | ok -> 246 | rocksdb:batch_put( 247 | Batch, 248 | StatusCF, 249 | Hash, 250 | <<(?TXN_STATUS_CLEARED):8, 0:64/integer-unsigned-little>> 251 | ); 252 | {error, Error} -> 253 | ErrorBin = list_to_binary(lists:flatten(io_lib:format("~p", [Error]))), 254 | rocksdb:batch_put( 255 | Batch, 256 | StatusCF, 257 | Hash, 258 | <<(?TXN_STATUS_FAILED):8, ErrorBin/binary>> 259 | ) 260 | end, 261 | %% Delete the transaction from the pending table 262 | rocksdb:batch_delete(Batch, PendingCF, Hash), 263 | ok = rocksdb:write_batch(DB, Batch, [{sync, true}]). 264 | 265 | submit_txn(Txn, State = #state{db = DB, pending = PendingCF}) -> 266 | {ok, Batch} = rocksdb:batch(), 267 | Hash = blockchain_txn:hash(Txn), 268 | ok = rocksdb:batch_put(Batch, PendingCF, Hash, blockchain_txn:serialize(Txn)), 269 | ok = rocksdb:write_batch(DB, Batch, [{sync, true}]), 270 | blockchain_txn_mgr:submit(Txn, fun(Result) -> 271 | finalize_txn(Hash, Result, State) 272 | end), 273 | {ok, Hash}. 274 | 275 | -spec get_txn(Hash :: binary(), #state{}) -> {ok, blockchain_txn:txn()} | {error, term()}. 276 | get_txn(Hash, #state{db = DB, pending = PendingCF}) -> 277 | case rocksdb:get(DB, PendingCF, Hash, []) of 278 | {ok, BinTxn} -> 279 | {ok, blockchain_txn:deserialize(BinTxn)}; 280 | not_found -> 281 | {error, not_found}; 282 | Error -> 283 | Error 284 | end. 285 | 286 | -spec get_max_nonce(nonce_address(), nonce_type()) -> nonce(). 287 | get_max_nonce(Address, NonceType) -> 288 | {ok, State} = get_state(), 289 | get_max_nonce(Address, NonceType, State). 290 | 291 | -spec get_max_nonce(nonce_address(), nonce_type(), #state{}) -> nonce(). 292 | get_max_nonce(Address, NonceType, #state{db = DB, pending = PendingCF}) -> 293 | {ok, Itr} = rocksdb:iterator(DB, PendingCF, []), 294 | Nonce = get_max_nonce(Address, NonceType, Itr, rocksdb:iterator_move(Itr, first), 0), 295 | catch rocksdb:iterator_close(Itr), 296 | Nonce. 297 | 298 | get_max_nonce(_Address, _NonceType, _Itr, {error, _Error}, Acc) -> 299 | Acc; 300 | get_max_nonce(Address, NonceType, Itr, {ok, _, BinTxn}, Acc) -> 301 | Max = 302 | case nonce_info(blockchain_txn:deserialize(BinTxn)) of 303 | {TxnAddress, Nonce, TxnNonceType} when 304 | TxnAddress == Address andalso TxnNonceType == NonceType 305 | -> 306 | max(Acc, Nonce); 307 | _ -> 308 | Acc 309 | end, 310 | get_max_nonce(Address, NonceType, Itr, rocksdb:iterator_move(Itr, next), Max). 311 | 312 | %% Calculates nonce information for a given transaction. This information % 313 | %% includes the actor whose nonce is impacted, the nonce in the transaction and % 314 | %% the type of nonce this is. 315 | %% 316 | %% NOTE: This list should include all transaction types that can be submitted to 317 | %% the endpoint. We try to make it match what blockchain-http supports. 318 | -spec nonce_info(supported_txn()) -> {nonce_address(), nonce(), nonce_type()}. 319 | nonce_info(#blockchain_txn_oui_v1_pb{owner = Owner}) -> 320 | %% There is no nonce type for that is useful to speculate values for 321 | {Owner, 0, none}; 322 | nonce_info(#blockchain_txn_routing_v1_pb{nonce = Nonce}) -> 323 | %% oui changes could get their own oui nonce, but since there is no good actor 324 | %% address for it (an oui is not a public key which a lot of code relies on, we don't 325 | %% track it right now. We can't lean on the owner address since an owner can 326 | %% have multiple ouis 327 | {undefined, Nonce, none}; 328 | nonce_info(#blockchain_txn_vars_v1_pb{nonce = Nonce}) -> 329 | %% A vars transaction doesn't have a clear actor at all so we don't track it 330 | {undefined, Nonce, none}; 331 | nonce_info(#blockchain_txn_add_gateway_v1_pb{gateway = GatewayAddress}) -> 332 | %% Adding a gateway uses the gateway nonce, even though it's 333 | %% expected to be 0 (a gateway can only be added once) 334 | {GatewayAddress, 0, gateway}; 335 | nonce_info(#blockchain_txn_assert_location_v1_pb{nonce = Nonce, gateway = GatewayAddress}) -> 336 | %% Asserting a location uses the gateway nonce 337 | {GatewayAddress, Nonce, gateway}; 338 | nonce_info(#blockchain_txn_assert_location_v2_pb{nonce = Nonce, gateway = GatewayAddress}) -> 339 | %% Asserting a location uses the gateway nonce 340 | {GatewayAddress, Nonce, gateway}; 341 | nonce_info(#blockchain_txn_payment_v1_pb{nonce = Nonce, payer = Address}) -> 342 | {Address, Nonce, balance}; 343 | nonce_info(#blockchain_txn_payment_v2_pb{nonce = Nonce, payer = Address}) -> 344 | {Address, Nonce, balance}; 345 | nonce_info(#blockchain_txn_create_htlc_v1_pb{nonce = Nonce, payer = Address}) -> 346 | {Address, Nonce, balance}; 347 | nonce_info(#blockchain_txn_redeem_htlc_v1_pb{}) -> 348 | {undefined, 0, balance}; 349 | nonce_info(#blockchain_txn_price_oracle_v1_pb{public_key = Address}) -> 350 | {Address, 0, none}; 351 | nonce_info(#blockchain_txn_security_exchange_v1_pb{nonce = Nonce, payer = Address}) -> 352 | {Address, Nonce, security}; 353 | nonce_info(#blockchain_txn_transfer_hotspot_v1_pb{buyer = Buyer, buyer_nonce = Nonce}) -> 354 | {Buyer, Nonce, balance}; 355 | nonce_info(#blockchain_txn_transfer_hotspot_v2_pb{gateway = GatewayAddress, nonce = Nonce}) -> 356 | {GatewayAddress, Nonce, gateway}; 357 | nonce_info(#blockchain_txn_token_burn_v1_pb{nonce = Nonce, payer = Address}) -> 358 | {Address, Nonce, balance}; 359 | nonce_info(#blockchain_txn_stake_validator_v1_pb{address = Address}) -> 360 | {Address, 0, none}; 361 | nonce_info(#blockchain_txn_transfer_validator_stake_v1_pb{old_address = Address}) -> 362 | {Address, 0, none}; 363 | nonce_info(#blockchain_txn_unstake_validator_v1_pb{address = Address}) -> 364 | {Address, 0, none}; 365 | nonce_info(#blockchain_txn_state_channel_open_v1_pb{owner = Address, nonce = Nonce}) -> 366 | {Address, Nonce, dc}; 367 | nonce_info(_) -> 368 | undefined. 369 | 370 | -spec load_db(Dir :: file:filename_all()) -> {ok, #state{}} | {error, any()}. 371 | load_db(Dir) -> 372 | case bn_db:open_db(Dir, ["default", "pending", "status"]) of 373 | {error, _Reason} = Error -> 374 | Error; 375 | {ok, DB, [DefaultCF, PendingCF, StatusCF]} -> 376 | State = #state{ 377 | db = DB, 378 | default = DefaultCF, 379 | pending = PendingCF, 380 | status = StatusCF 381 | }, 382 | compact_db(State), 383 | {ok, State} 384 | end. 385 | 386 | compact_db(#state{db = DB, default = Default, pending = PendingCF, status = StatusCF}) -> 387 | rocksdb:compact_range(DB, Default, undefined, undefined, []), 388 | rocksdb:compact_range(DB, PendingCF, undefined, undefined, []), 389 | rocksdb:compact_range(DB, StatusCF, undefined, undefined, []), 390 | ok. 391 | -------------------------------------------------------------------------------- /src/bn_sup.erl: -------------------------------------------------------------------------------- 1 | -module(bn_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | %% API 6 | -export([start_link/0, random_val_predicate/1]). 7 | %% Supervisor callbacks 8 | -export([init/1]). 9 | 10 | -define(SERVER, ?MODULE). 11 | -define(SUP(I, Args), #{ 12 | id => I, 13 | start => {I, start_link, Args}, 14 | restart => permanent, 15 | shutdown => 5000, 16 | type => supervisor, 17 | modules => [I] 18 | }). 19 | 20 | -define(WORKER(I, Args), ?WORKER(I, I, Args)). 21 | -define(WORKER(I, M, Args), #{ 22 | id => I, 23 | start => {M, start_link, Args}, 24 | restart => permanent, 25 | shutdown => 5000, 26 | type => worker, 27 | modules => [M] 28 | }). 29 | 30 | start_link() -> 31 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 32 | 33 | init([]) -> 34 | erlang:system_flag(fullsweep_after, 0), 35 | 36 | ok = libp2p_crypto:set_network(application:get_env(blockchain, network, mainnet)), 37 | 38 | SupFlags = #{strategy => one_for_all, intensity => 0, period => 1}, 39 | SeedNodes = 40 | case application:get_env(blockchain, seed_nodes) of 41 | {ok, ""} -> []; 42 | {ok, Seeds} -> string:split(Seeds, ",", all); 43 | _ -> [] 44 | end, 45 | 46 | BaseDir = application:get_env(blockchain, base_dir, "data"), 47 | SwarmKey = filename:join([BaseDir, "blockchain_node", "swarm_key"]), 48 | ok = filelib:ensure_dir(SwarmKey), 49 | {PublicKey, ECDHFun, SigFun} = 50 | case libp2p_crypto:load_keys(SwarmKey) of 51 | {ok, #{secret := PrivKey0, public := PubKey}} -> 52 | {PubKey, libp2p_crypto:mk_ecdh_fun(PrivKey0), libp2p_crypto:mk_sig_fun(PrivKey0)}; 53 | {error, enoent} -> 54 | KeyMap = 55 | #{secret := PrivKey0, public := PubKey} = libp2p_crypto:generate_keys( 56 | ecc_compact 57 | ), 58 | ok = libp2p_crypto:save_keys(KeyMap, SwarmKey), 59 | {PubKey, libp2p_crypto:mk_ecdh_fun(PrivKey0), libp2p_crypto:mk_sig_fun(PrivKey0)} 60 | end, 61 | SeedNodeDNS = application:get_env(blockchain, seed_node_dns, []), 62 | SeedAddresses = string:tokens( 63 | lists:flatten([ 64 | string:prefix(X, "blockchain-seed-nodes=") 65 | || [X] <- inet_res:lookup(SeedNodeDNS, in, txt), 66 | string:prefix(X, "blockchain-seed-nodes=") /= nomatch 67 | ]), 68 | "," 69 | ), 70 | 71 | Port = application:get_env(blockchain, port, 0), 72 | MaxInboundConnections = application:get_env(blockchain, max_inbound_connections, 10), 73 | BlockchainOpts = [ 74 | {key, {PublicKey, SigFun, ECDHFun}}, 75 | {seed_nodes, SeedNodes ++ SeedAddresses}, 76 | {max_inbound_connections, MaxInboundConnections}, 77 | {port, Port}, 78 | {update_dir, "update"}, 79 | {base_dir, BaseDir} 80 | ], 81 | 82 | {ok, NodePort} = application:get_env(blockchain_node, jsonrpc_port), 83 | {ok, 84 | {SupFlags, [ 85 | ?SUP(blockchain_sup, [BlockchainOpts]), 86 | ?WORKER(txn_follower, blockchain_follower, [ 87 | [{follower_module, {bn_txns, [{base_dir, BaseDir}]}}] 88 | ]), 89 | ?WORKER(pending_txn_follower, blockchain_follower, [ 90 | [{follower_module, {bn_pending_txns, [{base_dir, BaseDir}]}}] 91 | ]), 92 | ?WORKER(oracle_price_follower, blockchain_follower, [ 93 | [{follower_module, {bn_oracle_price, [{base_dir, BaseDir}]}}] 94 | ]), 95 | ?WORKER(bn_wallets, [[{base_dir, BaseDir}]]), 96 | ?WORKER(elli, [[{callback, bn_jsonrpc_handler}, {port, NodePort}]]), 97 | ?WORKER(bn_metrics_server, []) 98 | ]}}. 99 | 100 | random_val_predicate(Peer) -> 101 | not libp2p_peer:is_stale(Peer, timer:minutes(360)) andalso 102 | maps:get(<<"release_version">>, libp2p_peer:signed_metadata(Peer), undefined) /= undefined. 103 | -------------------------------------------------------------------------------- /src/bn_txns.erl: -------------------------------------------------------------------------------- 1 | -module(bn_txns). 2 | 3 | -include("bn_jsonrpc.hrl"). 4 | 5 | % -behaviour(blockchain_follower). 6 | 7 | -behavior(bn_jsonrpc_handler). 8 | 9 | %% blockchain_follower 10 | -export([ 11 | requires_sync/0, 12 | requires_ledger/0, 13 | init/1, 14 | follower_height/1, 15 | load_chain/2, 16 | load_block/5, 17 | terminate/2 18 | ]). 19 | 20 | %% jsonrpc_handler 21 | -export([handle_rpc/2]). 22 | %% api 23 | -export([follower_height/0]). 24 | 25 | -define(DB_FILE, "transactions.db"). 26 | 27 | -record(state, { 28 | db :: rocksdb:db_handle(), 29 | default :: rocksdb:cf_handle(), 30 | heights :: rocksdb:cf_handle(), 31 | transactions :: rocksdb:cf_handle(), 32 | json :: rocksdb:cf_handle() 33 | }). 34 | 35 | requires_ledger() -> false. 36 | 37 | requires_sync() -> false. 38 | 39 | init(Args) -> 40 | Dir = filename:join(proplists:get_value(base_dir, Args, "data"), ?DB_FILE), 41 | case load_db(Dir) of 42 | {error, {db_open, "Corruption:" ++ _Reason}} -> 43 | lager:error("DB could not be opened corrupted ~p, cleaning up", [_Reason]), 44 | ok = bn_db:clean_db(Dir), 45 | init(Args); 46 | {ok, State} -> 47 | persistent_term:put(?MODULE, State), 48 | {ok, State#state{}} 49 | end. 50 | 51 | follower_height(#state{db = DB, default = DefaultCF}) -> 52 | case bn_db:get_follower_height(DB, DefaultCF) of 53 | {ok, Height} -> Height; 54 | {error, _} = Error -> ?jsonrpc_error(Error) 55 | end. 56 | 57 | load_chain(Chain, State = #state{}) -> 58 | maybe_load_genesis(Chain, State). 59 | 60 | maybe_load_genesis(Chain, State = #state{}) -> 61 | case blockchain:get_block(1, Chain) of 62 | {ok, Block} -> 63 | Hash = blockchain_txn:hash(lists:last(blockchain_block:transactions(Block))), 64 | case get_transaction(Hash, State) of 65 | % already loaded 66 | {ok, _} -> 67 | {ok, State}; 68 | % attempt to load 69 | _ -> 70 | load_block([], Block, [], [], State) 71 | end; 72 | Error -> 73 | Error 74 | end. 75 | 76 | load_block(_Hash, Block, _Sync, _Ledger, State = #state{}) -> 77 | BlockHeight = blockchain_block_v1:height(Block), 78 | Transactions = blockchain_block:transactions(Block), 79 | lager:info("Loading Block ~p (~p transactions)", [BlockHeight, length(Transactions)]), 80 | Chain = blockchain_worker:blockchain(), 81 | ok = save_transactions( 82 | BlockHeight, 83 | Transactions, 84 | blockchain:ledger(Chain), 85 | Chain, 86 | State 87 | ), 88 | {ok, State}. 89 | 90 | terminate(_Reason, #state{db = DB}) -> 91 | rocksdb:close(DB). 92 | 93 | %% 94 | %% jsonrpc_handler 95 | %% 96 | 97 | handle_rpc(<<"transaction_get">>, {Param}) -> 98 | Hash = ?jsonrpc_b64_to_bin(<<"hash">>, Param), 99 | {ok, State} = get_state(), 100 | case get_transaction_json(Hash, State) of 101 | {ok, Json} -> 102 | Json; 103 | {error, not_found} -> 104 | ?jsonrpc_error({not_found, "No transaction: ~p", [?BIN_TO_B64(Hash)]}); 105 | {error, _} = Error -> 106 | ?jsonrpc_error(Error) 107 | end; 108 | handle_rpc(_, _) -> 109 | ?jsonrpc_error(method_not_found). 110 | 111 | %% 112 | %% api 113 | %% 114 | 115 | follower_height() -> 116 | {ok, State} = get_state(), 117 | follower_height(State). 118 | 119 | %% 120 | %% Internal 121 | %% 122 | 123 | get_state() -> 124 | bn_db:get_state(?MODULE). 125 | 126 | -spec get_transaction(Hash :: binary(), #state{}) -> 127 | {ok, {Height :: pos_integer() | undefined, blockchain_txn:txn()}} | {error, term()}. 128 | get_transaction(Hash, #state{db = DB, heights = HeightsCF, transactions = TransactionsCF}) -> 129 | case rocksdb:get(DB, TransactionsCF, Hash, []) of 130 | {ok, BinTxn} -> 131 | Height = 132 | case rocksdb:get(DB, HeightsCF, Hash, []) of 133 | not_found -> undefined; 134 | {ok, <>} -> H 135 | end, 136 | {ok, {Height, blockchain_txn:deserialize(BinTxn)}}; 137 | not_found -> 138 | {error, not_found}; 139 | Error -> 140 | Error 141 | end. 142 | 143 | -spec get_transaction_json(Hash :: binary(), #state{}) -> 144 | {ok, blockchain_json:json_object()} | {error, term()}. 145 | get_transaction_json(Hash, State = #state{db = DB, json = JsonCF}) -> 146 | Chain = blockchain_worker:blockchain(), 147 | case rocksdb:get(DB, JsonCF, Hash, []) of 148 | {ok, BinJson} -> 149 | Json = jsone:decode(BinJson, []), 150 | case blockchain:get_implicit_burn(Hash, Chain) of 151 | {ok, ImplicitBurn} -> 152 | {ok, Json#{implicit_burn => blockchain_implicit_burn:to_json(ImplicitBurn, [])}}; 153 | {error, _} -> 154 | {ok, Json} 155 | end; 156 | not_found -> 157 | case get_transaction(Hash, State) of 158 | {ok, {Height, Txn}} -> 159 | Json = blockchain_txn:to_json(Txn, []), 160 | case blockchain:get_implicit_burn(Hash, Chain) of 161 | {ok, ImplicitBurn} -> 162 | {ok, Json#{ 163 | block => Height, 164 | implicit_burn => blockchain_implicit_burn:to_json(ImplicitBurn, []) 165 | }}; 166 | {error, _} -> 167 | {ok, Json#{block => Height}} 168 | end; 169 | Error -> 170 | Error 171 | end; 172 | Error -> 173 | Error 174 | end. 175 | 176 | save_transactions(Height, Transactions, Ledger, Chain, #state{ 177 | db = DB, 178 | default = DefaultCF, 179 | heights = HeightsCF, 180 | transactions = TransactionsCF, 181 | json = JsonCF 182 | }) -> 183 | {ok, Batch} = rocksdb:batch(), 184 | HeightBin = <>, 185 | lists:foreach( 186 | fun(Txn) -> 187 | Hash = blockchain_txn:hash(Txn), 188 | case application:get_env(blockchain, store_json, false) of 189 | true -> 190 | Json = 191 | try 192 | blockchain_txn:to_json(Txn, [{ledger, Ledger}, {chain, Chain}]) 193 | catch 194 | _:_ -> 195 | blockchain_txn:to_json(Txn, []) 196 | end, 197 | ok = rocksdb:batch_put( 198 | Batch, 199 | JsonCF, 200 | Hash, 201 | jsone:encode(Json#{block => Height}, [undefined_as_null]) 202 | ); 203 | _ -> 204 | ok 205 | end, 206 | 207 | ok = rocksdb:batch_put( 208 | Batch, 209 | TransactionsCF, 210 | Hash, 211 | blockchain_txn:serialize(Txn) 212 | ), 213 | ok = rocksdb:batch_put(Batch, HeightsCF, Hash, HeightBin) 214 | end, 215 | Transactions 216 | ), 217 | bn_db:batch_put_follower_height(Batch, DefaultCF, Height), 218 | rocksdb:write_batch(DB, Batch, [{sync, true}]). 219 | 220 | -spec load_db(file:filename_all()) -> {ok, #state{}} | {error, any()}. 221 | load_db(Dir) -> 222 | case bn_db:open_db(Dir, ["default", "heights", "transactions", "json"]) of 223 | {error, _Reason} = Error -> 224 | Error; 225 | {ok, DB, [DefaultCF, HeightsCF, TransactionsCF, JsonCF]} -> 226 | State = #state{ 227 | db = DB, 228 | default = DefaultCF, 229 | heights = HeightsCF, 230 | transactions = TransactionsCF, 231 | json = JsonCF 232 | }, 233 | compact_db(State), 234 | {ok, State} 235 | end. 236 | 237 | compact_db(#state{db = DB, default = Default, transactions = TransactionsCF, json = JsonCF}) -> 238 | rocksdb:compact_range(DB, Default, undefined, undefined, []), 239 | rocksdb:compact_range(DB, TransactionsCF, undefined, undefined, []), 240 | rocksdb:compact_range(DB, JsonCF, undefined, undefined, []), 241 | ok. 242 | -------------------------------------------------------------------------------- /src/bn_wallets.erl: -------------------------------------------------------------------------------- 1 | -module(bn_wallets). 2 | 3 | -include("bn_jsonrpc.hrl"). 4 | 5 | -behavior(bn_jsonrpc_handler). 6 | -behavior(gen_server). 7 | 8 | %% gen_server 9 | -export([start_link/1, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). 10 | %% jsonrpc_handler 11 | -export([handle_rpc/2]). 12 | 13 | -define(DB_FILE, "wallets.db"). 14 | -define(SERVER, ?MODULE). 15 | -define(KEY_TIMEOUT, 60000). 16 | -define(UNLOCK_TIMEOUT, 30000). 17 | 18 | -record(state, { 19 | dir :: file:filename_all(), 20 | db :: rocksdb:db_handle(), 21 | default :: rocksdb:cf_handle(), 22 | wallets :: rocksdb:cf_handle(), 23 | keys = #{} :: #{libp2p_crypto:pubkey_bin() => libp2p_crypto:key_map()} 24 | }). 25 | 26 | -spec unlock(libp2p_crypto:pubkey_bin(), binary()) -> ok | {error, term()}. 27 | unlock(Address, Password) -> 28 | gen_server:call(?SERVER, {unlock, Address, Password}, ?UNLOCK_TIMEOUT). 29 | 30 | -spec sign(libp2p_crypto:pubkey_bin(), blockchain_txn:txn()) -> 31 | {ok, blockchain_txn:txn()} | {error, term()}. 32 | sign(Address, Txn) -> 33 | gen_server:call(?SERVER, {sign, Address, Txn}). 34 | 35 | -spec keys(libp2p_crypto:pubkey_bin()) -> {ok, lib2p_crypto:key_map()} | {error, term()}. 36 | keys(Address) -> 37 | gen_server:call(?SERVER, {keys, Address}). 38 | 39 | -spec lock(libp2p_crypto:pubkey_bin()) -> ok. 40 | lock(Address) -> 41 | gen_server:call(?SERVER, {lock, Address}). 42 | 43 | -spec is_locked(libp2p_crypto:pubkey_bin()) -> boolean(). 44 | is_locked(Address) -> 45 | gen_server:call(?SERVER, {is_locked, Address}). 46 | 47 | restore(Path, BackupID) -> 48 | gen_server:call(?SERVER, {restore, Path, BackupID}). 49 | 50 | %% 51 | %% gen_server 52 | %% 53 | start_link(Args) -> 54 | gen_server:start_link({local, ?SERVER}, ?MODULE, Args, []). 55 | 56 | init(Args) -> 57 | Dir = filename:join(proplists:get_value(base_dir, Args, "data"), ?DB_FILE), 58 | case load_db(Dir) of 59 | {ok, State} -> 60 | persistent_term:put(?MODULE, State), 61 | {ok, State}; 62 | Error -> 63 | Error 64 | end. 65 | 66 | handle_call({unlock, Address, Password}, _From, State) -> 67 | case maps:is_key(Address, State#state.keys) of 68 | true -> 69 | {reply, ok, State}; 70 | false -> 71 | case get_wallet(Address, State) of 72 | {error, Error} -> 73 | {reply, {error, Error}, State}; 74 | {ok, Wallet} -> 75 | case wallet:decrypt(Password, Wallet) of 76 | {error, Error} -> 77 | {reply, {error, Error}, State}; 78 | {ok, KeyMap} -> 79 | timer:send_after(?KEY_TIMEOUT, self(), {key_timeout, Address}), 80 | {reply, ok, State#state{ 81 | keys = maps:put(Address, KeyMap, State#state.keys) 82 | }} 83 | end 84 | end 85 | end; 86 | handle_call({lock, Address}, _From, State) -> 87 | {reply, ok, State#state{keys = maps:remove(Address, State#state.keys)}}; 88 | handle_call({is_locked, Address}, _From, State) -> 89 | {reply, not maps:is_key(Address, State#state.keys), State}; 90 | handle_call({keys, Address}, _From, State) -> 91 | case maps:get(Address, State#state.keys, false) of 92 | false -> 93 | {reply, {error, not_found}, State}; 94 | KeyMap -> 95 | {reply, {ok, KeyMap}, State} 96 | end; 97 | handle_call({sign, Address, Txn}, _From, State) -> 98 | case maps:get(Address, State#state.keys, false) of 99 | false -> 100 | {reply, {error, not_found}, State}; 101 | #{secret := PrivKey} -> 102 | SigFun = libp2p_crypto:mk_sig_fun(PrivKey), 103 | {reply, {ok, blockchain_txn:sign(Txn, SigFun)}, State} 104 | end; 105 | handle_call({restore, Path, BackupID}, _From, State) -> 106 | {ok, Engine} = rocksdb:open_backup_engine(Path), 107 | case rocksdb:verify_backup(Engine, BackupID) of 108 | {error, Error} -> 109 | {reply, {error, Error}, State}; 110 | ok -> 111 | rocksdb:close(State#state.db), 112 | case rocksdb:restore_db_from_backup(Engine, BackupID, State#state.dir) of 113 | ok -> 114 | case load_db(State#state.dir) of 115 | {ok, NewState} -> 116 | persistent_term:put(?MODULE, NewState), 117 | {reply, ok, NewState}; 118 | Error -> 119 | {reply, Error, State} 120 | end 121 | end 122 | end; 123 | handle_call(Request, _From, State) -> 124 | lager:notice("Unhandled call ~p", [Request]), 125 | {reply, ok, State}. 126 | 127 | handle_cast(Msg, State) -> 128 | lager:notice("Unhandled cast ~p", [Msg]), 129 | {noreply, State}. 130 | 131 | handle_info({key_timeout, Address}, State) -> 132 | {noreply, State#state{keys = maps:remove(Address, State#state.keys)}}; 133 | handle_info(Info, State) -> 134 | lager:notice("Unhandled info ~p", [Info]), 135 | {noreply, State}. 136 | 137 | terminate(_Reason, #state{db = DB}) -> 138 | rocksdb:close(DB). 139 | 140 | %% 141 | %% jsonrpc_handler 142 | %% 143 | 144 | handle_rpc(<<"wallet_create">>, {Param}) -> 145 | KeyMap = libp2p_crypto:generate_keys(ed25519), 146 | Password = 147 | case ?jsonrpc_get_param(<<"password">>, Param) of 148 | V when is_binary(V) andalso byte_size(V) > 0 -> V; 149 | _ -> ?jsonrpc_error(invalid_params) 150 | end, 151 | {ok, State} = get_state(), 152 | {ok, Wallet} = wallet:encrypt(KeyMap, Password), 153 | ok = save_wallet(Wallet, State), 154 | ?BIN_TO_B58(wallet:pubkey_bin(Wallet)); 155 | handle_rpc(<<"wallet_delete">>, {Param}) -> 156 | Address = ?jsonrpc_b58_to_bin(<<"address">>, Param), 157 | {ok, State} = get_state(), 158 | case delete_wallet(Address, State) of 159 | {error, _} = Error -> 160 | ?jsonrpc_error(Error); 161 | ok -> 162 | true 163 | end; 164 | handle_rpc(<<"wallet_list">>, _Params) -> 165 | {ok, State} = get_state(), 166 | [?BIN_TO_B58(Addr) || Addr <- get_wallet_list(State)]; 167 | handle_rpc(<<"wallet_unlock">>, {Param}) -> 168 | Address = ?jsonrpc_b58_to_bin(<<"address">>, Param), 169 | Password = ?jsonrpc_get_param(<<"password">>, Param), 170 | case unlock(Address, Password) of 171 | {error, not_found} -> 172 | ?jsonrpc_error({not_found, "Wallet not found"}); 173 | {error, decrypt} -> 174 | ?jsonrpc_error(invalid_password); 175 | ok -> 176 | true 177 | end; 178 | handle_rpc(<<"wallet_lock">>, {Param}) -> 179 | Address = ?jsonrpc_b58_to_bin(<<"address">>, Param), 180 | ok = lock(Address), 181 | true; 182 | handle_rpc(<<"wallet_is_locked">>, {Param}) -> 183 | Address = ?jsonrpc_b58_to_bin(<<"address">>, Param), 184 | is_locked(Address); 185 | handle_rpc(<<"wallet_pay">>, {Param}) -> 186 | Payer = ?jsonrpc_b58_to_bin(<<"address">>, Param), 187 | Payee = ?jsonrpc_b58_to_bin(<<"payee">>, Param), 188 | Amount = ?jsonrpc_get_param(<<"bones">>, Param, undefined), 189 | Max = ?jsonrpc_get_param(<<"max">>, Param, false), 190 | TokenBin = ?jsonrpc_get_param(<<"token_type">>, Param, <<"hnt">>), 191 | Token = jsonrpc_binary_to_token_type(TokenBin), 192 | Chain = blockchain_worker:blockchain(), 193 | Nonce = jsonrpc_nonce_param(Param, Payer, balance, Chain), 194 | 195 | case mk_payment_txn_v2(Payer, [{Payee, Token, Amount, Max}], Nonce, Chain) of 196 | {ok, Txn} -> 197 | case sign(Payer, Txn) of 198 | {ok, SignedTxn} -> 199 | {ok, _} = bn_pending_txns:submit_txn(SignedTxn), 200 | blockchain_txn:to_json(SignedTxn, []); 201 | {error, not_found} -> 202 | ?jsonrpc_error({not_found, "Wallet is locked"}) 203 | end; 204 | {error, invalid_payment} -> 205 | ?jsonrpc_error({invalid_params, "Missing or invalid payment amount"}) 206 | end; 207 | handle_rpc(<<"wallet_pay_multi">>, {Param}) -> 208 | Payer = ?jsonrpc_b58_to_bin(<<"address">>, Param), 209 | Payments = 210 | case ?jsonrpc_get_param(<<"payments">>, Param, false) of 211 | L when is_list(L) andalso length(L) > 0 -> 212 | lists:map( 213 | fun({Entry}) -> 214 | Payee = ?jsonrpc_b58_to_bin(<<"payee">>, Entry), 215 | Amount = ?jsonrpc_get_param(<<"bones">>, Entry, undefined), 216 | Max = ?jsonrpc_get_param(<<"max">>, Entry, false), 217 | TokenBin = ?jsonrpc_get_param(<<"token_type">>, Entry, <<"hnt">>), 218 | Token = jsonrpc_binary_to_token_type(TokenBin), 219 | {Payee, Token, Amount, Max} 220 | end, 221 | L 222 | ); 223 | _ -> 224 | ?jsonrpc_error({invalid_params, "Missing or empty payment list"}) 225 | end, 226 | Chain = blockchain_worker:blockchain(), 227 | Nonce = jsonrpc_nonce_param(Param, Payer, balance, Chain), 228 | 229 | case mk_payment_txn_v2(Payer, Payments, Nonce, Chain) of 230 | {ok, Txn} -> 231 | case sign(Payer, Txn) of 232 | {ok, SignedTxn} -> 233 | {ok, _} = bn_pending_txns:submit_txn(SignedTxn), 234 | blockchain_txn:to_json(SignedTxn, []); 235 | {error, not_found} -> 236 | ?jsonrpc_error({not_found, "Wallet is locked"}) 237 | end; 238 | {error, invalid_payment} -> 239 | ?jsonrpc_error({invalid_params, "Missing or invalid payment(s)"}) 240 | end; 241 | handle_rpc(<<"wallet_import">>, {Param}) -> 242 | Password = ?jsonrpc_get_param(<<"password">>, Param), 243 | Path = ?jsonrpc_get_param(<<"path">>, Param), 244 | {ok, State} = get_state(), 245 | case file:read_file(Path) of 246 | {error, enoent} -> 247 | ?jsonrpc_error({not_found, "Path not found"}); 248 | {error, _} = Error -> 249 | ?jsonrpc_error(Error); 250 | {ok, FileBin} -> 251 | case wallet:from_binary(FileBin) of 252 | {error, _} = Error -> 253 | ?jsonrpc_error(Error); 254 | {ok, Wallet} -> 255 | case wallet:decrypt(Password, Wallet) of 256 | {error, decrypt} -> 257 | ?jsonrpc_error(invalid_password); 258 | {ok, _} -> 259 | ok = save_wallet(Wallet, State), 260 | ?BIN_TO_B58(wallet:pubkey_bin(Wallet)) 261 | end 262 | end 263 | end; 264 | handle_rpc(<<"wallet_export">>, {Param}) -> 265 | Address = ?jsonrpc_b58_to_bin(<<"address">>, Param), 266 | Path = ?jsonrpc_get_param(<<"path">>, Param), 267 | {ok, State} = get_state(), 268 | case get_wallet(Address, State) of 269 | {error, not_found} -> 270 | ?jsonrpc_error({not_found, "Wallet not found"}); 271 | {ok, Wallet} -> 272 | WalletBin = wallet:to_binary(Wallet), 273 | case file:write_file(Path, WalletBin) of 274 | ok -> true; 275 | {error, _} = Error -> ?jsonrpc_error(Error) 276 | end 277 | end; 278 | handle_rpc(<<"wallet_export_secret">>, {Param}) -> 279 | Address = ?jsonrpc_b58_to_bin(<<"address">>, Param), 280 | Path = ?jsonrpc_get_param(<<"path">>, Param), 281 | case keys(Address) of 282 | {error, not_found} -> 283 | ?jsonrpc_error({not_found, "Wallet not found"}); 284 | {ok, #{secret := {ed25519, <>}}} -> 285 | case jsone:try_encode(binary:bin_to_list(Secret), []) of 286 | {ok, Json} -> 287 | case file:write_file(Path, Json) of 288 | ok -> true; 289 | {error, _} = Error -> ?jsonrpc_error(Error) 290 | end; 291 | {error, _} = Error -> ?jsonrpc_error(Error) 292 | end; 293 | {ok, _} -> 294 | ?jsonrpc_error({not_supported, "Wallet not ed25519"}) 295 | end; 296 | handle_rpc(<<"wallet_backup_list">>, {Param}) -> 297 | Path = ?jsonrpc_get_param(<<"path">>, Param), 298 | {ok, Engine} = rocksdb:open_backup_engine(binary_to_list(Path)), 299 | {ok, Info} = rocksdb:get_backup_info(Engine), 300 | Info; 301 | handle_rpc(<<"wallet_backup_create">>, {Param}) -> 302 | Path = ?jsonrpc_get_param(<<"path">>, Param), 303 | NumBackupToKeep = ?jsonrpc_get_param(<<"max_backups">>, Param), 304 | {ok, Engine} = rocksdb:open_backup_engine(binary_to_list(Path)), 305 | {ok, #state{db = DB}} = get_state(), 306 | ok = rocksdb:create_new_backup(Engine, DB), 307 | ok = rocksdb:purge_old_backup(Engine, NumBackupToKeep), 308 | {ok, Info} = rocksdb:get_backup_info(Engine), 309 | LastBackup = hd(Info), 310 | LastBackup; 311 | handle_rpc(<<"wallet_backup_delete">>, {Param}) -> 312 | Path = ?jsonrpc_get_param(<<"path">>, Param), 313 | BackupID = ?jsonrpc_get_param(<<"backup_id">>, Param), 314 | {ok, Engine} = rocksdb:open_backup_engine(binary_to_list(Path)), 315 | case rocksdb:delete_backup(Engine, BackupID) of 316 | ok -> 317 | true; 318 | {error, not_found} -> 319 | ?jsonrpc_error({not_found, "Backup not found: ~p", [BackupID]}); 320 | {error, _} = Error -> 321 | ?jsonrpc_error(Error) 322 | end; 323 | handle_rpc(<<"wallet_backup_restore">>, {Param}) -> 324 | Path = ?jsonrpc_get_param(<<"path">>, Param), 325 | BackupID = ?jsonrpc_get_param(<<"backup_id">>, Param), 326 | case restore(binary_to_list(Path), BackupID) of 327 | ok -> 328 | true; 329 | {error, not_found} -> 330 | ?jsonrpc_error({not_found, "Backup not found: ~p", [BackupID]}); 331 | {error, _} = Error -> 332 | ?jsonrpc_error(Error) 333 | end; 334 | handle_rpc(_, _) -> 335 | ?jsonrpc_error(method_not_found). 336 | 337 | %% 338 | %% Internal 339 | %% 340 | 341 | %% Gets a nonce from a given jsonrpc parameter list. If not present it gets the 342 | %% speculative nonce for the given account address and adds one to construct a new 343 | %% nonce. 344 | -spec jsonrpc_nonce_param( 345 | [term()], 346 | libp2p_crypto:pubkey_bin(), 347 | bn_pending_txns:nonce_type(), 348 | blockchain:chain() 349 | ) -> 350 | non_neg_integer(). 351 | jsonrpc_nonce_param(Param, Address, NonceType, Chain) -> 352 | case ?jsonrpc_get_param(<<"nonce">>, Param, false) of 353 | false -> 354 | bn_accounts:get_speculative_nonce( 355 | Address, 356 | NonceType, 357 | blockchain:ledger(Chain) 358 | ) + 1; 359 | V when is_integer(V) -> 360 | V; 361 | _ -> 362 | ?jsonrpc_error({invalid_params, Param}) 363 | end. 364 | 365 | -spec mk_payment_txn_v2( 366 | Payer :: libp2p_crypto:pubkey_bin(), 367 | [ 368 | { 369 | Payee :: libp2p_crypto:pubkey_bin(), 370 | Token :: atom(), 371 | Bones :: pos_integer() | undefined, 372 | Max :: boolean() 373 | } 374 | ], 375 | Nonce :: non_neg_integer(), 376 | Chain :: blockchain:blockchain() 377 | ) -> 378 | {ok, blockchain_txn:txn()} | {error, term()}. 379 | mk_payment_txn_v2(Payer, PaymentList, Nonce, Chain) -> 380 | try 381 | Payments = [ 382 | mk_payment(Payee, Token, Bones, Max) 383 | || {Payee, Token, Bones, Max} <- PaymentList 384 | ], 385 | Txn = blockchain_txn_payment_v2:new(Payer, Payments, Nonce), 386 | TxnFee = blockchain_txn_payment_v2:calculate_fee(Txn, Chain), 387 | {ok, blockchain_txn_payment_v2:fee(Txn, TxnFee)} 388 | catch 389 | _:_ -> {error, invalid_payment} 390 | end. 391 | 392 | -spec mk_payment( 393 | Payee :: libp2p_crypto:pubkey_bin(), 394 | Token :: blockchain_token_v1:type(), 395 | Bones :: (undefined | non_neg_integer()), 396 | PayMaximum :: boolean() 397 | ) -> blockchain_payment_v2:payment(). 398 | mk_payment(Payee, Token, undefined, true) -> 399 | blockchain_payment_v2:new(Payee, max, 0, Token); 400 | mk_payment(Payee, Token, Bones, false) -> 401 | blockchain_payment_v2:new(Payee, Bones, 0, Token). 402 | 403 | get_state() -> 404 | case persistent_term:get(?MODULE, false) of 405 | false -> 406 | {error, {no_database, ?MODULE}}; 407 | State -> 408 | {ok, State} 409 | end. 410 | 411 | -spec get_wallet(libp2p_crypto:pubkey_bin(), #state{}) -> 412 | {ok, wallet:wallet()} | {error, term()}. 413 | get_wallet(Address, #state{db = DB, wallets = WalletCF}) -> 414 | case rocksdb:get(DB, WalletCF, Address, []) of 415 | not_found -> 416 | {error, not_found}; 417 | {ok, BinWallet} -> 418 | wallet:from_binary(BinWallet); 419 | Error -> 420 | Error 421 | end. 422 | 423 | get_wallet_list(#state{db = DB, wallets = WalletCF}) -> 424 | {ok, Itr} = rocksdb:iterator(DB, WalletCF, []), 425 | Wallets = get_wallet_list(Itr, rocksdb:iterator_move(Itr, first), []), 426 | catch rocksdb:iterator_close(Itr), 427 | Wallets. 428 | 429 | get_wallet_list(_Itr, {error, _Error}, Acc) -> 430 | lists:reverse(Acc); 431 | get_wallet_list(Itr, {ok, Addr, _}, Acc) -> 432 | get_wallet_list(Itr, rocksdb:iterator_move(Itr, next), [Addr | Acc]). 433 | 434 | -spec save_wallet(wallet:wallet(), #state{}) -> ok | {error, term()}. 435 | save_wallet(Wallet, #state{db = DB, wallets = WalletCF}) -> 436 | PubKeyBin = wallet:pubkey_bin(Wallet), 437 | WalletBin = wallet:to_binary(Wallet), 438 | rocksdb:put(DB, WalletCF, PubKeyBin, WalletBin, [{sync, true}]). 439 | 440 | -spec delete_wallet(Address :: libp2p_crypto:pubkey_bin(), #state{}) -> 441 | ok | {error, term()}. 442 | delete_wallet(Address, #state{db = DB, wallets = WalletCF}) -> 443 | rocksdb:delete(DB, WalletCF, Address, [{sync, true}]). 444 | 445 | -spec load_db(file:filename_all()) -> {ok, #state{}} | {error, any()}. 446 | load_db(Dir) -> 447 | case bn_db:open_db(Dir, ["default", "wallets"]) of 448 | {error, _Reason} = Error -> 449 | Error; 450 | {ok, DB, [DefaultCF, WalletCF]} -> 451 | State = #state{ 452 | dir = Dir, 453 | db = DB, 454 | default = DefaultCF, 455 | wallets = WalletCF 456 | }, 457 | compact_db(State), 458 | {ok, State} 459 | end. 460 | 461 | compact_db(#state{db = DB, default = Default, wallets = WalletCF}) -> 462 | rocksdb:compact_range(DB, Default, undefined, undefined, []), 463 | rocksdb:compact_range(DB, WalletCF, undefined, undefined, []), 464 | ok. 465 | 466 | -spec jsonrpc_binary_to_token_type( 467 | TokenBin :: binary() 468 | ) -> atom(). 469 | jsonrpc_binary_to_token_type(Token) -> 470 | case catch binary_to_existing_atom(Token) of 471 | TokenType when is_atom(TokenType) -> TokenType; 472 | _ -> ?jsonrpc_error({invalid_params, "Invalid token type found"}) 473 | end. 474 | 475 | -ifdef(TEST). 476 | -include_lib("eunit/include/eunit.hrl"). 477 | 478 | token_test() -> 479 | ?assertEqual(jsonrpc_binary_to_token_type(<<"hnt">>), hnt), 480 | ?assertThrow({invalid_params, _}, jsonrpc_binary_to_token_type(<<" NOT EVER A TOKEN">>)). 481 | 482 | -endif. 483 | -------------------------------------------------------------------------------- /src/helium_follower_service.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% 3 | %% Handler module for the follower service's streaming API/RPC 4 | %% 5 | %%%------------------------------------------------------------------- 6 | -module(helium_follower_service). 7 | 8 | -behaviour(helium_follower_follower_bhvr). 9 | 10 | -include("grpc/autogen/server/follower_pb.hrl"). 11 | -include_lib("blockchain/include/blockchain.hrl"). 12 | -include_lib("blockchain/include/blockchain_vars.hrl"). 13 | 14 | -export([ 15 | txn_stream/2, 16 | find_gateway/2, 17 | subnetwork_last_reward_height/2, 18 | active_gateways/2 19 | ]). 20 | 21 | -export([init/2, handle_info/2]). 22 | 23 | -type handler_state() :: #{ 24 | chain => blockchain:blockchain(), 25 | streaming_initialized => boolean(), 26 | txn_types => [atom()] 27 | }. 28 | 29 | -export_type([handler_state/0]). 30 | 31 | -define(GW_STREAM_BATCH_SIZE, 5000). 32 | 33 | %% ------------------------------------------------------------------- 34 | %% helium_follower_bhvr callback functions 35 | %% ------------------------------------------------------------------- 36 | -spec txn_stream(follower_pb:follower_txn_stream_req_v1_pb(), grpcbox_stream:t()) -> 37 | {ok, grpcbox_stream:t()} | grpcbox_stream:grpc_error_response(). 38 | txn_stream(#follower_txn_stream_req_v1_pb{} = Msg, StreamState) -> 39 | #{chain := Chain, streaming_initialized := StreamInitialized} = 40 | grpcbox_stream:stream_handler_state(StreamState), 41 | txn_stream(Chain, StreamInitialized, Msg, StreamState); 42 | txn_stream(_Msg, StreamState) -> 43 | lager:warning("unhandled grpc msg ~p", [_Msg]), 44 | {ok, StreamState}. 45 | 46 | -spec find_gateway(ctx:ctx(), follower_pb:follower_gateway_req_v1_pb()) -> 47 | {ok, follower_pb:follower_gateway_resp_v1_pb(), ctx:ctx()} 48 | | grpcbox_stream:grpc_error_response(). 49 | find_gateway(Ctx, Req) -> 50 | Chain = blockchain_worker:cached_blockchain(), 51 | find_gateway(Chain, Ctx, Req). 52 | 53 | -spec subnetwork_last_reward_height( 54 | ctx:ctx(), follower_pb:follower_subnetwork_last_reward_height_req_v1_pb() 55 | ) -> 56 | {ok, follower_pb:follower_subnetwork_last_reward_height_resp_v1_pb(), ctx:ctx()} 57 | | grpcbox_stream:grpc_error_response(). 58 | subnetwork_last_reward_height(Ctx, Req) -> 59 | Chain = blockchain_worker:cached_blockchain(), 60 | subnetwork_last_reward_height(Chain, Ctx, Req). 61 | 62 | -spec active_gateways(follower_pb:follower_gateway_stream_req_v1_pb(), grpcbox_stream:t()) -> 63 | {ok, grpcbox_stream:t()} | grpcbox_stream:grpc_error_response(). 64 | active_gateways(#follower_gateway_stream_req_v1_pb{batch_size = BatchSize} = _Msg, StreamState) when BatchSize =< ?GW_STREAM_BATCH_SIZE -> 65 | #{chain := Chain} = grpcbox_stream:stream_handler_state(StreamState), 66 | case Chain of 67 | undefined -> 68 | lager:debug("chain not ready, returning error response for msg ~p", [_Msg]), 69 | {grpc_error, {grpcbox_stream:code_to_status(14), <<"temporarily unavailable">>}}; 70 | _ -> 71 | Ledger = blockchain:ledger(Chain), 72 | {ok, Height} = blockchain_ledger_v1:current_height(Ledger), 73 | {NumGateways, FinalGws, StreamState1} = 74 | blockchain_ledger_v1:cf_fold( 75 | active_gateways, 76 | fun({Addr, BinGw}, {CountAcc, GwAcc, StreamAcc}) -> 77 | Gw = blockchain_ledger_gateway_v2:deserialize(BinGw), 78 | Loc = blockchain_ledger_gateway_v2:location(Gw), 79 | case Loc /= undefined of 80 | true -> 81 | Region = case blockchain_region_v1:h3_to_region(Loc, Ledger) of 82 | {ok, R} -> normalize_region(R); 83 | _ -> undefined 84 | end, 85 | GwResp = #follower_gateway_resp_v1_pb{ 86 | height = Height, 87 | result = {info, #gateway_info_pb{ 88 | address = Addr, 89 | location = h3:to_string(Loc), 90 | owner = blockchain_ledger_gateway_v2:owner_address(Gw), 91 | staking_mode = blockchain_ledger_gateway_v2:mode(Gw), 92 | gain = blockchain_ledger_gateway_v2:gain(Gw), 93 | region = Region, 94 | %% we dont need region params in the active gw resp 95 | %% default to empty list 96 | region_params = #blockchain_region_params_v1_pb{ 97 | region_params = [] 98 | } 99 | }}}, 100 | GwAcc1 = [GwResp | GwAcc], 101 | RespLen = length(GwAcc1), 102 | case RespLen >= BatchSize of 103 | true -> 104 | Resp = #follower_gateway_stream_resp_v1_pb{ gateways = GwAcc1 }, 105 | StreamAcc1 = grpcbox_stream:send(false, Resp, StreamAcc), 106 | {CountAcc + RespLen, [], StreamAcc1}; 107 | _ -> 108 | {CountAcc, GwAcc1, StreamAcc} 109 | end; 110 | _ -> {CountAcc, GwAcc, StreamAcc} 111 | end 112 | end, {0, [], StreamState}, Ledger), 113 | 114 | FinalGwLen = length(FinalGws), 115 | {FinalGwCount, StreamState3} = 116 | case FinalGwLen > 0 of 117 | true -> 118 | Resp = #follower_gateway_stream_resp_v1_pb{ gateways = FinalGws }, 119 | StreamState2 = grpcbox_stream:send(false, Resp, StreamState1), 120 | {NumGateways + FinalGwLen, StreamState2}; 121 | _ -> 122 | {NumGateways, StreamState1} 123 | end, 124 | 125 | StreamState4 = grpcbox_stream:update_trailers([{<<"num_gateways">>, integer_to_binary(FinalGwCount)}], StreamState3), 126 | {stop, StreamState4} 127 | end; 128 | active_gateways(#follower_gateway_stream_req_v1_pb{batch_size = BatchSize} = _Msg, _StreamState) -> 129 | lager:info("Requested batch size exceeds maximum allowed batch count: ~p", [BatchSize]), 130 | {grpc_error, {grpcbox_stream:code_to_status(3), <<"maximum batch size exceeded">>}}; 131 | active_gateways(_Msg, StreamState) -> 132 | lager:warning("unhandled grpc msg ~p", [_Msg]), 133 | {ok, StreamState}. 134 | 135 | -spec init(atom(), grpcbox_stream:t()) -> grpcbox_stream:t(). 136 | init(_RPC, StreamState) -> 137 | lager:debug("handler init, stream state ~p", [StreamState]), 138 | Chain = blockchain_worker:blockchain(), 139 | _NewStreamState = grpcbox_stream:stream_handler_state( 140 | StreamState, 141 | #{chain => Chain, streaming_initialized => false} 142 | ). 143 | 144 | handle_info({blockchain_event, {add_block, BlockHash, _Sync, _Ledger}}, StreamState) -> 145 | #{chain := Chain, txn_types := TxnTypes} = grpcbox_stream:stream_handler_state(StreamState), 146 | case blockchain:get_block(BlockHash, Chain) of 147 | {ok, Block} -> 148 | Height = blockchain_block:height(Block), 149 | Timestamp = blockchain_block:time(Block), 150 | SortedTxns = filter_hash_sort_txns(Block, TxnTypes), 151 | _NewStreamState = send_txn_sequence(SortedTxns, Height, Timestamp, StreamState); 152 | _ -> 153 | lager:error("failed to find block with hash: ~p", [BlockHash]), 154 | StreamState 155 | end; 156 | handle_info(_Msg, StreamState) -> 157 | lager:warning("unhandled info msg: ~p", [_Msg]), 158 | StreamState. 159 | 160 | %% ------------------------------------------------------------------- 161 | %% internal and callback breakdown functions 162 | %% ------------------------------------------------------------------- 163 | -spec txn_stream( 164 | blockchain:blockchain() | undefined, 165 | boolean(), 166 | follower_pb:follower_txn_stream_req_v1_pb(), 167 | grpcbox_stream:t() 168 | ) -> 169 | {ok, grpcbox_stream:t()} | grpcbox_stream:grpc_error_response(). 170 | txn_stream(undefined = _Chain, _StreamInitialized, _Msg, _StreamState) -> 171 | lager:debug("chain not ready, returning error response for msg ~p", [_Msg]), 172 | {grpc_error, {grpcbox_stream:code_to_status(14), <<"temporarily unavailable">>}}; 173 | txn_stream(_Chain, true = _StreamInitialized, _Msg, StreamState) -> 174 | {ok, StreamState}; 175 | txn_stream( 176 | Chain, 177 | false = _StreamInitialized, 178 | #follower_txn_stream_req_v1_pb{height = Height, txn_hash = Hash, txn_types = TxnTypes0} = _Msg, 179 | StreamState 180 | ) -> 181 | lager:debug("subscribing client to txn stream with msg ~p", [_Msg]), 182 | ok = blockchain_event:add_handler(self()), 183 | case validate_txn_filters(TxnTypes0) of 184 | {error, invalid_filters} -> 185 | {grpc_error, {grpcbox_stream:code_to_status(3), <<"invalid txn filter">>}}; 186 | {ok, TxnTypes} -> 187 | case process_past_blocks(Height, Hash, TxnTypes, Chain, StreamState) of 188 | {ok, StreamState1} -> 189 | HandlerState = grpcbox_stream:stream_handler_state(StreamState1), 190 | StreamState2 = grpcbox_stream:stream_handler_state( 191 | StreamState1, 192 | HandlerState#{streaming_initialized => true, txn_types => TxnTypes} 193 | ), 194 | {ok, StreamState2}; 195 | {error, invalid_req_params} -> 196 | {grpc_error, { 197 | grpcbox_stream:code_to_status(3), 198 | <<"invalid starting height, txn hash, or filter">> 199 | }}; 200 | {error, _} -> 201 | {grpc_error, { 202 | grpcbox_stream:code_to_status(5), <<"requested block not found">> 203 | }} 204 | end 205 | end. 206 | 207 | -spec find_gateway( 208 | blockchain:chain() | undefined, ctx:ctx(), follower_pb:follower_gateway_req_v1_pb() 209 | ) -> 210 | {ok, follower_pb:follower_gateway_resp_v1_pb(), ctx:ctx()} 211 | | grpcbox_stream:grpc_error_response(). 212 | find_gateway(undefined = _Chain, _Ctx, _Req) -> 213 | lager:debug("chain not ready, returning error response for msg ~p", [_Req]), 214 | {grpc_error, {grpcbox_stream:code_to_status(14), <<"temporarily unavailable">>}}; 215 | find_gateway(Chain, Ctx, Req) -> 216 | Ledger = blockchain:ledger(Chain), 217 | PubKeyBin = Req#follower_gateway_req_v1_pb.address, 218 | {ok, Height} = blockchain_ledger_v1:current_height(Ledger), 219 | case blockchain_ledger_v1:find_gateway_info(PubKeyBin, Ledger) of 220 | {ok, GwInfo} -> 221 | {Location, Region} = 222 | case blockchain_ledger_gateway_v2:location(GwInfo) of 223 | undefined -> {<<>>, undefined}; 224 | H3 -> 225 | case blockchain_region_v1:h3_to_region(H3, Ledger) of 226 | {ok, R} -> {h3:to_string(H3), normalize_region(R)}; 227 | _ -> {h3:to_string(H3), undefined} 228 | end 229 | end, 230 | RegionParams = 231 | case region_params_for_region(Region, Ledger) of 232 | {ok, Params} -> Params; 233 | {error, _} -> [] 234 | end, 235 | {ok, 236 | #follower_gateway_resp_v1_pb{ 237 | height = Height, 238 | result = {info, #gateway_info_pb{ 239 | address = PubKeyBin, 240 | location = Location, 241 | owner = blockchain_ledger_gateway_v2:owner_address(GwInfo), 242 | staking_mode = blockchain_ledger_gateway_v2:mode(GwInfo), 243 | gain = blockchain_ledger_gateway_v2:gain(GwInfo), 244 | region = Region, 245 | region_params = #blockchain_region_params_v1_pb{ 246 | region_params = RegionParams 247 | } 248 | }} 249 | }, 250 | Ctx}; 251 | _ -> 252 | ErrorResult = {error, #follower_error_pb{type = {not_found, #gateway_not_found_pb{address = PubKeyBin}}}}, 253 | {ok, #follower_gateway_resp_v1_pb{height = Height, result = ErrorResult}, Ctx} 254 | end. 255 | 256 | -spec subnetwork_last_reward_height( 257 | blockchain:chain() | undefined, 258 | ctx:ctx(), 259 | follower_pb:follower_subnetwork_last_reward_height_req_v1_pb() 260 | ) -> 261 | {ok, follower_pb:follower_subnetwork_last_reward_height_resp_v1_pb(), ctx:ctx()} 262 | | grpcbox_stream:grpc_error_response(). 263 | subnetwork_last_reward_height(undefined = _Chain, _Ctx, _Req) -> 264 | lager:debug("chain not ready, returning error response for msg ~p", [_Req]), 265 | {grpc_error, {grpcbox_stream:code_to_status(14), <<"temporarily unavailable">>}}; 266 | subnetwork_last_reward_height(Chain, Ctx, Req) -> 267 | Ledger = blockchain:ledger(Chain), 268 | TokenType = Req#follower_subnetwork_last_reward_height_req_v1_pb.token_type, 269 | {ok, CurrentHeight} = blockchain_ledger_v1:current_height(Ledger), 270 | case blockchain_ledger_v1:find_subnetwork_v1(TokenType, Ledger) of 271 | {ok, SubnetworkLedger} -> 272 | LastRewardHt = blockchain_ledger_subnetwork_v1:last_rewarded_block(SubnetworkLedger), 273 | {ok, #follower_subnetwork_last_reward_height_resp_v1_pb{height = CurrentHeight, reward_height = LastRewardHt}, Ctx}; 274 | _ -> {grpc_error, {grpcbox_stream:code_to_status(3), <<"unable to get retrieve subnetwork for requested token">>}} 275 | end. 276 | 277 | -spec process_past_blocks(Height :: pos_integer(), 278 | TxnHash :: binary(), 279 | TxnTypes :: [atom()], 280 | Chain :: blockchain:blockchain(), 281 | StreamState :: grpcbox_stream:t()) -> {ok, grpcbox_stream:t()} | {error, term()}. 282 | process_past_blocks(Height, TxnHash, TxnTypes, Chain, StreamState) when is_integer(Height) andalso Height > 0 -> 283 | {ok, #block_info_v2{height = HeadHeight}} = blockchain:head_block_info(Chain), 284 | case Height > HeadHeight of 285 | %% requested a future block; nothing to do but wait 286 | true -> {ok, StreamState}; 287 | false -> 288 | case blockchain:get_block(Height, Chain) of 289 | {ok, SubscribeBlock} -> 290 | process_past_blocks_(SubscribeBlock, TxnHash, TxnTypes, HeadHeight, Chain, StreamState); 291 | {error, not_found} -> 292 | case blockchain:find_first_block_after(Height, Chain) of 293 | {ok, _Height, ClosestBlock} -> 294 | process_past_blocks_(ClosestBlock, <<>>, TxnTypes, HeadHeight, Chain, StreamState); 295 | {error, _} = Error -> Error 296 | end 297 | end 298 | end; 299 | process_past_blocks(_Height, _TxnHash, _TxnTypes, _Chain, _StreamState) -> {error, invalid_req_params}. 300 | 301 | -spec process_past_blocks_(StartBlock :: blockchain_block:block(), 302 | TxnHash :: binary(), 303 | TxnTypes :: [atom()], 304 | HeadHeight :: pos_integer(), 305 | Chain :: blockchain:blockchain(), 306 | StreamState :: grpcbox_stream:t()) -> {ok, grpcbox_stream:t()}. 307 | process_past_blocks_(StartBlock, TxnHash, TxnTypes, HeadHeight, Chain, StreamState) -> 308 | StartHeight = blockchain_block:height(StartBlock), 309 | StartBlockTimestamp = blockchain_block:time(StartBlock), 310 | SortedStartTxns = filter_hash_sort_txns(StartBlock, TxnTypes), 311 | {UnhandledTxns, _} = lists:partition(fun({H, _T}) -> H > TxnHash end, SortedStartTxns), 312 | StreamState1 = send_txn_sequence(UnhandledTxns, StartHeight, StartBlockTimestamp, StreamState), 313 | BlockSeq = lists:seq(StartHeight + 1, HeadHeight), 314 | StreamState2 = lists:foldl(fun(HeightX, StateAcc) -> 315 | {ok, BlockX} = blockchain:get_block(HeightX, Chain), 316 | BlockXTimestamp = blockchain_block:time(BlockX), 317 | SortedTxnsX = filter_hash_sort_txns(BlockX, TxnTypes), 318 | _NewStateAcc = send_txn_sequence(SortedTxnsX, HeightX, BlockXTimestamp, StateAcc) 319 | end, StreamState1, BlockSeq), 320 | {ok, StreamState2}. 321 | 322 | -spec filter_hash_sort_txns(blockchain_block:block(), [atom()]) -> [{binary(), blockchain_txn:txn()}]. 323 | filter_hash_sort_txns(Block, TxnTypes) -> 324 | Txns = blockchain_block:transactions(Block), 325 | FilteredTxns = lists:filter(fun(Txn) -> subscribed_type(blockchain_txn:type(Txn), TxnTypes) end, Txns), 326 | HashKeyedTxns = lists:map(fun(Txn) -> {blockchain_txn:hash(Txn), Txn} end, FilteredTxns), 327 | lists:sort(fun({H1, _T1}, {H2, _T2}) -> H1 < H2 end, HashKeyedTxns). 328 | 329 | -spec send_txn_sequence(SortedTxns :: [{binary(), blockchain_txn:txn()}], 330 | Height :: pos_integer(), 331 | Timestamp :: pos_integer(), 332 | StreamState :: grpcbox_stream:t()) -> grpcbox_stream:t(). 333 | send_txn_sequence(SortedTxns, Height, Timestamp, StreamState) -> 334 | lists:foldl(fun({TxnHash, Txn}, StateAcc) -> 335 | Msg = encode_follower_resp(TxnHash, Txn, Height, Timestamp), 336 | grpcbox_stream:send(false, Msg, StateAcc) 337 | end, StreamState, SortedTxns). 338 | 339 | -spec encode_follower_resp(TxnHash :: binary(), 340 | Txn :: blockchain_txn:txn(), 341 | TxnHeight :: pos_integer(), 342 | Timestamp :: pos_integer()) -> follower_pb:follower_resp_v1_pb(). 343 | encode_follower_resp(TxnHash, Txn, TxnHeight, Timestamp) -> 344 | #follower_txn_stream_resp_v1_pb{ 345 | height = TxnHeight, 346 | txn_hash = TxnHash, 347 | txn = blockchain_txn:wrap_txn(Txn), 348 | timestamp = Timestamp 349 | }. 350 | 351 | subscribed_type(_Type, []) -> true; 352 | subscribed_type(Type, FilterTypes) -> lists:member(Type, FilterTypes). 353 | 354 | validate_txn_filters(TxnFilters0) -> 355 | case 356 | (catch lists:foldl( 357 | fun 358 | (BinType, AtomTypes) when is_binary(BinType) -> 359 | [binary_to_existing_atom(BinType, utf8) | AtomTypes]; 360 | (BinType, AtomTypes) when is_list(BinType) -> 361 | [list_to_existing_atom(BinType) | AtomTypes] 362 | end, 363 | [], 364 | TxnFilters0 365 | )) 366 | of 367 | {'EXIT', _} -> 368 | {error, invalid_filter}; 369 | TxnFilters when is_list(TxnFilters) -> 370 | case lists:all(fun is_blockchain_txn/1, TxnFilters) of 371 | true -> {ok, TxnFilters}; 372 | false -> {error, invalid_filters} 373 | end 374 | end. 375 | 376 | is_blockchain_txn(Module) -> 377 | ModInfo = Module:module_info(attributes), 378 | lists:any(fun({behavior, [blockchain_txn]}) -> true; (_) -> false end, ModInfo). 379 | 380 | %% blockchain_region_v1 returns region as an atom with a 'region_' prefix, ie 381 | %% 'region_us915' etc, we need it without the prefix and capitalised to 382 | %% be compatible with the proto 383 | normalize_region(V) -> 384 | list_to_atom(string:to_upper(string:slice(atom_to_list(V), 7))). 385 | 386 | -spec region_params_for_region(atom(), blockchain_ledger_v1:ledger()) -> 387 | {ok, [blockchain_region_param_v1:region_param_v1()]} | {error, no_params_for_region}. 388 | region_params_for_region(Region, Ledger) -> 389 | case blockchain_region_params_v1:for_region(Region, Ledger) of 390 | {error, Reason} -> 391 | lager:error( 392 | "Could not get params for region: ~p, reason: ~p", 393 | [Region, Reason] 394 | ), 395 | {error, no_params_for_region}; 396 | {ok, Params} -> 397 | {ok, Params} 398 | end. 399 | -------------------------------------------------------------------------------- /src/metrics/bn_metrics_exporter.erl: -------------------------------------------------------------------------------- 1 | -module(bn_metrics_exporter). 2 | 3 | -behaviour(elli_handler). 4 | 5 | -include_lib("elli/include/elli.hrl"). 6 | 7 | -export([handle/2, handle_event/3]). 8 | 9 | handle(Req, _Args) -> 10 | handle(Req#req.method, elli_request:path(Req), Req). 11 | 12 | %% Expose `/metrics` for Prometheus as a scrape target 13 | handle('GET', [<<"metrics">>], _Req) -> 14 | {ok, [], prometheus_text_format:format()}; 15 | handle(_Verb, _Path, _Req) -> 16 | ignore. 17 | 18 | handle_event(_Event, _Data, _Args) -> 19 | ok. 20 | -------------------------------------------------------------------------------- /src/metrics/bn_metrics_server.erl: -------------------------------------------------------------------------------- 1 | -module(bn_metrics_server). 2 | 3 | -behaviour(gen_server). 4 | 5 | -include("metrics.hrl"). 6 | 7 | -export([ 8 | handle_metric/4, 9 | start_link/0 10 | ]). 11 | 12 | -export([ 13 | init/1, 14 | handle_call/3, 15 | handle_cast/2, 16 | handle_info/2, 17 | terminate/2, 18 | code_change/3 19 | ]). 20 | 21 | -define(SERVER, ?MODULE). 22 | 23 | -type metric() :: { 24 | Metric :: string(), 25 | Event :: [atom()], 26 | PrometheusHandler :: module(), 27 | Labels :: [atom()], 28 | Description :: string() 29 | }. 30 | 31 | -type metrics() :: [metric()]. 32 | 33 | -type exporter_opts() :: [ 34 | {callback, module()} | 35 | {callback_args, map()} | 36 | {port, integer()} 37 | ]. 38 | 39 | -record(state, { 40 | metrics :: metrics(), 41 | exporter_opts :: exporter_opts(), 42 | exporter_pid :: pid() | undefined 43 | }). 44 | 45 | handle_metric(Event, Measurements, Metadata, _Config) -> 46 | handle_metric_event(Event, Measurements, Metadata). 47 | 48 | start_link() -> 49 | gen_server:start_link({local, ?SERVER}, ?SERVER, [], []). 50 | 51 | init(_Args) -> 52 | case get_configs() of 53 | {[], []} -> ignore; 54 | {[_ | _], [_ | _] = Metrics} = MetricConfigs -> 55 | erlang:process_flag(trap_exit, true), 56 | 57 | ok = setup_metrics(MetricConfigs), 58 | 59 | ElliOpts = [ 60 | {callback, bn_metrics_exporter}, 61 | {callback_args, #{}}, 62 | {port, application:get_env(blockchain_node, metrics_port, 9090)} 63 | ], 64 | {ok, ExporterPid} = elli:start_link(ElliOpts), 65 | {ok, #state{ 66 | metrics = Metrics, 67 | exporter_opts = ElliOpts, 68 | exporter_pid = ExporterPid}} 69 | end. 70 | 71 | handle_call(_Msg, _From, State) -> 72 | lager:debug("Received unknown call msg: ~p from ~p", [_Msg, _From]), 73 | {reply, ok, State}. 74 | 75 | handle_cast(_Msg, State) -> 76 | {noreply, State}. 77 | 78 | handle_info({'EXIT', ExporterPid, Reason}, #state{exporter_pid=ExporterPid} = State) -> 79 | lager:warning("Metrics exporter exited with reason ~p, restarting", [Reason]), 80 | {ok, NewExporter} = elli:start_link(State#state.exporter_opts), 81 | {noreply, State#state{exporter_pid = NewExporter}}; 82 | handle_info(_Msg, State) -> 83 | lager:debug("Received unknown info msg: ~p", [_Msg]), 84 | {noreply, State}. 85 | 86 | code_change(_OldVsn, State, _Extra) -> 87 | {ok, State}. 88 | 89 | terminate(Reason, #state{metrics = Metrics, exporter_pid = Exporter}) -> 90 | true = erlang:exit(Exporter, Reason), 91 | lists:foreach( 92 | fun({Metric, Module, _, _}) -> 93 | lager:info("De-registering metric ~p as ~p", [Metric, Module]), 94 | Module:deregister(Metric) 95 | end, 96 | Metrics 97 | ). 98 | 99 | setup_metrics({EventNames, EventSpecs}) -> 100 | lager:info("METRICS ~p", [EventSpecs]), 101 | lists:foreach( 102 | fun({Metric, Module, Meta, Description}) -> 103 | lager:info("Declaring metric ~p as ~p meta=~p", [Metric, Module, Meta]), 104 | MetricOpts = [{name, Metric}, {help, Description}, {labels, Meta}], 105 | case Module of 106 | prometheus_histogram -> 107 | Module:declare(MetricOpts ++ [{buckets, ?METRICS_HISTOGRAM_BUCKETS}]); 108 | _ -> 109 | Module:declare(MetricOpts) 110 | end 111 | end, 112 | EventSpecs 113 | ), 114 | 115 | ok = telemetry:attach_many(<<"bn-metrics-handler">>, EventNames, fun bn_metrics_server:handle_metric/4, []). 116 | 117 | get_configs() -> 118 | lists:foldl( 119 | fun(Metric, {Names, Specs} = Acc) -> 120 | case maps:get(Metric, ?METRICS, undefined) of 121 | undefined -> Acc; 122 | {N, S} -> {Names ++ N, Specs ++ S} 123 | end 124 | end, 125 | {[], []}, 126 | application:get_env(blockchain_node, metrics, []) 127 | ). 128 | 129 | handle_metric_event([blockchain, block, absorb], #{duration := Duration}, #{stage := Stage}) -> 130 | prometheus_histogram:observe(?METRICS_BLOCK_ABSORB, [Stage], Duration), 131 | ok; 132 | handle_metric_event([blockchain, block, height], #{height := Height}, #{time := Time}) -> 133 | prometheus_gauge:set(?METRICS_BLOCK_HEIGHT, [Time], Height), 134 | ok; 135 | handle_metric_event([blockchain, block, unvalidated_absorb], #{duration := Duration}, #{stage := Stage}) -> 136 | prometheus_histogram:observe(?METRICS_BLOCK_UNVAL_ABSORB, [Stage], Duration), 137 | ok; 138 | handle_metric_event([blockchain, block, unvalidated_height], #{height := Height}, #{time := Time}) -> 139 | prometheus_gauge:set(?METRICS_BLOCK_UNVAL_HEIGHT, [Time], Height), 140 | ok; 141 | handle_metric_event([blockchain, txn, absorb], #{duration := Duration}, #{type := Type}) -> 142 | prometheus_histogram:observe(?METRICS_TXN_ABSORB_DURATION, [Type], Duration), 143 | ok; 144 | handle_metric_event([blockchain, txn_mgr, submit], _Measurements, #{type := Type}) -> 145 | prometheus_counter:inc(?METRICS_TXN_SUBMIT_COUNT, [Type]), 146 | ok; 147 | handle_metric_event([blockchain, txn_mgr, reject], #{block_span := Span}, #{type := Type}) -> 148 | prometheus_counter:inc(?METRICS_TXN_REJECT_COUNT, [Type]), 149 | prometheus_gauge:set(?METRICS_TXN_BLOCK_SPAN, [], Span), 150 | ok; 151 | handle_metric_event([blockchain, txn_mgr, accept], #{block_span := Span, queue_len := QLen}, #{type := Type}) -> 152 | prometheus_counter:inc(?METRICS_TXN_ACCEPT_COUNT, [Type]), 153 | prometheus_gauge:set(?METRICS_TXN_BLOCK_SPAN, [], Span), 154 | prometheus_gauge:set(?METRICS_TXN_QUEUE, [], QLen), 155 | ok; 156 | handle_metric_event([blockchain, txn_mgr, update], #{block_span := Span, queue_len := QLen}, #{type := Type}) -> 157 | prometheus_counter:inc(?METRICS_TXN_UPDATE_COUNT, [Type]), 158 | prometheus_gauge:set(?METRICS_TXN_BLOCK_SPAN, [], Span), 159 | prometheus_gauge:set(?METRICS_TXN_QUEUE, [], QLen), 160 | ok; 161 | handle_metric_event([blockchain, txn_mgr, process], #{duration := Duration}, #{stage := Stage}) -> 162 | prometheus_histogram:observe(?METRICS_TXN_PROCESS_DURATION, [Stage], Duration), 163 | ok; 164 | handle_metric_event([blockchain, txn_mgr, add_block], #{cache := Cache, block_time := BlockTime, block_age := BlockAge}, #{height := Height}) -> 165 | prometheus_gauge:set(?METRICS_TXN_CACHE_SIZE, [Height], Cache), 166 | prometheus_gauge:set(?METRICS_TXN_BLOCK_TIME, [Height], BlockTime), 167 | prometheus_gauge:set(?METRICS_TXN_BLOCK_AGE, [Height], BlockAge), 168 | ok; 169 | handle_metric_event([grpcbox, server, rpc_end], #{server_latency := Latency}, #{grpc_server_method := Method, grpc_server_status := Status}) -> 170 | prometheus_gauge:dec(?METRICS_GRPC_SESSIONS, [Method]), 171 | prometheus_histogram:observe(?METRICS_GRPC_LATENCY, [Method, Status], Latency), 172 | ok; 173 | handle_metric_event([grpcbox, server, rpc_begin], _Measurements, #{grpc_server_method := Method}) -> 174 | prometheus_gauge:inc(?METRICS_GRPC_SESSIONS, [Method]), 175 | ok. 176 | -------------------------------------------------------------------------------- /src/metrics/metrics.hrl: -------------------------------------------------------------------------------- 1 | -define(METRICS_HISTOGRAM_BUCKETS, [50, 100, 250, 500, 1000, 2000, 5000, 10000, 30000, 60000]). 2 | 3 | -define(METRICS_BLOCK_ABSORB, "blockchain_block_absorb_duration"). 4 | -define(METRICS_BLOCK_UNVAL_ABSORB, "blockchain_block_unval_absorb_duration"). 5 | -define(METRICS_BLOCK_HEIGHT, "blockchain_block_height"). 6 | -define(METRICS_BLOCK_UNVAL_HEIGHT, "blockchain_block_unval_height"). 7 | -define(METRICS_TXN_ABSORB_DURATION, "blockchain_txn_absorb_duration"). 8 | -define(METRICS_TXN_BLOCK_SPAN, "blockchain_txn_mgr_block_span"). 9 | -define(METRICS_TXN_QUEUE, "blockchain_txn_mgr_queue"). 10 | -define(METRICS_TXN_SUBMIT_COUNT, "blockchain_txn_mgr_submitted_count"). 11 | -define(METRICS_TXN_REJECT_COUNT, "blockchain_txn_mgr_rejected_count"). 12 | -define(METRICS_TXN_ACCEPT_COUNT, "blockchain_txn_mgr_accepted_count"). 13 | -define(METRICS_TXN_UPDATE_COUNT, "blockchain_txn_mgr_updated_count"). 14 | -define(METRICS_TXN_PROCESS_DURATION, "blockchain_txn_mgr_process_duration"). 15 | -define(METRICS_TXN_CACHE_SIZE, "blockchain_txn_mgr_cache_size"). 16 | -define(METRICS_TXN_BLOCK_TIME, "blockchain_txn_mgr_block_time"). 17 | -define(METRICS_TXN_BLOCK_AGE, "blockchain_txn_mgr_block_age"). 18 | -define(METRICS_GRPC_SESSIONS, "grpc_session_count"). 19 | -define(METRICS_GRPC_LATENCY, "grpcbox_session_latency"). 20 | 21 | -define(METRICS, #{ 22 | block_metrics => { 23 | [ [blockchain, block, absorb], 24 | [blockchain, block, height], 25 | [blockchain, block, unvalidated_absorb], 26 | [blockchain, block, unvalidated_height] ], 27 | [ {?METRICS_BLOCK_ABSORB, prometheus_histogram, [stage], "Block absorb duration"}, 28 | {?METRICS_BLOCK_HEIGHT, prometheus_gauge, [time], "Most recent block height"}, 29 | {?METRICS_BLOCK_UNVAL_ABSORB, prometheus_histogram, [stage], "Block unvalidated absorb duration"}, 30 | {?METRICS_BLOCK_UNVAL_HEIGHT, prometheus_gauge, [time], "Most recent unvalidated block height"} ] 31 | }, 32 | txn_metrics => { 33 | [ [blockchain, txn, absorb], 34 | [blockchain, txn_mgr, submit], 35 | [blockchain, txn_mgr, reject], 36 | [blockchain, txn_mgr, accept], 37 | [blockchain, txn_mgr, update], 38 | [blockchain, txn_mgr, process], 39 | [blockchain, txn_mgr, add_block] ], 40 | [ {?METRICS_TXN_ABSORB_DURATION, prometheus_histogram, [stage], "Txn absorb duration"}, 41 | {?METRICS_TXN_BLOCK_SPAN, prometheus_gauge, [], "Block span on transactions"}, 42 | {?METRICS_TXN_QUEUE, prometheus_gauge, [], "Txn manager submission queue length"}, 43 | {?METRICS_TXN_SUBMIT_COUNT, prometheus_counter, [type], "Count of submitted transactions"}, 44 | {?METRICS_TXN_REJECT_COUNT, prometheus_counter, [type], "Count of rejected transactions"}, 45 | {?METRICS_TXN_ACCEPT_COUNT, prometheus_counter, [type], "Count of accepted transactions"}, 46 | {?METRICS_TXN_UPDATE_COUNT, prometheus_counter, [type], "Count of updated transaction"}, 47 | {?METRICS_TXN_PROCESS_DURATION, prometheus_histogram, [stage], "Transaction manager cache process duration"}, 48 | {?METRICS_TXN_CACHE_SIZE, prometheus_gauge, [height], "Transaction manager buffer size"}, 49 | {?METRICS_TXN_BLOCK_TIME, prometheus_gauge, [height], "Block time observed from the transaction manager"}, 50 | {?METRICS_TXN_BLOCK_AGE, prometheus_gauge, [height], "Block age observed from the transaction manager"} ] 51 | }, 52 | grpc_metrics => { 53 | [ [grpcbox, server, rpc_begin], 54 | [grpcbox, server, rpc_end] ], 55 | [ {?METRICS_GRPC_SESSIONS, prometheus_gauge, [method], "GRPC session count"}, 56 | {?METRICS_GRPC_LATENCY, prometheus_histogram, [method, status], "GRPC session latency"} ] 57 | } 58 | }). 59 | -------------------------------------------------------------------------------- /src/wallet.erl: -------------------------------------------------------------------------------- 1 | -module(wallet). 2 | 3 | -define(IV_LENGTH, 12). 4 | -define(TAG_LENGTH, 16). 5 | -define(PASSWORD_ITERATIONS, 100000). 6 | 7 | -define(BASIC_KEY_V1, 16#0001). 8 | -define(BASIC_KEY_V2, 16#0002). 9 | -define(PWHASH_BKDF2, 16#00). 10 | -define(PWHASH_ARGON2ID13, 16#01). 11 | 12 | -define(PBKDF2_SALT_LENGTH, 8). 13 | -record(pwhash_pbkdf2, 14 | { 15 | salt=undefined :: undefined | <<_:(8 * ?PBKDF2_SALT_LENGTH)>>, 16 | iterations :: pos_integer() 17 | }). 18 | 19 | 20 | 21 | -define(ARGON2ID13_SALT_LENGTH, 16). 22 | -record(pwhash_argon2id13, 23 | { 24 | salt=undefined :: undefined | <<_:(8 * ?ARGON2ID13_SALT_LENGTH)>>, 25 | ops_limit :: pwhash_limit(), 26 | mem_limit :: pwhash_limit() 27 | }). 28 | 29 | -type pwhash() :: #pwhash_pbkdf2{} | #pwhash_argon2id13{}. 30 | -type pwhash_limit() :: interactive | moderate | sensitive | pos_integer(). 31 | -type iv() :: <<_:(8 * ?IV_LENGTH)>>. 32 | -type tag() :: <<_:(8 * ?TAG_LENGTH)>>. 33 | 34 | -record(wallet, 35 | { 36 | pwhash :: pwhash(), 37 | pubkey_bin :: libp2p_crypto:pubkey_bin(), 38 | iv :: iv(), 39 | tag :: tag(), 40 | encrypted :: binary() 41 | }). 42 | 43 | -type wallet() :: #wallet{}. 44 | 45 | -export_type([wallet/0]). 46 | 47 | -export([encrypt/2, encrypt/3, decrypt/2, pubkey_bin/1]). 48 | -export([from_binary/1, to_binary/1, to_binary/2]). 49 | 50 | -spec encrypt(KeyMap::libp2p_crypto:key_map(), Password::binary()) -> {ok, wallet()} | {error, term()}. 51 | encrypt(KeyMap, Password) -> 52 | encrypt(KeyMap, Password, 53 | #pwhash_argon2id13{ops_limit=ops_limit({argon2id13, sensitive}), 54 | mem_limit=mem_limit({argon2id13, sensitive})}). 55 | 56 | -spec encrypt(KeyMap::libp2p_crypto:key_map(), Password::binary(), PWHash::pwhash()) 57 | -> {ok, wallet()} | {error, term()}. 58 | encrypt(KeyMap, Password, PWHash) -> 59 | IV = crypto:strong_rand_bytes(?IV_LENGTH), 60 | {ok, EncryptionKey, NewPWHash} = pwhash(Password, PWHash), 61 | {PubKeyBin, EncryptBin, Tag} = encrypt_keymap(EncryptionKey, IV, ?TAG_LENGTH, KeyMap), 62 | {ok, #wallet{ 63 | pubkey_bin=PubKeyBin, 64 | pwhash=NewPWHash, 65 | iv=IV, 66 | tag=Tag, 67 | encrypted=EncryptBin 68 | }}. 69 | 70 | -spec pubkey_bin(wallet()) -> lib2p_crypto:pubkey_bin(). 71 | pubkey_bin(#wallet{pubkey_bin=PubKeyBin}) -> 72 | PubKeyBin. 73 | 74 | -spec decrypt(Password::binary(), wallet()) -> {ok, libp2p_crypto:key_map()} | {error, term()}. 75 | decrypt(Password, #wallet{ 76 | pubkey_bin=PubKeyBin, 77 | iv=IV, 78 | tag=Tag, 79 | pwhash=PWHash, 80 | encrypted=Encrypted 81 | }) -> 82 | {ok, AESKey, PWHash} = pwhash(Password, PWHash), 83 | decrypt_keymap(AESKey, IV, Tag, PubKeyBin, Encrypted). 84 | 85 | 86 | -spec from_binary(binary()) -> {ok, wallet()} | {error, term()}. 87 | from_binary(<<(?BASIC_KEY_V1):16/integer-unsigned-little, 88 | PubKeyBin:33/binary, 89 | IV:(?IV_LENGTH)/binary, 90 | Salt:(?PBKDF2_SALT_LENGTH)/binary, 91 | Iterations:32/integer-unsigned-little, 92 | Tag:(?TAG_LENGTH)/binary, 93 | Encrypted/binary>>) -> 94 | {ok, #wallet{ 95 | pubkey_bin=PubKeyBin, 96 | iv = IV, 97 | pwhash = #pwhash_pbkdf2{salt=Salt, iterations=Iterations}, 98 | tag = Tag, 99 | encrypted = Encrypted 100 | }}; 101 | from_binary(<<(?BASIC_KEY_V2):16/integer-unsigned-little, 102 | (?PWHASH_BKDF2):8/integer-unsigned, 103 | PubKeyBin:33/binary, 104 | IV:(?IV_LENGTH)/binary, 105 | Salt:(?PBKDF2_SALT_LENGTH)/binary, 106 | Iterations:32/integer-unsigned-little, 107 | Tag:(?TAG_LENGTH)/binary, 108 | Encrypted/binary>>) -> 109 | {ok, #wallet{ 110 | pubkey_bin=PubKeyBin, 111 | iv = IV, 112 | pwhash = #pwhash_pbkdf2{salt=Salt, iterations=Iterations}, 113 | tag = Tag, 114 | encrypted = Encrypted 115 | }}; 116 | from_binary(<<(?BASIC_KEY_V2):16/integer-unsigned-little, 117 | (?PWHASH_ARGON2ID13):8/integer-unsigned, 118 | PubKeyBin:33/binary, 119 | IV:(?IV_LENGTH)/binary, 120 | Salt:(?ARGON2ID13_SALT_LENGTH)/binary, 121 | MemLimit:32/integer-unsigned-little, 122 | OpsLimit:32/integer-unsigned-little, 123 | Tag:(?TAG_LENGTH)/binary, 124 | Encrypted/binary>>) -> 125 | {ok, #wallet{ 126 | pubkey_bin=PubKeyBin, 127 | iv = IV, 128 | pwhash = #pwhash_argon2id13{salt=Salt, ops_limit=OpsLimit, mem_limit=MemLimit}, 129 | tag = Tag, 130 | encrypted = Encrypted 131 | }}; 132 | from_binary(_) -> 133 | {error, invalid_wallet}. 134 | 135 | 136 | -spec to_binary(wallet()) -> binary(). 137 | to_binary(Wallet) -> 138 | to_binary(?BASIC_KEY_V2, Wallet). 139 | 140 | -spec to_binary(Version::pos_integer(), wallet() | pwhash()) -> binary(). 141 | to_binary(?BASIC_KEY_V1, 142 | #wallet{ pubkey_bin=PubKeyBin, 143 | iv=IV, 144 | pwhash=#pwhash_pbkdf2 { salt=Salt, iterations=Iterations }, 145 | tag=Tag, 146 | encrypted=Encrypted}) -> 147 | <>; 154 | to_binary(?BASIC_KEY_V1, _) -> 155 | error(invalid_v1_wallet); 156 | to_binary(?BASIC_KEY_V2, 157 | #wallet{ pubkey_bin=PubKeyBin, 158 | iv=IV, 159 | pwhash=PWHash, 160 | tag=Tag, 161 | encrypted=Encrypted}) -> 162 | <>; 169 | to_binary(_, #pwhash_argon2id13{salt=Salt, mem_limit=MemLimit, ops_limit=OpsLimit}) -> 170 | <>; 171 | to_binary(_, #pwhash_pbkdf2{salt=Salt, iterations=Iterations}) -> 172 | <>. 173 | 174 | pwhash_kind(#pwhash_pbkdf2{}) -> 175 | ?PWHASH_BKDF2; 176 | pwhash_kind(#pwhash_argon2id13{}) -> 177 | ?PWHASH_ARGON2ID13. 178 | 179 | -spec encrypt_keymap(Key::binary(), IV::binary(), TagLength::pos_integer(), KeyMap::libp2p_crypto:key_map()) 180 | -> {PubKeyBin::binary(), Encrypted::binary(), Tag::binary()}. 181 | encrypt_keymap(Key, IV, TagLength, KeyMap=#{public := PubKey}) -> 182 | KeysBin = libp2p_crypto:keys_to_bin(KeyMap), 183 | PubKeyBin = libp2p_crypto:pubkey_to_bin(PubKey), 184 | {Encrypted, Tag} = crypto:crypto_one_time_aead(aes_256_gcm, Key, IV, KeysBin, PubKeyBin, TagLength, true), 185 | {PubKeyBin, Encrypted, Tag}. 186 | 187 | -spec decrypt_keymap(Key::binary(), IV::binary(), Tag::binary(), PubKeyBin::libp2p_crypto:pubkey_bin(), 188 | Encryted::binary()) -> {ok, libp2p_crypto:key_map()} | {error, term()}. 189 | decrypt_keymap(Key, IV, Tag, PubKeyBin, Encrypted) -> 190 | case crypto:crypto_one_time_aead(aes_256_gcm, Key, IV, Encrypted, PubKeyBin, Tag, false) of 191 | error -> 192 | {error, decrypt}; 193 | Bin -> 194 | {ok, libp2p_crypto:keys_from_bin(Bin)} 195 | end. 196 | 197 | pwhash(Password, PWHash=#pwhash_pbkdf2{ iterations=Iterations}) -> 198 | Salt = case PWHash#pwhash_pbkdf2.salt of 199 | undefined -> crypto:strong_rand_bytes(?PBKDF2_SALT_LENGTH); 200 | V -> V 201 | end, 202 | {ok, AESKey} = pbkdf2:pbkdf2(sha256, Password, Salt, Iterations), 203 | {ok, AESKey, PWHash#pwhash_pbkdf2{salt=Salt}}; 204 | pwhash(Password, PWHash=#pwhash_argon2id13{}) -> 205 | Salt = case PWHash#pwhash_argon2id13.salt of 206 | undefined -> crypto:strong_rand_bytes(?ARGON2ID13_SALT_LENGTH); 207 | V -> V 208 | end, 209 | AESKey = enacl:pwhash(Password, Salt, PWHash#pwhash_argon2id13.ops_limit, PWHash#pwhash_argon2id13.mem_limit), 210 | {ok, AESKey, PWHash#pwhash_argon2id13{salt=Salt}}. 211 | 212 | 213 | -dialyzer({nowarn_function, ops_limit/1}). % Not all values are used right now 214 | %% These values come from the libsodium build for constants that are 215 | %% named the same. 216 | -spec ops_limit({argon2id13, pwhash_limit()}) -> pos_integer(). 217 | ops_limit({argon2id13, interactive}) -> 218 | 2; 219 | ops_limit({argon2id13, moderate}) -> 220 | 3; 221 | ops_limit({argon2id13, sensitive}) -> 222 | 4; 223 | ops_limit({argon2id13, Num}) when is_integer(Num) -> 224 | Num. 225 | 226 | -dialyzer({nowarn_function, mem_limit/1}). % Not all values are used right now 227 | %% These values come from the libsodium build for constants that are 228 | %% named the same. 229 | -spec mem_limit({argon2id13, pwhash_limit()}) -> pos_integer(). 230 | mem_limit({argon2id13, interactive}) -> 231 | 67108864; 232 | mem_limit({argon2id13, moderate}) -> 233 | 268435456; 234 | mem_limit({argon2id13, sensitive}) -> 235 | 1073741824; 236 | mem_limit({argon2id13, Num}) when is_integer(Num) -> 237 | Num. 238 | 239 | 240 | -ifdef(TEST). 241 | -include_lib("eunit/include/eunit.hrl"). 242 | 243 | -define(TEST_PASSWORD, <<"password">>). 244 | 245 | mk_wallet({argon2id13, _}=PWHash) -> 246 | KeyMap = libp2p_crypto:generate_keys(ed25519), 247 | {ok, Wallet} = encrypt(KeyMap, ?TEST_PASSWORD, 248 | #pwhash_argon2id13{salt=undefined, 249 | ops_limit=ops_limit(PWHash), 250 | mem_limit=mem_limit(PWHash)}), 251 | {ok, KeyMap, Wallet}; 252 | mk_wallet(argon2id13) -> 253 | KeyMap = libp2p_crypto:generate_keys(ed25519), 254 | {ok, Wallet} = encrypt(KeyMap, ?TEST_PASSWORD), 255 | {ok, KeyMap, Wallet}; 256 | mk_wallet(pbkdf2) -> 257 | KeyMap = libp2p_crypto:generate_keys(ed25519), 258 | {ok, Wallet} = encrypt(KeyMap, ?TEST_PASSWORD, #pwhash_pbkdf2{iterations=1000}), 259 | {ok, KeyMap, Wallet}. 260 | 261 | 262 | roundtrip_default_test() -> 263 | {ok, KeyMap, Wallet} = mk_wallet(argon2id13), 264 | {ok, Decrypted} = decrypt(?TEST_PASSWORD, Wallet), 265 | ?assertEqual(KeyMap, Decrypted). 266 | 267 | roundtrip_argon2id13_test() -> 268 | lists:foreach(fun(Level) -> 269 | {ok, KeyMap, Wallet} = mk_wallet({argon2id13, Level}), 270 | {ok, Decrypted} = decrypt(?TEST_PASSWORD, Wallet), 271 | ?assertEqual(KeyMap, Decrypted) 272 | end, [interactive, moderate]). 273 | 274 | roundtrip_pbkdf2_test() -> 275 | {ok, KeyMap, Wallet} = mk_wallet(pbkdf2), 276 | {ok, Decrypted} = decrypt(?TEST_PASSWORD, Wallet), 277 | ?assertEqual(KeyMap, Decrypted). 278 | 279 | roundtrip_binary_test() -> 280 | lists:foreach(fun(PWHash) -> 281 | {ok, _, Wallet} = mk_wallet(PWHash), 282 | WalletBin = wallet:to_binary(Wallet), 283 | {ok, Decoded} = wallet:from_binary(WalletBin), 284 | ?assertEqual(Wallet, Decoded) 285 | end, [{argon2id13, interactive}, pbkdf2]). 286 | 287 | roundtrip_binary_v1_test() -> 288 | {ok, _, Wallet} = mk_wallet(pbkdf2), 289 | WalletBin = to_binary(?BASIC_KEY_V1, Wallet), 290 | {ok, Decoded} = wallet:from_binary(WalletBin), 291 | ?assertEqual(Wallet, Decoded), 292 | 293 | {ok, _, Wallet2} = mk_wallet({argon2id13, interactive}), 294 | ?assertError(invalid_v1_wallet, to_binary(?BASIC_KEY_V1, Wallet2)). 295 | 296 | invalid_binary_test() -> 297 | ?assertEqual({error, invalid_wallet}, 298 | wallet:from_binary(<<"invalid wallet binary">>)). 299 | 300 | roundtrip_pbkdf2_binary_test() -> 301 | {ok, _, Wallet} = mk_wallet(pbkdf2), 302 | WalletBin = wallet:to_binary(Wallet), 303 | {ok, Decoded} = wallet:from_binary(WalletBin), 304 | ?assertEqual(Wallet, Decoded). 305 | 306 | invalid_password_test() -> 307 | {ok, _, Wallet} = mk_wallet({argon2id13,interactive}), 308 | ?assertEqual({error, decrypt}, wallet:decrypt(<<"invalid_password">>, Wallet)). 309 | 310 | pubkey_bin_test() -> 311 | {ok, KeyMap, Wallet} = mk_wallet({argon2id13, interactive}), 312 | #{ public := PubKey } = KeyMap, 313 | ?assertEqual(libp2p_crypto:pubkey_to_bin(PubKey), 314 | wallet:pubkey_bin(Wallet)). 315 | 316 | -endif. 317 | --------------------------------------------------------------------------------