├── .gitattributes ├── launcher ├── exonum_btc_anchoring_plugin │ ├── __init__.py │ └── plugin.py └── setup.py ├── .cspell.json ├── .markdownlintrc ├── .gitignore ├── .editorconfig ├── package.json ├── cspell.sh ├── examples ├── btc_anchoring.rs ├── btc_payload_extractor.rs └── btc_anchoring_sync.rs ├── src ├── proto │ ├── internal.proto │ ├── btc_types.proto │ ├── service.proto │ ├── binary_map.rs │ └── mod.rs ├── blockchain │ ├── errors.rs │ ├── data_layout │ │ └── mod.rs │ ├── mod.rs │ ├── transactions.rs │ └── schema.rs ├── lib.rs ├── service.rs ├── sync │ ├── bitcoin_relay.rs │ └── mod.rs ├── btc │ ├── macros.rs │ ├── mod.rs │ └── payload.rs ├── config.rs └── test_helpers │ └── mod.rs ├── README.md ├── Cargo.toml ├── .travis.yml ├── guides ├── maintenance.md └── newbie.md ├── exonum-dictionary.txt ├── CHANGELOG.md ├── LICENSE └── tests ├── sync.rs └── api.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | CHANGELOG.md merge=union 2 | -------------------------------------------------------------------------------- /launcher/exonum_btc_anchoring_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import AnchoringInstanceSpecLoader 2 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1", 3 | "language": "en", 4 | "dictionaries": ["en_US", "softwareTerms", "exonum"], 5 | "dictionaryDefinitions": [ 6 | { "name": "exonum", "path": "./exonum-dictionary.txt" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "no-duplicate-header": false, 4 | "first-header-h1": false, 5 | "first-line-h1": false, 6 | "MD013": { 7 | "line_length": 100, 8 | "code_blocks": false, 9 | "tables": false, 10 | "headers": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vim files 2 | *.swp 3 | # macos files 4 | *.DS_Store 5 | 6 | # cargo 7 | target 8 | */target 9 | rls 10 | */rls 11 | *.rs.bk 12 | Cargo.lock 13 | sandbox_tests/Cargo.lock 14 | 15 | # IDE files 16 | .idea 17 | .vscode 18 | *.iml 19 | 20 | # Python files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | .mypy_cache 25 | launcher/exonum_btc_anchoring_plugin.egg-info 26 | 27 | # other 28 | *.orig 29 | .python-version 30 | 31 | # node 32 | node_modules 33 | npm-debug.log 34 | -------------------------------------------------------------------------------- /launcher/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from distutils.core import setup 3 | 4 | install_requires = ["exonum-launcher==0.2.0"] 5 | 6 | python_requires = ">=3.6" 7 | 8 | setup( 9 | name="exonum_btc_anchoring_plugin", 10 | version="0.1", 11 | description="Exonum BTC anchoring plugin", 12 | url="https://github.com/exonum/exonum-btc-anchoring", 13 | packages=["exonum_btc_anchoring_plugin"], 14 | install_requires=install_requires, 15 | python_requires=python_requires, 16 | ) 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exonum", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "JS dependencies for Exonum", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "cspell": "^4.0.63", 9 | "markdownlint-cli": "^0.17.0" 10 | }, 11 | "scripts": { 12 | "cspell": "./cspell.sh", 13 | "md": "find . -not -path \"*/node_modules/*\" -and -not -path \"*/target/*\" -and -not -path \"*/.git/*\" -name \"*.md\" | xargs ./node_modules/.bin/markdownlint --config .markdownlintrc" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/exonum/exonum-btc-anchoring" 18 | }, 19 | "author": "Exonum Team ", 20 | "license": "Apache-2.0", 21 | "bugs": { 22 | "url": "https://github.com/exonum/exonum-btc-anchoring/issues" 23 | }, 24 | "homepage": "https://github.com/exonum/exonum-btc-anchoring#readme" 25 | } 26 | -------------------------------------------------------------------------------- /cspell.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script running cspell checks on all the specified project directories. 4 | 5 | # Copyright 2019 The Exonum Team 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | set -e 20 | 21 | find . -not -path "*/target/*" -and -not -path "*/.git/*" -and -not -path "*/node_modules/*" \( -name "*.rs" -or -name "*.md" -or -name "*.proto" \) | xargs ./node_modules/.bin/cspell 22 | -------------------------------------------------------------------------------- /examples/btc_anchoring.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use exonum_cli::{NodeBuilder, Spec}; 16 | 17 | #[tokio::main] 18 | async fn main() -> anyhow::Result<()> { 19 | exonum::helpers::init_logger()?; 20 | NodeBuilder::new() 21 | .with(Spec::new(exonum_btc_anchoring::BtcAnchoringService)) 22 | .run() 23 | .await 24 | } 25 | -------------------------------------------------------------------------------- /src/proto/internal.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Internal data types of Bitcoin anchoring service. 16 | 17 | syntax = "proto3"; 18 | 19 | package exonum.service.btc_anchoring.schema; 20 | 21 | import "btc_types.proto"; 22 | 23 | // Some non-scalar key-value pair. 24 | message KeyValue { 25 | bytes key = 1; 26 | bytes value = 2; 27 | } 28 | 29 | message BinaryMap { 30 | repeated KeyValue inner = 1; 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Exonum Anchoring Service to Bitcoin 2 | 3 | [![Build Status][travis:image]][travis:url] 4 | 5 | This crate implements a service for [Exonum] blockchain that provides 6 | a protocol for anchoring onto the Bitcoin blockchain that utilizes the 7 | native Bitcoin capabilities of creating multisig transactions. 8 | 9 | * [Reference documentation][anchoring:reference] 10 | * [Specification][anchoring:specification] 11 | * [Example code](examples/btc_anchoring.rs) 12 | * [Newbie guide](guides/newbie.md) 13 | * [Maintenance guide](guides/maintenance.md) 14 | * [Contribution guide][exonum:contribution] 15 | 16 | ## Licence 17 | 18 | Exonum core library is licensed under the Apache License (Version 2.0). 19 | See [LICENSE](LICENSE) for details. 20 | 21 | [anchoring:reference]: https://docs.rs/exonum-btc-anchoring 22 | [anchoring:specification]: https://exonum.com/doc/version/latest/advanced/bitcoin-anchoring/ 23 | [exonum:contribution]: https://exonum.com/doc/contributing/ 24 | [exonum:install]: https://exonum.com/doc/get-started/install/ 25 | [Exonum]: https://github.com/exonum/exonum 26 | [travis:image]: https://travis-ci.org/exonum/exonum-btc-anchoring.svg?branch=master 27 | [travis:url]: https://travis-ci.org/exonum/exonum-btc-anchoring 28 | -------------------------------------------------------------------------------- /src/proto/btc_types.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Protobuf wrappers for the common Bitcoin types. 16 | 17 | syntax = "proto3"; 18 | 19 | package exonum.btc; 20 | 21 | // Bitcoin public key wrapper. 22 | message PublicKey { 23 | // Inner Data. 24 | bytes data = 1; 25 | } 26 | 27 | // Bitcoin transaction wrapper. 28 | message Transaction { 29 | // Inner data. 30 | bytes data = 1; 31 | } 32 | 33 | // Bitcoin transaction input signature wrapper. 34 | message InputSignature { 35 | // Inner data. 36 | bytes data = 1; 37 | } 38 | 39 | // Bitcoin SHA256d hash. 40 | message Sha256d { 41 | // Inner data. 42 | bytes data = 1; 43 | } 44 | -------------------------------------------------------------------------------- /examples/btc_payload_extractor.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use anyhow::anyhow; 16 | use hex::FromHex; 17 | use structopt::StructOpt; 18 | 19 | use exonum_btc_anchoring::btc::Transaction; 20 | 21 | /// BTC anchoring payload extractor 22 | /// 23 | /// Extracts and prints JSON object with payload of the given anchoring transaction. 24 | #[derive(StructOpt)] 25 | struct Opts { 26 | /// Bitcoin transaction hex. 27 | hex: String, 28 | } 29 | 30 | fn main() -> anyhow::Result<()> { 31 | let transaction = Transaction::from_hex(Opts::from_args().hex)?; 32 | let payload = transaction 33 | .anchoring_payload() 34 | .ok_or_else(|| anyhow!("Given transaction does not contains anchoring payload"))?; 35 | println!("{}", serde_json::to_string_pretty(&payload)?); 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "exonum-btc-anchoring" 3 | edition = "2018" 4 | version = "1.0.0" 5 | authors = ["The Exonum Team "] 6 | homepage = "https://exonum.com/doc/advanced/bitcoin-anchoring/" 7 | repository = "https://github.com/exonum/exonum-btc-anchoring" 8 | documentation = "https://docs.rs/exonum-btc-anchoring" 9 | readme = "README.md" 10 | license = "Apache-2.0" 11 | keywords = ["exonum", "blockchain", "bitcoin", "anchoring"] 12 | categories = ["database-implementations"] 13 | description = "An Exonum service that provides anchoring to Bitcoin blockchain." 14 | 15 | [badges] 16 | travis-ci = { repository = "exonum/exonum-btc-anchoring" } 17 | 18 | [dependencies] 19 | exonum = "1.0.0" 20 | exonum-cli = "1.0.0" 21 | exonum-crypto = { version = "1.0.0", features = ["with-protobuf"] } 22 | exonum-derive = "1.0.0" 23 | exonum-explorer = "1.0.0" 24 | exonum-merkledb = "1.0.0" 25 | exonum-proto = "1.0.0" 26 | exonum-rust-runtime = "1.0.0" 27 | exonum-supervisor = "1.0.0" 28 | exonum-testkit = "1.0.0" 29 | 30 | anyhow = "1.0.26" 31 | async-trait = "0.1.24" 32 | bitcoin = { version = "0.23", features = ["serde"] } 33 | bitcoin_hashes = { version = "0.7", features = ["serde"] } 34 | bitcoincore-rpc = "0.9.0" 35 | btc-transaction-utils = "0.9" 36 | byteorder = "1.3" 37 | derive_more = "0.99.3" 38 | futures = "0.3.4" 39 | hex = "0.4.0" 40 | jsonrpc = "0.11" 41 | log = "0.4" 42 | protobuf = { version = "2.8", features = ["with-serde"] } 43 | rand = "0.6" 44 | reqwest = "0.10.4" 45 | secp256k1 = { version = "0.17", features = ["serde"] } 46 | serde = "1.0" 47 | serde_derive = "1.0" 48 | serde_json = "1.0" 49 | serde_str = "0.1" 50 | structopt = "0.3" 51 | thiserror = "1.0.11" 52 | tokio = { version = "0.2.13", features = ["blocking", "dns", "io-util", "macros", "rt-threaded", "tcp", "time"] } 53 | toml = "0.5.6" 54 | 55 | [dev-dependencies] 56 | proptest = "0.9" 57 | 58 | [build-dependencies] 59 | exonum-build = "1.0.0" 60 | -------------------------------------------------------------------------------- /src/blockchain/errors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Error types of the BTC anchoring service. 16 | 17 | use exonum::runtime::{ExecutionError, ExecutionFail}; 18 | use exonum_derive::ExecutionFail; 19 | 20 | use crate::btc; 21 | 22 | /// Possible errors during execution of the `sign_input` method. 23 | #[derive(Debug, ExecutionFail)] 24 | pub enum Error { 25 | /// Transaction author is not authorized to sign anchoring transactions. 26 | UnauthorizedAnchoringKey = 0, 27 | /// Transaction input with the specified index is absent in the anchoring proposal. 28 | NoSuchInput = 1, 29 | /// The transaction input signature is invalid. 30 | InputVerificationFailed = 2, 31 | /// An error occurred while creating of the anchoring transaction proposal. 32 | AnchoringBuilderError = 3, 33 | /// Unexpected anchoring proposal transaction ID. 34 | UnexpectedProposalTxId = 4, 35 | /// Funding transaction has been already used. 36 | AlreadyUsedFundingTx = 5, 37 | /// Funding transaction is unsuitable. 38 | UnsuitableFundingTx = 6, 39 | } 40 | 41 | impl Error { 42 | /// Creates an error instance from the anchoring transaction builder error. 43 | pub fn anchoring_builder_error(error: btc::BuilderError) -> ExecutionError { 44 | Error::AnchoringBuilderError.with_description(error) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/proto/service.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Bitcoin anchoring service protobuf description. 16 | 17 | syntax = "proto3"; 18 | 19 | package exonum.service.btc_anchoring; 20 | 21 | import "exonum/crypto/types.proto"; 22 | import "btc_types.proto"; 23 | 24 | // Public keys of an anchoring node. 25 | message AnchoringKeys { 26 | // Service key is used to authorize service transactions. 27 | exonum.crypto.PublicKey service_key = 1; 28 | // The Bitcoin public key is used to calculate the corresponding redeem script. 29 | exonum.btc.PublicKey bitcoin_key = 2; 30 | } 31 | 32 | // Exonum message with a signature for one of the inputs of a new anchoring transaction. 33 | message SignInput { 34 | // Proposal transaction ID. 35 | exonum.btc.Sha256d txid = 1; 36 | // Signed input. 37 | fixed32 input = 2; 38 | // Signature content. 39 | exonum.btc.InputSignature input_signature = 3; 40 | } 41 | 42 | // Exonum message with the unspent funding transaction. 43 | message AddFunds { 44 | // Bitcoin transaction content. 45 | exonum.btc.Transaction transaction = 1; 46 | } 47 | 48 | /// Configuration parameters. 49 | message Config { 50 | // Type of the used BTC network. 51 | // 52 | // Possible values: 53 | // 54 | // Bitcoin - 3652501241(0xD9B4BEF9) 55 | // Testnet - 118034699(0x0709110B) 56 | // Regtest - 3669344250(0xDAB5BFFA) 57 | fixed32 network = 1; 58 | // Bitcoin public keys of nodes from from which the current anchoring redeem script can 59 | // be calculated. 60 | repeated AnchoringKeys anchoring_keys = 2; 61 | // Interval in blocks between anchored blocks. 62 | uint64 anchoring_interval = 3; 63 | // Fee per byte in satoshis. 64 | uint64 transaction_fee = 4; 65 | } 66 | 67 | // TODO Create separate constructor. 68 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! # Introduction 16 | //! 17 | //! Private blockchain infrastructure necessitates additional measures for 18 | //! accountability of the blockchain validators. 19 | //! In public proof of work blockchains (e.g., Bitcoin), accountability is purely economic and is 20 | //! based on game theory and equivocation or retroactive modifications being economically costly. 21 | //! Not so in private blockchains, where these two behaviors 22 | //! are a real threat per any realistic threat model that assumes 23 | //! that the blockchain is of use not only to the system validators, 24 | //! but also to third parties. 25 | //! 26 | //! This crate implements a protocol for blockchain anchoring onto the Bitcoin blockchain 27 | //! that utilizes the native Bitcoin capabilities of creating multisig([p2sh][1]) transactions. 28 | //! This transactions contains metadata from Exonum blockchain (block's hash on corresponding 29 | //! height) and forms a chain. 30 | //! 31 | //! You can read the details in [specification][2]. 32 | //! 33 | //! [1]: https://bitcoin.org/en/glossary/p2sh-multisig 34 | //! [2]: https://github.com/exonum/exonum-doc/blob/master/src/advanced/bitcoin-anchoring.md 35 | //! 36 | //! # Examples 37 | //! 38 | //! Create application with anchoring service 39 | //! 40 | //! ```rust,no_run 41 | //! use exonum_cli::{NodeBuilder, Spec}; 42 | //! 43 | //! #[tokio::main] 44 | //! async fn main() -> anyhow::Result<()> { 45 | //! exonum::helpers::init_logger()?; 46 | //! NodeBuilder::new() 47 | //! .with(Spec::new(exonum_btc_anchoring::BtcAnchoringService)) 48 | //! .run() 49 | //! .await 50 | //! } 51 | //! ``` 52 | //! 53 | 54 | #![warn( 55 | missing_docs, 56 | missing_debug_implementations, 57 | unsafe_code, 58 | bare_trait_objects 59 | )] 60 | 61 | pub use crate::service::BtcAnchoringService; 62 | 63 | pub mod api; 64 | pub mod blockchain; 65 | pub mod btc; 66 | pub mod config; 67 | pub mod sync; 68 | pub mod test_helpers; 69 | 70 | pub(crate) mod service; 71 | 72 | mod proto; 73 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | addons: 4 | apt: 5 | sources: 6 | - sourceline: 'ppa:maarten-fonville/protobuf' 7 | - sourceline: 'ppa:fsgmhoward/shadowsocks-libev' 8 | packages: 9 | - binutils-dev 10 | - build-essential 11 | - cmake 12 | - g++ 13 | - gcc 14 | - libcurl4-openssl-dev 15 | - libdw-dev 16 | - libelf-dev 17 | - libiberty-dev 18 | - libprotobuf-dev 19 | - librocksdb-dev 20 | - libsnappy-dev 21 | - libsodium-dev 22 | - libssl-dev 23 | - pkg-config 24 | - protobuf-compiler 25 | - unzip 26 | - zlib1g-dev 27 | 28 | rust: 29 | - 1.45.2 30 | 31 | cache: 32 | npm: true 33 | cargo: false 34 | directories: 35 | - $HOME/.cache 36 | - $HOME/.cargo 37 | - $HOME/.local 38 | - $HOME/.kcov 39 | 40 | dist: bionic 41 | sudo: required 42 | 43 | env: 44 | global: 45 | - SCCACHE_VERS=0.2.13 46 | - DEADLINKS_VERS=0.4.1 47 | - RUSTFLAGS="-D warnings" 48 | - ROCKSDB_LIB_DIR=/usr/lib 49 | - SNAPPY_LIB_DIR=/usr/lib/x86_64-linux-gnu 50 | 51 | before_install: 52 | - sccache -V | grep $SCCACHE_VERS || cargo install sccache --vers $SCCACHE_VERS --force 53 | - export RUSTC_WRAPPER=sccache 54 | 55 | jobs: 56 | allow_failures: 57 | - name: publish-with-rust 58 | - name: deadlinks 59 | include: 60 | # Formatting & other lints that do not require compilation 61 | - name: lints 62 | install: 63 | - rustup component add rustfmt 64 | - rustfmt -V 65 | - nvm install 12 && nvm use 12 66 | - npm install 67 | - ./node_modules/.bin/cspell --version 68 | - ./node_modules/.bin/markdownlint --version 69 | script: 70 | - cargo fmt -- --check 71 | - npm run cspell 72 | - npm run md 73 | 74 | # Clippy linting 75 | - name: clippy 76 | install: 77 | - rustup component add clippy 78 | - cargo clippy --version 79 | script: 80 | # Force clippy to use sccache, from issue 81 | # https://github.com/mozilla/sccache/issues/423#issuecomment-526614168 82 | - cargo check 83 | - touch Cargo.toml 84 | - cargo clippy --all -- -D warnings 85 | 86 | # Tests 87 | - name: linux-tests 88 | script: 89 | - cargo test --all 90 | 91 | # Non-fatal checks 92 | - name: deadlinks 93 | env: FEATURE=non-fatal-checks 94 | install: 95 | - cargo-deadlinks -V | grep $DEADLINKS_VERS || cargo install cargo-deadlinks --vers $DEADLINKS_VERS --force 96 | script: 97 | - cargo doc --no-deps 98 | - cargo deadlinks --dir target/doc 99 | 100 | # Check publish with Rust 1.45.2 101 | - name: publish-with-rust 102 | env: FEATURE=non-fatal-checks 103 | rust: 1.45.2 104 | script: 105 | - cargo publish --dry-run 106 | -------------------------------------------------------------------------------- /src/blockchain/data_layout/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Additional data types for the BTC anchoring information schema. 16 | 17 | use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; 18 | use exonum::crypto::{self, Hash}; 19 | use exonum_merkledb::{BinaryKey, ObjectHash}; 20 | 21 | use crate::btc::Sha256d; 22 | 23 | use std::io::{Cursor, Read, Write}; 24 | 25 | /// Unique transaction input identifier composed of a transaction identifier 26 | /// and an input index. 27 | #[derive(Debug, Copy, Clone, PartialEq)] 28 | pub struct TxInputId { 29 | /// Transaction identifier. 30 | pub txid: Sha256d, 31 | /// Transaction input index. 32 | pub input: u32, 33 | } 34 | 35 | impl TxInputId { 36 | /// Creates a new identifier. 37 | pub fn new(txid: Sha256d, input: u32) -> Self { 38 | Self { txid, input } 39 | } 40 | } 41 | 42 | impl BinaryKey for TxInputId { 43 | fn size(&self) -> usize { 44 | self.txid.size() + self.input.size() 45 | } 46 | 47 | fn read(inp: &[u8]) -> Self { 48 | let mut reader = Cursor::new(inp); 49 | 50 | let txid = { 51 | let mut txid = [0_u8; 32]; 52 | let _ = reader.read(&mut txid).unwrap(); 53 | Sha256d::new(txid) 54 | }; 55 | let input = reader.read_u32::().unwrap(); 56 | Self { txid, input } 57 | } 58 | 59 | fn write(&self, out: &mut [u8]) -> usize { 60 | let mut writer = Cursor::new(out); 61 | let _ = writer.write(&self.txid.0[..]).unwrap(); 62 | writer.write_u32::(self.input).unwrap(); 63 | self.size() 64 | } 65 | } 66 | 67 | impl ObjectHash for TxInputId { 68 | fn object_hash(&self) -> Hash { 69 | let mut bytes = [0_u8; 36]; 70 | self.write(&mut bytes); 71 | crypto::hash(bytes.as_ref()) 72 | } 73 | } 74 | 75 | #[test] 76 | fn test_tx_input_id_binary_key() { 77 | let txout = TxInputId { 78 | txid: Sha256d::from_slice(crypto::hash(&[1, 2, 3]).as_ref()).unwrap(), 79 | input: 2, 80 | }; 81 | 82 | let mut buf = vec![0_u8; txout.size()]; 83 | txout.write(&mut buf); 84 | 85 | let txout2 = TxInputId::read(&buf); 86 | assert_eq!(txout, txout2); 87 | 88 | let buf_hash = crypto::hash(&buf); 89 | assert_eq!(txout2.object_hash(), buf_hash); 90 | } 91 | -------------------------------------------------------------------------------- /launcher/exonum_btc_anchoring_plugin/plugin.py: -------------------------------------------------------------------------------- 1 | from exonum_launcher.instances import InstanceSpecLoader 2 | 3 | from exonum_client.protobuf_loader import ProtobufLoader 4 | from exonum_client.module_manager import ModuleManager 5 | from exonum_client.proofs.encoder import build_encoder_function 6 | 7 | from exonum_launcher.configuration import Instance 8 | 9 | from exonum_launcher.instances.instance_spec_loader import InstanceSpecLoader, InstanceSpecLoadError 10 | 11 | RUST_RUNTIME_ID = 0 12 | ANCHORING_ARTIFACT_NAME = "exonum-btc-anchoring" 13 | ANCHORING_ARTIFACT_VERSION = "1.0.0" 14 | 15 | 16 | def import_anchoring_module(name: str): 17 | return ModuleManager.import_service_module( 18 | ANCHORING_ARTIFACT_NAME, ANCHORING_ARTIFACT_VERSION, name) 19 | 20 | 21 | def bitcoin_network_from_string(network_string: str) -> int: 22 | match = { 23 | "bitcoin": 0xD9B4BEF9, 24 | "testnet": 0x0709110B, 25 | "regtest": 0xDAB5BFFA 26 | } 27 | return match[network_string] 28 | 29 | 30 | class AnchoringInstanceSpecLoader(InstanceSpecLoader): 31 | """Spec loader for btc anchoring.""" 32 | 33 | def load_spec(self, loader: ProtobufLoader, instance: Instance) -> bytes: 34 | try: 35 | # Load proto files for the Exonum anchoring service: 36 | loader.load_service_proto_files( 37 | RUST_RUNTIME_ID, ANCHORING_ARTIFACT_NAME, ANCHORING_ARTIFACT_VERSION) 38 | 39 | service_module = import_anchoring_module("service") 40 | btc_types_module = import_anchoring_module("btc_types") 41 | exonum_types_module = import_anchoring_module("exonum.crypto.types") 42 | 43 | # Create config message 44 | config = service_module.Config() 45 | config.network = bitcoin_network_from_string( 46 | instance.config["network"]) 47 | config.anchoring_interval = instance.config["anchoring_interval"] 48 | config.transaction_fee = instance.config["transaction_fee"] 49 | 50 | anchoring_keys = [] 51 | for keypair in instance.config["anchoring_keys"]: 52 | service_key = exonum_types_module.PublicKey( 53 | data=bytes.fromhex(keypair["service_key"])) 54 | bitcoin_key = btc_types_module.PublicKey( 55 | data=bytes.fromhex(keypair["bitcoin_key"])) 56 | 57 | anchoring_keys_type = service_module.AnchoringKeys() 58 | anchoring_keys_type.service_key.CopyFrom(service_key) 59 | anchoring_keys_type.bitcoin_key.CopyFrom(bitcoin_key) 60 | anchoring_keys.append(anchoring_keys_type) 61 | config.anchoring_keys.extend(anchoring_keys) 62 | 63 | result = config.SerializeToString() 64 | 65 | # We're catching all the exceptions to shutdown gracefully (on the caller side) just in case. 66 | # pylint: disable=broad-except 67 | except Exception as error: 68 | artifact_name = instance.artifact.name 69 | raise InstanceSpecLoadError( 70 | f"Couldn't get a proto description for artifact: {artifact_name}, error: {error}" 71 | ) 72 | 73 | return result 74 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use exonum::{ 16 | helpers::ValidateInput, 17 | merkledb::BinaryValue, 18 | runtime::{CommonError, ExecutionContext, ExecutionError}, 19 | }; 20 | use exonum_derive::{ServiceDispatcher, ServiceFactory}; 21 | use exonum_rust_runtime::{api::ServiceApiBuilder, Service}; 22 | use exonum_supervisor::Configure; 23 | 24 | use crate::{ 25 | api, 26 | blockchain::{BtcAnchoringInterface, Schema}, 27 | config::Config, 28 | proto, 29 | }; 30 | 31 | /// Bitcoin anchoring service implementation for the Exonum blockchain. 32 | #[derive(ServiceFactory, ServiceDispatcher, Debug, Clone, Copy)] 33 | #[service_dispatcher(implements("BtcAnchoringInterface", raw = "Configure"))] 34 | #[service_factory(proto_sources = "proto")] 35 | pub struct BtcAnchoringService; 36 | 37 | impl Service for BtcAnchoringService { 38 | fn initialize( 39 | &self, 40 | context: ExecutionContext<'_>, 41 | params: Vec, 42 | ) -> Result<(), ExecutionError> { 43 | // TODO Use a special type for constructor. [ECR-3222] 44 | let config = Config::from_bytes(params.into()) 45 | .and_then(ValidateInput::into_validated) 46 | .map_err(CommonError::malformed_arguments)?; 47 | 48 | Schema::new(context.service_data()) 49 | .actual_config 50 | .set(config); 51 | Ok(()) 52 | } 53 | 54 | fn wire_api(&self, builder: &mut ServiceApiBuilder) { 55 | api::wire(builder); 56 | } 57 | } 58 | 59 | impl Configure for BtcAnchoringService { 60 | type Params = Config; 61 | 62 | fn verify_config( 63 | &self, 64 | context: ExecutionContext<'_>, 65 | params: Self::Params, 66 | ) -> Result<(), ExecutionError> { 67 | context 68 | .caller() 69 | .as_supervisor() 70 | .ok_or(CommonError::UnauthorizedCaller)?; 71 | 72 | params.validate().map_err(CommonError::malformed_arguments) 73 | } 74 | 75 | fn apply_config( 76 | &self, 77 | context: ExecutionContext<'_>, 78 | params: Self::Params, 79 | ) -> Result<(), ExecutionError> { 80 | context 81 | .caller() 82 | .as_supervisor() 83 | .ok_or(CommonError::UnauthorizedCaller)?; 84 | 85 | let mut schema = Schema::new(context.service_data()); 86 | if schema.actual_config().anchoring_address() == params.anchoring_address() { 87 | // There are no changes in the anchoring address, so we just apply the config 88 | // immediately. 89 | schema.actual_config.set(params); 90 | } else { 91 | // Set the config as the next one, which will become an actual after the transition 92 | // of the anchoring chain to the following address. 93 | schema.following_config.set(params); 94 | } 95 | Ok(()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/sync/bitcoin_relay.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Collections of helpers for synchronization with the Bitcoin network. 16 | 17 | use async_trait::async_trait; 18 | use bitcoincore_rpc::RpcApi; 19 | use jsonrpc::Error as JsonRpcError; 20 | 21 | use crate::btc; 22 | 23 | /// Status of the transaction in the Bitcoin network. 24 | #[derive(Debug, Copy, Clone, PartialEq)] 25 | pub enum TransactionStatus { 26 | /// Transaction is unknown in the Bitcoin network. 27 | Unknown, 28 | /// The transaction is not committed, but presented in the Bitcoin node memory pool. 29 | Mempool, 30 | /// The transaction was completed to the Bitcoin blockchain with the specified number 31 | /// of confirmations. 32 | Committed(u32), 33 | } 34 | 35 | impl TransactionStatus { 36 | /// Checks that this transaction is known by the Bitcoin network. 37 | pub fn is_known(self) -> bool { 38 | self != TransactionStatus::Unknown 39 | } 40 | 41 | /// Returns number of transaction confirmations in Bitcoin blockchain. 42 | pub fn confirmations(self) -> Option { 43 | if let TransactionStatus::Committed(confirmations) = self { 44 | Some(confirmations) 45 | } else { 46 | None 47 | } 48 | } 49 | } 50 | 51 | /// Describes communication with the Bitcoin network node. 52 | #[async_trait] 53 | pub trait BitcoinRelay { 54 | /// Error type for the current Bitcoin relay implementation. 55 | type Error; 56 | /// Sends a raw transaction to the Bitcoin network node. 57 | async fn send_transaction( 58 | &self, 59 | transaction: &btc::Transaction, 60 | ) -> Result; 61 | /// Gets status for the transaction with the specified identifier. 62 | async fn transaction_status(&self, id: btc::Sha256d) -> Result; 63 | } 64 | 65 | #[async_trait] 66 | impl BitcoinRelay for bitcoincore_rpc::Client { 67 | type Error = bitcoincore_rpc::Error; 68 | 69 | async fn send_transaction( 70 | &self, 71 | transaction: &btc::Transaction, 72 | ) -> Result { 73 | self.send_raw_transaction(transaction.to_string()) 74 | .map(|txid| btc::Sha256d(txid.into())) 75 | } 76 | 77 | async fn transaction_status(&self, id: btc::Sha256d) -> Result { 78 | match self.get_raw_transaction_verbose(&id.into(), None) { 79 | Ok(info) => { 80 | let status = match info.confirmations { 81 | None => TransactionStatus::Mempool, 82 | Some(num) => TransactionStatus::Committed(num), 83 | }; 84 | Ok(status) 85 | } 86 | // TODO Write more graceful error handling. [ECR-3222] 87 | Err(bitcoincore_rpc::Error::JsonRpc(JsonRpcError::Rpc(_))) => { 88 | Ok(TransactionStatus::Unknown) 89 | } 90 | Err(e) => Err(e), 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/proto/binary_map.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use exonum::crypto::Hash; 16 | use exonum_merkledb::{BinaryValue, ObjectHash}; 17 | use exonum_proto::ProtobufConvert; 18 | use protobuf::Message; 19 | 20 | use std::{borrow::Cow, collections::BTreeMap}; 21 | 22 | /// Protobuf wrapper type to store small maps of non-scalar keys and values. 23 | #[derive(Debug)] 24 | pub struct BinaryMap(pub BTreeMap); 25 | 26 | impl Default for BinaryMap 27 | where 28 | K: Ord, 29 | { 30 | fn default() -> Self { 31 | Self(BTreeMap::new()) 32 | } 33 | } 34 | 35 | #[derive(ProtobufConvert)] 36 | #[protobuf_convert(source = "crate::proto::internal::KeyValue")] 37 | struct KeyValue { 38 | key: Vec, 39 | value: Vec, 40 | } 41 | 42 | fn pair_to_key_value_pb(pair: (&K, &V)) -> crate::proto::internal::KeyValue 43 | where 44 | K: BinaryValue, 45 | V: BinaryValue, 46 | { 47 | KeyValue { 48 | key: pair.0.to_bytes(), 49 | value: pair.1.to_bytes(), 50 | } 51 | .to_pb() 52 | } 53 | 54 | fn key_value_pb_to_pair(pb: crate::proto::internal::KeyValue) -> anyhow::Result<(K, V)> 55 | where 56 | K: BinaryValue, 57 | V: BinaryValue, 58 | { 59 | let KeyValue { key, value } = KeyValue::from_pb(pb)?; 60 | let key = K::from_bytes(key.into())?; 61 | let value = V::from_bytes(value.into())?; 62 | Ok((key, value)) 63 | } 64 | 65 | impl ProtobufConvert for BinaryMap 66 | where 67 | K: BinaryValue + Ord, 68 | V: BinaryValue, 69 | { 70 | type ProtoStruct = crate::proto::internal::BinaryMap; 71 | 72 | fn to_pb(&self) -> Self::ProtoStruct { 73 | let mut proto_struct = Self::ProtoStruct::new(); 74 | proto_struct.inner = self 75 | .0 76 | .iter() 77 | .map(pair_to_key_value_pb) 78 | .collect::>() 79 | .into(); 80 | proto_struct 81 | } 82 | 83 | fn from_pb(proto_struct: Self::ProtoStruct) -> anyhow::Result { 84 | let inner = proto_struct 85 | .inner 86 | .into_iter() 87 | .map(key_value_pb_to_pair) 88 | .collect::>()?; 89 | Ok(Self(inner)) 90 | } 91 | } 92 | 93 | impl BinaryValue for BinaryMap 94 | where 95 | K: BinaryValue + Ord, 96 | V: BinaryValue, 97 | { 98 | fn to_bytes(&self) -> Vec { 99 | self.to_pb() 100 | .write_to_bytes() 101 | .expect("Error while serializing value") 102 | } 103 | 104 | fn from_bytes(bytes: Cow<[u8]>) -> anyhow::Result { 105 | let mut pb = ::ProtoStruct::new(); 106 | pb.merge_from_bytes(bytes.as_ref())?; 107 | Self::from_pb(pb) 108 | } 109 | } 110 | 111 | impl ObjectHash for BinaryMap 112 | where 113 | K: BinaryValue + Ord, 114 | V: BinaryValue, 115 | { 116 | fn object_hash(&self) -> Hash { 117 | exonum::crypto::hash(&self.to_bytes()) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /guides/maintenance.md: -------------------------------------------------------------------------------- 1 | # A Maintenance Guide 2 | 3 | This manual is intended for advanced users who are already able to launch an anchoring 4 | instance in accordance of a [newbie](newbie.md) guide. 5 | 6 | The manual describes most common procedures of the service maintenance: 7 | 8 | * [Funding of anchoring chain wallet](#Funding-of-anchoring-chain-wallet) 9 | * [Modification of configuration parameters](#Modification-of-configuration-parameters) 10 | * [Changing the list of anchoring nodes](#Changing-the-list-of-anchoring-nodes) 11 | 12 | ## Funding of Anchoring Chain Wallet 13 | 14 | Anchoring process can be performed only if there is a sufficient amount of funds 15 | on the anchoring wallet. To keep anchoring working, you have to add funds to 16 | this wallet and keep the balance non-zero all the time that anchoring node is 17 | running. To increase a balance of the anchoring wallet, you should do the following: 18 | 19 | 1. Get the actual anchoring address, which can be obtained by the public HTTP API 20 | [endpoint][anchoring:actual-address]. 21 | 2. Send a some amount of Bitcoins to the actual anchoring address and then save received. 22 | transaction hex and wait until it get enough confirmations. 23 | 3. After ensuring that transaction have got enough confirmations send it to each of the 24 | anchoring nodes using the corresponding private HTTP API [endpoint][anchoring:add-funds]. 25 | 26 | ***Beware!** The anchoring node itself does not check that the funding 27 | transaction is confirmed and can be spend. If you send a malformed transaction, 28 | the behavior of the anchoring node is undefined.* 29 | 30 | ## Modification of Configuration Parameters 31 | 32 | You can use the [`exonum-python-client`][exonum-python-client] utility to change the 33 | anchoring configuration. 34 | 35 | List of parameters that you can change without any preparatory actions: 36 | 37 | * `transaction_fee` - the amount of the fee per byte in satoshis for anchoring 38 | transactions. 39 | * `anchoring_interval` - the interval in blocks between anchored blocks. 40 | 41 | The `anchoring_keys` change procedure is more complicated, you can find the description of this process 42 | in the next section. 43 | 44 | ## Changing The List of Anchoring Nodes 45 | 46 | * **Excluding node from the anchoring nodes.** 47 | 48 | The simplest case of changing anchoring nodes list is to exclude one of node from anchoring. 49 | You just have to exclude their keys from the `anchoring_keys` array. 50 | 51 | * **Adding a new node to the list of anchoring nodes.** 52 | 53 | In this case you must prepare the candidate node for inclusion in the list of 54 | anchoring nodes. In according of a [newbie guide][newbie_guide:step-3] you 55 | should generate Bitcoin keypair for the candidate. After tha configuration 56 | is applied, you must remember to run the `btc_anchoring_sync` utility. 57 | 58 | * **Changing of the bitcoin key of an existing anchoring node.** 59 | 60 | This case is rare and in many ways similar to the previous one, but there 61 | are some differences. Instead of generating a new config for the sync utility 62 | you have to add a new Bitcoin keypair to the existing one. 63 | 64 | To do it, run `btc_anchoring_sync` utility: 65 | 66 | ```shell 67 | cargo run --example btc_anchoring_sync generate-keypair -c path/to/anchoring/sync.toml 68 | ``` 69 | 70 | As a result of this call you will obtain a new `bitcoin_key`, which you may 71 | use to replace the existing one. 72 | 73 | [anchoring:actual-address]: https://exonum.com/doc/version/latest/advanced/bitcoin-anchoring/#actual-address 74 | [anchoring:add-funds]: https://exonum.com/doc/version/latest/advanced/bitcoin-anchoring/#add-funds 75 | [exonum-python-client]: https://github.com/exonum/exonum-python-client 76 | [newbie_guide:step-3]: newbie.md#step-3-deploying-and-running 77 | -------------------------------------------------------------------------------- /exonum-dictionary.txt: -------------------------------------------------------------------------------- 1 | 2NFNp5RbTyEwV8yijYg9sUCHsVApiqov8DA 2 | actix 3 | addrs 4 | amqvim 5 | ARQF 6 | Asyqzv 7 | atomicity 8 | Awnht 9 | backend 10 | bech32 11 | bigint 12 | bitcoincore 13 | bitcoind 14 | bitcoinrpc 15 | bitfury 16 | bitvec 17 | blockchain 18 | blockchains 19 | blockdata 20 | bodyparser 21 | brainwallet 22 | btree 23 | Bwnht 24 | bytearray 25 | byteorder 26 | cfgs 27 | CHECKMULTISIG 28 | checkpointed 29 | clippy 30 | clonned 31 | coinbase 32 | compat 33 | concat 34 | counterintuitive 35 | cryptocurrency 36 | cryptographically 37 | deadlinks 38 | deque 39 | deref 40 | deserialization 41 | deserialize 42 | deserialized 43 | deserializer 44 | deserializes 45 | deserializing 46 | DESTDIR 47 | deterministic 48 | deterministically 49 | dhash 50 | dumprpivkey 51 | ecdsa 52 | Ejehs 53 | emsp 54 | Exonum 55 | fsync 56 | fuzzer 57 | generatetoaddress 58 | getbalance 59 | getbestblockhash 60 | getbestblockhash 61 | getblock 62 | getblockcount 63 | getblockhash 64 | getnewaddress 65 | getrawtransaction 66 | GFBRKYE 67 | gitter 68 | Hasher 69 | hdkeypath 70 | hdmasterkeyid 71 | healthcheck 72 | idempotence 73 | importaddress 74 | inited 75 | iscompressed 76 | ismine 77 | isscript 78 | isvalid 79 | iswatchonly 80 | JJBZ 81 | Jjqe 82 | jsonrpc 83 | keepalive 84 | keyhash 85 | keypair 86 | keypairs 87 | lects 88 | leveldb 89 | libc 90 | libfuzzer 91 | librocksdb 92 | libsnappy 93 | libsodium 94 | libssl 95 | listunspent 96 | locktime 97 | mainnet 98 | Mainnet 99 | maintainer's 100 | maplit 101 | markdownlint 102 | Mdewrpx 103 | memorydb 104 | mempool 105 | Merkelized 106 | Merkle 107 | merkledb 108 | millis 109 | mkdir 110 | mmoXxKhAwnhtFiAMvxJ82CKCBia751mzfY 111 | mmoXxKhAwnhtFiAMvxJ82CKCBia751mzfY 112 | mmoXxKhBwnhtFiAMvxJ82CKCBia751mzfY 113 | mn1jSMdewrpxTDkg1N6brC7fpTNV9X2Cmq 114 | mpsc 115 | msgs 116 | multisig 117 | mutex 118 | mynk 119 | mynkNvvoysgzn3CX51KwyKyNVbEJEHs8Cw 120 | mynkNvvoysgzn3CX51KwyKyNVbEJEHs8Cw 121 | n4a3q23iUKZsmmrT5bVkeAsyqzvR5TmUbf 122 | nanos 123 | NCJY 124 | newtype 125 | nodelay 126 | ntxid 127 | Nvvoysgzn 128 | oneshot 129 | OP_CHECKSIG 130 | OP_EQUALVERIFY 131 | openssl 132 | passwd 133 | PASSWD 134 | peekable 135 | permissioned 136 | pkey 137 | postcondition 138 | postpropose 139 | postvote 140 | precommit 141 | precommits 142 | prevote 143 | prevotes 144 | println 145 | PrivateKey 146 | PrivateKey 147 | proptest 148 | proto 149 | protobuf 150 | protoc 151 | PROTOS 152 | pubkey 153 | pubkeyhash 154 | pubkeys 155 | PUSHBYTES 156 | readonly 157 | reddit 158 | regtest 159 | Regtest 160 | reimplemented 161 | repr 162 | reqwest 163 | rescan 164 | RESTful 165 | roadmap 166 | rocksdb 167 | roughtime 168 | roundtrip 169 | rpcbind 170 | rpcpassword 171 | rpcuser 172 | rustfmt 173 | rustup 174 | SATOSHI 175 | satoshis 176 | sccache 177 | scripthash 178 | scriptSig 179 | scriptSigs 180 | secp 181 | seedable 182 | segwit 183 | sendrawtransaction 184 | sendtoaddress 185 | serde 186 | serializable 187 | serialize 188 | serializer 189 | serizalize 190 | sighash 191 | sighex 192 | signum 193 | Sigs 194 | socketaddr 195 | sodiumoxide 196 | stringify 197 | struct 198 | structfield 199 | structopt 200 | structs 201 | subcommand 202 | subcommands 203 | subfolder 204 | subsec 205 | supermajority 206 | supervisorctl 207 | supervisord 208 | tempdir 209 | terminfo 210 | testdata 211 | testkit 212 | testnet 213 | testnetctl 214 | Tfop 215 | thiserror 216 | timestamping 217 | timestamps 218 | tlsdate 219 | Tmjn 220 | tmpdir 221 | Toas 222 | toml 223 | toolchain 224 | txhex 225 | txid 226 | txin 227 | txindex 228 | txinfo 229 | txinwitness 230 | txout 231 | txvec 232 | tymethod 233 | uint 234 | unboxed 235 | unreceived 236 | unsync 237 | unsynced 238 | untagged 239 | userid 240 | usize 241 | utxo 242 | utxos 243 | validateaddress 244 | validator 245 | validator's 246 | validators 247 | Varint 248 | venv 249 | vout 250 | vsize 251 | Vxyzr 252 | whitelisted 253 | writeln 254 | wtxid 255 | Xqsmt 256 | Zsmmr 257 | -------------------------------------------------------------------------------- /src/btc/macros.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | macro_rules! impl_wrapper_for_bitcoin_type { 16 | ($name:ident) => { 17 | impl_wrapper_for_bitcoin_consensus_encoding! { $name } 18 | impl_string_conversions_for_hex! { $name } 19 | impl_serde_str! { $name } 20 | }; 21 | } 22 | 23 | macro_rules! impl_wrapper_for_bitcoin_consensus_encoding { 24 | ($name:ident) => { 25 | impl exonum_merkledb::BinaryValue for $name { 26 | fn to_bytes(&self) -> Vec { 27 | bitcoin::consensus::serialize(&self.0) 28 | } 29 | 30 | fn from_bytes(value: ::std::borrow::Cow<[u8]>) -> anyhow::Result<$name> { 31 | let inner = bitcoin::consensus::deserialize(value.as_ref())?; 32 | Ok(Self(inner)) 33 | } 34 | } 35 | 36 | impl exonum_merkledb::ObjectHash for $name { 37 | fn object_hash(&self) -> exonum::crypto::Hash { 38 | let bytes = bitcoin::consensus::serialize(&self.0); 39 | exonum::crypto::hash(&bytes) 40 | } 41 | } 42 | 43 | impl hex::FromHex for $name { 44 | type Error = anyhow::Error; 45 | 46 | fn from_hex>(hex: T) -> Result { 47 | let bytes = ::hex::decode(hex)?; 48 | let inner = ::bitcoin::consensus::deserialize(bytes.as_ref())?; 49 | Ok(Self(inner)) 50 | } 51 | } 52 | 53 | impl hex::ToHex for $name { 54 | fn encode_hex>(&self) -> T { 55 | bitcoin::consensus::serialize(&self.0).encode_hex() 56 | } 57 | 58 | fn encode_hex_upper>(&self) -> T { 59 | bitcoin::consensus::serialize(&self.0).encode_hex_upper() 60 | } 61 | } 62 | }; 63 | } 64 | 65 | macro_rules! impl_string_conversions_for_hex { 66 | ($name:ident) => { 67 | impl std::fmt::LowerHex for $name { 68 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 69 | use hex::ToHex; 70 | write!(f, "{}", self.encode_hex::()) 71 | } 72 | } 73 | 74 | impl std::fmt::Display for $name { 75 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 76 | write!(f, "{:x}", self) 77 | } 78 | } 79 | 80 | impl std::str::FromStr for $name { 81 | type Err = anyhow::Error; 82 | 83 | fn from_str(s: &str) -> Result { 84 | use hex::FromHex; 85 | Self::from_hex(s).map_err(From::from) 86 | } 87 | } 88 | }; 89 | } 90 | 91 | macro_rules! impl_serde_str { 92 | ($name:ident) => { 93 | impl serde::Serialize for $name { 94 | fn serialize(&self, ser: S) -> std::result::Result 95 | where 96 | S: serde::Serializer, 97 | { 98 | serde_str::serialize(self, ser) 99 | } 100 | } 101 | 102 | impl<'de> serde::Deserialize<'de> for $name { 103 | fn deserialize(deserializer: D) -> Result 104 | where 105 | D: serde::Deserializer<'de>, 106 | { 107 | serde_str::deserialize(deserializer) 108 | } 109 | } 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /src/blockchain/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Blockchain implementation details for the BTC anchoring service. 16 | 17 | pub use self::{schema::Schema, transactions::BtcAnchoringInterface}; 18 | pub use crate::proto::{AddFunds, SignInput}; 19 | 20 | use bitcoin::blockdata::script::Script; 21 | use btc_transaction_utils::{multisig::RedeemScript, p2wsh}; 22 | use exonum::helpers::Height; 23 | 24 | use crate::{btc::Address, config::Config}; 25 | 26 | pub mod data_layout; 27 | pub mod errors; 28 | pub mod schema; 29 | pub mod transactions; 30 | 31 | /// Current state of the BTC anchoring service. 32 | #[derive(Debug, Clone)] 33 | pub enum BtcAnchoringState { 34 | /// The usual anchoring workflow. 35 | Regular { 36 | /// Current anchoring configuration. 37 | actual_configuration: Config, 38 | }, 39 | /// The transition from the current anchoring address to the following one. 40 | Transition { 41 | /// Current anchoring configuration. 42 | actual_configuration: Config, 43 | /// Following anchoring configuration. 44 | following_configuration: Config, 45 | }, 46 | } 47 | 48 | impl BtcAnchoringState { 49 | /// Returns the redeem script corresponding to the address to which the anchoring 50 | /// transaction will be sent. 51 | pub fn redeem_script(&self) -> RedeemScript { 52 | match self { 53 | BtcAnchoringState::Regular { 54 | actual_configuration, 55 | } => actual_configuration.redeem_script(), 56 | BtcAnchoringState::Transition { 57 | following_configuration, 58 | .. 59 | } => following_configuration.redeem_script(), 60 | } 61 | } 62 | 63 | /// Returns the `script_pubkey` for the corresponding redeem script. 64 | pub fn script_pubkey(&self) -> Script { 65 | self.redeem_script().as_ref().to_v0_p2wsh() 66 | } 67 | 68 | /// Returns the output address for the corresponding redeem script. 69 | pub fn output_address(&self) -> Address { 70 | p2wsh::address(&self.redeem_script(), self.actual_config().network).into() 71 | } 72 | 73 | /// Checks that anchoring state is regular. 74 | pub fn is_regular(&self) -> bool { 75 | if let BtcAnchoringState::Regular { .. } = self { 76 | true 77 | } else { 78 | false 79 | } 80 | } 81 | 82 | /// Checks that anchoring is in the transition state. 83 | pub fn is_transition(&self) -> bool { 84 | if let BtcAnchoringState::Transition { .. } = self { 85 | true 86 | } else { 87 | false 88 | } 89 | } 90 | 91 | /// Returns the actual anchoring configuration. 92 | pub fn actual_config(&self) -> &Config { 93 | match self { 94 | BtcAnchoringState::Regular { 95 | ref actual_configuration, 96 | } => actual_configuration, 97 | BtcAnchoringState::Transition { 98 | ref actual_configuration, 99 | .. 100 | } => actual_configuration, 101 | } 102 | } 103 | 104 | /// Returns the following anchoring configuration if anchoring is in transition state. 105 | pub fn following_config(&self) -> Option<&Config> { 106 | match self { 107 | BtcAnchoringState::Regular { .. } => None, 108 | BtcAnchoringState::Transition { 109 | ref following_configuration, 110 | .. 111 | } => Some(following_configuration), 112 | } 113 | } 114 | 115 | /// Returns the nearest following anchoring height for the given height. 116 | pub fn following_anchoring_height(&self, latest_anchored_height: Option) -> Height { 117 | latest_anchored_height.map_or_else(Height::zero, |height| match self { 118 | BtcAnchoringState::Regular { 119 | ref actual_configuration, 120 | } => actual_configuration.following_anchoring_height(height), 121 | BtcAnchoringState::Transition { .. } => height, 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/proto/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Module of the rust-protobuf generated files. 16 | 17 | pub use binary_map::BinaryMap; 18 | 19 | use anyhow::anyhow; 20 | use exonum::{ 21 | crypto::{proto::*, Hash, PublicKey}, 22 | merkledb::{ 23 | impl_object_hash_for_binary_value, impl_serde_hex_for_binary_value, BinaryKey, BinaryValue, 24 | ObjectHash, 25 | }, 26 | }; 27 | use exonum_derive::{BinaryValue, ObjectHash}; 28 | use exonum_proto::ProtobufConvert; 29 | use protobuf::Message; 30 | use serde_derive::{Deserialize, Serialize}; 31 | 32 | use std::borrow::Cow; 33 | 34 | use crate::btc; 35 | 36 | mod binary_map; 37 | 38 | include!(concat!(env!("OUT_DIR"), "/protobuf_mod.rs")); 39 | 40 | impl ProtobufConvert for btc::PublicKey { 41 | type ProtoStruct = btc_types::PublicKey; 42 | 43 | fn to_pb(&self) -> Self::ProtoStruct { 44 | let mut proto_struct = Self::ProtoStruct::default(); 45 | self.0.write_into(&mut proto_struct.data); 46 | proto_struct 47 | } 48 | 49 | fn from_pb(pb: Self::ProtoStruct) -> anyhow::Result { 50 | let bytes = pb.get_data(); 51 | Ok(Self(bitcoin::PublicKey::from_slice(bytes)?)) 52 | } 53 | } 54 | 55 | impl ProtobufConvert for btc::Transaction { 56 | type ProtoStruct = btc_types::Transaction; 57 | 58 | fn to_pb(&self) -> Self::ProtoStruct { 59 | let bytes = bitcoin::consensus::serialize(&self.0); 60 | let mut proto_struct = Self::ProtoStruct::default(); 61 | proto_struct.set_data(bytes); 62 | proto_struct 63 | } 64 | 65 | fn from_pb(pb: Self::ProtoStruct) -> anyhow::Result { 66 | let bytes = pb.get_data(); 67 | Ok(Self(bitcoin::consensus::deserialize(bytes)?)) 68 | } 69 | } 70 | 71 | impl ProtobufConvert for btc::InputSignature { 72 | type ProtoStruct = btc_types::InputSignature; 73 | 74 | fn to_pb(&self) -> Self::ProtoStruct { 75 | let mut proto_struct = Self::ProtoStruct::default(); 76 | proto_struct.set_data(self.0.as_ref().to_vec()); 77 | proto_struct 78 | } 79 | 80 | fn from_pb(pb: Self::ProtoStruct) -> anyhow::Result { 81 | let bytes = pb.get_data().to_vec(); 82 | Ok(Self(btc_transaction_utils::InputSignature::from_bytes( 83 | bytes, 84 | )?)) 85 | } 86 | } 87 | 88 | impl ProtobufConvert for btc::Sha256d { 89 | type ProtoStruct = btc_types::Sha256d; 90 | 91 | fn to_pb(&self) -> Self::ProtoStruct { 92 | let mut proto_struct = Self::ProtoStruct::default(); 93 | proto_struct.data.extend(&self.0[..]); 94 | proto_struct 95 | } 96 | 97 | fn from_pb(pb: Self::ProtoStruct) -> anyhow::Result { 98 | use bitcoin_hashes::{sha256d, Hash}; 99 | sha256d::Hash::from_slice(pb.get_data()) 100 | .map(Self::from) 101 | .map_err(From::from) 102 | } 103 | } 104 | 105 | /// Public keys of an anchoring node. 106 | #[derive( 107 | Serialize, Deserialize, Debug, Clone, PartialEq, ProtobufConvert, BinaryValue, ObjectHash, 108 | )] 109 | #[protobuf_convert(source = "self::service::AnchoringKeys")] 110 | pub struct AnchoringKeys { 111 | /// Service key is used to authorize transactions. 112 | pub service_key: PublicKey, 113 | /// The Bitcoin public key is used to calculate the corresponding redeem script. 114 | pub bitcoin_key: btc::PublicKey, 115 | } 116 | 117 | /// Exonum message with a signature for one of the inputs of a new anchoring transaction. 118 | #[derive(Debug, Clone, PartialEq, ProtobufConvert, BinaryValue, ObjectHash)] 119 | #[protobuf_convert(source = "self::service::SignInput")] 120 | pub struct SignInput { 121 | /// Proposal transaction id. 122 | pub txid: Sha256d, 123 | /// Signed input. 124 | pub input: u32, 125 | /// Signature content. 126 | pub input_signature: btc::InputSignature, 127 | } 128 | 129 | /// Exonum message with the unspent funding transaction. 130 | #[derive(Debug, Clone, PartialEq, ProtobufConvert, BinaryValue, ObjectHash)] 131 | #[protobuf_convert(source = "self::service::AddFunds")] 132 | pub struct AddFunds { 133 | /// Transaction content. 134 | pub transaction: btc::Transaction, 135 | } 136 | 137 | /// Consensus parameters in the BTC anchoring. 138 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, BinaryValue, ObjectHash)] 139 | pub struct Config { 140 | /// Type of the used BTC network. 141 | pub network: bitcoin::Network, 142 | /// Bitcoin public keys of nodes from from which the current anchoring redeem script can be calculated. 143 | pub anchoring_keys: Vec, 144 | /// Interval in blocks between anchored blocks. 145 | pub anchoring_interval: u64, 146 | /// Fee per byte in satoshis. 147 | pub transaction_fee: u64, 148 | } 149 | 150 | impl ProtobufConvert for Config { 151 | type ProtoStruct = self::service::Config; 152 | 153 | fn to_pb(&self) -> Self::ProtoStruct { 154 | let mut proto_struct = Self::ProtoStruct::default(); 155 | 156 | proto_struct.set_network(self.network.magic()); 157 | proto_struct.set_anchoring_keys(self.anchoring_keys.to_pb().into()); 158 | proto_struct.set_anchoring_interval(self.anchoring_interval.to_pb()); 159 | proto_struct.set_transaction_fee(self.transaction_fee.to_pb()); 160 | proto_struct 161 | } 162 | 163 | fn from_pb(mut pb: Self::ProtoStruct) -> anyhow::Result { 164 | let network = bitcoin::Network::from_magic(pb.get_network()) 165 | .ok_or_else(|| anyhow!("Unknown Bitcoin network"))?; 166 | 167 | Ok(Self { 168 | network, 169 | anchoring_keys: ProtobufConvert::from_pb(pb.take_anchoring_keys().into_vec())?, 170 | anchoring_interval: ProtobufConvert::from_pb(pb.get_anchoring_interval())?, 171 | transaction_fee: ProtobufConvert::from_pb(pb.get_transaction_fee())?, 172 | }) 173 | } 174 | } 175 | 176 | impl_serde_hex_for_binary_value! { SignInput } 177 | 178 | impl BinaryValue for btc::Sha256d { 179 | fn to_bytes(&self) -> Vec { 180 | self.to_pb() 181 | .write_to_bytes() 182 | .expect("Error while serializing value") 183 | } 184 | 185 | fn from_bytes(bytes: Cow<[u8]>) -> anyhow::Result { 186 | let mut pb = btc_types::Sha256d::new(); 187 | pb.merge_from_bytes(bytes.as_ref())?; 188 | Self::from_pb(pb) 189 | } 190 | } 191 | 192 | impl BinaryKey for btc::Sha256d { 193 | fn size(&self) -> usize { 194 | Self::LEN 195 | } 196 | 197 | fn write(&self, buffer: &mut [u8]) -> usize { 198 | buffer.copy_from_slice(&self.0[..]); 199 | self.size() 200 | } 201 | 202 | fn read(buffer: &[u8]) -> Self::Owned { 203 | Self::from_slice(buffer).unwrap() 204 | } 205 | } 206 | 207 | // TODO Fix kind of input for these macro [ECR-3222] 208 | use btc::Sha256d; 209 | impl_object_hash_for_binary_value! { Sha256d } 210 | -------------------------------------------------------------------------------- /src/btc/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Collection of wrappers for the rust-bitcoin crate. 16 | 17 | pub use btc_transaction_utils::test_data::{secp_gen_keypair, secp_gen_keypair_with_rng}; 18 | 19 | pub use self::{ 20 | payload::Payload, 21 | transaction::{BtcAnchoringTransactionBuilder, BuilderError, Transaction}, 22 | }; 23 | 24 | use bitcoin::{network::constants::Network, util::address}; 25 | use bitcoin_hashes::sha256d; 26 | use derive_more::{Display, From, FromStr, Into}; 27 | use exonum_merkledb::{BinaryValue, ObjectHash}; 28 | use hex::{self, FromHex, ToHex}; 29 | use rand::Rng; 30 | use serde_derive::{Deserialize, Serialize}; 31 | 32 | #[macro_use] 33 | mod macros; 34 | 35 | pub(crate) mod payload; 36 | pub(crate) mod transaction; 37 | 38 | /// Bitcoin ECDSA private key wrapper. 39 | #[derive(Clone, From, Into, PartialEq, Eq)] 40 | pub struct PrivateKey(pub bitcoin::PrivateKey); 41 | 42 | /// Secp256k1 public key wrapper, used for verification of signatures. 43 | #[derive(Debug, Clone, Copy, From, Into, PartialEq, Eq, PartialOrd, Ord, Hash, Display, FromStr)] 44 | pub struct PublicKey(pub bitcoin::PublicKey); 45 | 46 | /// Bitcoin address wrapper. 47 | #[derive(Debug, Clone, From, Into, PartialEq, Eq, PartialOrd, Ord, Hash, Display, FromStr)] 48 | pub struct Address(pub address::Address); 49 | 50 | /// Bitcoin input signature wrapper. 51 | #[derive(Debug, Clone, PartialEq, Into, From)] 52 | pub struct InputSignature(pub btc_transaction_utils::InputSignature); 53 | 54 | /// Bitcoin SHA256d hash. 55 | #[derive( 56 | Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Into, From, Serialize, Deserialize, Display, 57 | )] 58 | pub struct Sha256d(pub sha256d::Hash); 59 | 60 | impl ToString for PrivateKey { 61 | fn to_string(&self) -> String { 62 | self.0.to_string() 63 | } 64 | } 65 | 66 | impl std::str::FromStr for PrivateKey { 67 | type Err = ::Err; 68 | 69 | fn from_str(s: &str) -> Result { 70 | bitcoin::PrivateKey::from_str(s).map(From::from) 71 | } 72 | } 73 | 74 | impl std::fmt::Debug for PrivateKey { 75 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 76 | f.debug_struct("PrivateKey").finish() 77 | } 78 | } 79 | 80 | impl FromHex for PublicKey { 81 | type Error = anyhow::Error; 82 | 83 | fn from_hex>(hex: T) -> Result { 84 | let bytes = hex::decode(hex)?; 85 | let inner = bitcoin::PublicKey::from_slice(&bytes)?; 86 | Ok(Self(inner)) 87 | } 88 | } 89 | 90 | impl ToHex for PublicKey { 91 | fn encode_hex>(&self) -> T { 92 | let mut bytes = Vec::default(); 93 | self.0.write_into(&mut bytes); 94 | bytes.encode_hex() 95 | } 96 | 97 | fn encode_hex_upper>(&self) -> T { 98 | let mut bytes = Vec::default(); 99 | self.0.write_into(&mut bytes); 100 | bytes.encode_hex_upper() 101 | } 102 | } 103 | 104 | impl BinaryValue for PublicKey { 105 | fn to_bytes(&self) -> Vec { 106 | let mut bytes = Vec::default(); 107 | self.0.write_into(&mut bytes); 108 | bytes 109 | } 110 | 111 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> anyhow::Result { 112 | bitcoin::PublicKey::from_slice(bytes.as_ref()) 113 | .map(Self) 114 | .map_err(From::from) 115 | } 116 | } 117 | 118 | impl ObjectHash for PublicKey { 119 | fn object_hash(&self) -> exonum::crypto::Hash { 120 | exonum::crypto::hash(&self.to_bytes()) 121 | } 122 | } 123 | 124 | impl AsRef for Address { 125 | fn as_ref(&self) -> &bitcoin::Address { 126 | &self.0 127 | } 128 | } 129 | 130 | impl FromHex for InputSignature { 131 | type Error = anyhow::Error; 132 | 133 | fn from_hex>(hex: T) -> Result { 134 | let bytes = hex::decode(hex)?; 135 | let inner = btc_transaction_utils::InputSignature::from_bytes(bytes)?; 136 | Ok(Self(inner)) 137 | } 138 | } 139 | 140 | impl ToHex for InputSignature { 141 | fn encode_hex>(&self) -> T { 142 | self.0.as_ref().encode_hex() 143 | } 144 | 145 | fn encode_hex_upper>(&self) -> T { 146 | self.0.as_ref().encode_hex_upper() 147 | } 148 | } 149 | 150 | impl AsRef for InputSignature { 151 | fn as_ref(&self) -> &btc_transaction_utils::InputSignature { 152 | &self.0 153 | } 154 | } 155 | 156 | impl From for Vec { 157 | fn from(f: InputSignature) -> Self { 158 | f.0.into() 159 | } 160 | } 161 | 162 | impl BinaryValue for InputSignature { 163 | fn to_bytes(&self) -> Vec { 164 | self.0.clone().into() 165 | } 166 | 167 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> anyhow::Result { 168 | btc_transaction_utils::InputSignature::from_bytes(bytes.into()) 169 | .map(Self) 170 | .map_err(From::from) 171 | } 172 | } 173 | 174 | impl ObjectHash for InputSignature { 175 | fn object_hash(&self) -> exonum::crypto::Hash { 176 | exonum::crypto::hash(&self.to_bytes()) 177 | } 178 | } 179 | 180 | impl Sha256d { 181 | pub(crate) const LEN: usize = ::LEN; 182 | 183 | /// Creates a new instance from bytes array. 184 | pub fn new(bytes: [u8; Self::LEN]) -> Self { 185 | Self::from_slice(&bytes).unwrap() 186 | } 187 | 188 | /// Creates a new instance from bytes slice. 189 | pub fn from_slice(slice: &[u8]) -> Option { 190 | use bitcoin_hashes::Hash; 191 | sha256d::Hash::from_slice(slice).ok().map(Self) 192 | } 193 | } 194 | 195 | impl AsRef for Sha256d { 196 | fn as_ref(&self) -> &bitcoin_hashes::sha256d::Hash { 197 | &self.0 198 | } 199 | } 200 | 201 | impl From for Sha256d { 202 | fn from(txid: bitcoin::hash_types::Txid) -> Self { 203 | Self(txid.into()) 204 | } 205 | } 206 | 207 | impl From for bitcoin::hash_types::Txid { 208 | fn from(hash: Sha256d) -> Self { 209 | hash.0.into() 210 | } 211 | } 212 | 213 | impl_string_conversions_for_hex! { InputSignature } 214 | 215 | impl_serde_str! { PrivateKey } 216 | impl_serde_str! { PublicKey } 217 | impl_serde_str! { Address } 218 | impl_serde_str! { InputSignature } 219 | 220 | /// Generates Bitcoin keypair using the given random number generator. 221 | pub fn gen_keypair_with_rng( 222 | rng: &mut R, 223 | network: Network, 224 | ) -> (PublicKey, PrivateKey) { 225 | let (pk, sk) = secp_gen_keypair_with_rng(rng, network); 226 | (PublicKey(pk), PrivateKey(sk)) 227 | } 228 | 229 | /// Same as [`gen_keypair_with_rng`](fn.gen_keypair_with_rng.html) 230 | /// but it uses a default random number generator. 231 | pub fn gen_keypair(network: Network) -> (PublicKey, PrivateKey) { 232 | let (pk, sk) = secp_gen_keypair(network); 233 | (PublicKey(pk), PrivateKey(sk)) 234 | } 235 | -------------------------------------------------------------------------------- /guides/newbie.md: -------------------------------------------------------------------------------- 1 | # A Complete Beginner Guide to BTC Anchoring 2 | 3 | This manual is intended for people who are about to use `exonum-btc-anchoring` for the first time. 4 | 5 | The manual describes the process of compiling and launching the `btc_anchoring` example within the 6 | `run-dev` mode on Linux. 7 | 8 | ## Step 1. Preparing `bitcoind` 9 | 10 | - Download `bitcoind`: [https://bitcoin.org/en/download](https://bitcoin.org/en/download). 11 | - Install `bitcoind`: 12 | 13 | ```sh 14 | tar xzf bitcoin-0.18.0-x86_64-linux-gnu.tar.gz 15 | sudo install -m 0755 -o root -g root -t /usr/local/bin bitcoin-0.18.0/bin/* 16 | ``` 17 | 18 | - Create a Bitcoin config file: `~/.bitcoin/bitcoin.conf` with the following content: 19 | 20 | ```sh 21 | server=1 22 | regtest=1 23 | txindex=1 24 | rpcuser=user 25 | rpcpassword=password 26 | ``` 27 | 28 | - Run the `bitcoind` in the daemon mode: 29 | 30 | ```sh 31 | bitcoind --daemon 32 | ``` 33 | 34 | - Now `bitcoind` is running in the `regtest` mode. Mine some blocks to obtain balance on 35 | your account. 36 | 37 | Get the address of your wallet: 38 | 39 | ```sh 40 | bitcoin-cli -regtest getnewaddress 41 | ``` 42 | 43 | Mine 100 blocks (change the address to the address you have obtained with the previous command): 44 | 45 | ```sh 46 | bitcoin-cli -regtest generatetoaddress 100 2NFNp5RbTyEwV8yijYg9sUCHsVApiqov8DA 47 | ``` 48 | 49 | Verify that the balance is now non-zero: 50 | 51 | ```sh 52 | bitcoin-cli -regtest getbalance 53 | ``` 54 | 55 | If the balance is still zero, generate 100 more blocks. 56 | 57 | ## Step 2. Compilation and Initial Run 58 | 59 | - Be sure to have [`exonum_launcher`](https://github.com/exonum/exonum-launcher) 60 | installed via `pip3` (see `exonum-launcher` README for details). 61 | - Install `exonum_btc_anchoring_plugin` 62 | (if you are using `venv`, activate `venv` in which `exonum_launcher` is installed): 63 | 64 | ```sh 65 | pip3 install -e exonum-btc-anchoring/launcher 66 | ``` 67 | 68 | - Run the example: 69 | 70 | ```sh 71 | RUST_LOG="exonum_btc_anchoring=info" cargo run --example btc_anchoring run-dev -a target/anchoring 72 | ``` 73 | 74 | `-a target/anchoring` here denotes the directory in which the data of the node will be generated. 75 | 76 | ## Step 3. Deploying and Running 77 | 78 | - First of all, obtain `service_key` and `bitcoin_key`. 79 | 80 | To obtain `service_key`, go to the directory where the data of the node lies 81 | (in our example it is `anchoring`) and open `node.toml`. 82 | Here you can find `service_key`. 83 | 84 | To obtain `bitcoin_key`, go to the `exonum-btc-anchoring` directory and launch the following command: 85 | 86 | ```sh 87 | cargo run --example btc_anchoring_sync generate-config -o target/anchoring/sync.toml --bitcoin-rpc-host http://localhost:18332 --bitcoin-rpc-user user --bitcoin-rpc-password password 88 | ``` 89 | 90 | In the code above you should replace `target/anchoring` with the directory where the data of 91 | your node lies. 92 | 93 | As a result of this call you will obtain `bitcoin_key`. 94 | - Create file `anchoring.yml` with the following contents: 95 | 96 | ```yaml 97 | plugins: 98 | runtime: {} 99 | artifact: 100 | anchoring: "exonum_btc_anchoring_plugin.AnchoringInstanceSpecLoader" 101 | 102 | networks: 103 | - host: "127.0.0.1" 104 | ssl: false 105 | public-api-port: 8080 106 | private-api-port: 8081 107 | 108 | deadline_height: 10000 109 | 110 | artifacts: 111 | anchoring: 112 | runtime: rust 113 | name: "exonum-btc-anchoring" 114 | version: "1.0.0" 115 | 116 | instances: 117 | anchoring: 118 | artifact: anchoring 119 | config: 120 | network: testnet 121 | anchoring_interval: 500 122 | transaction_fee: 10 123 | anchoring_keys: 124 | - bitcoin_key: "02d6086aaccc86e6a711ac84ff21a266684c17d188aa7c4eeab0c0f12133308584" 125 | service_key: "850eb20eebe0b07cf2721ecc9c90aa465a96413dccafad11045a9cb8abf04ed0" 126 | ``` 127 | 128 | Replace `bitcoin_key` and `service_key` with values obtained in the previous step. 129 | - Run `exonum_launcher` to start & deploy the instance: 130 | 131 | ```sh 132 | python3 -m exonum_launcher -i anchoring.yml 133 | ``` 134 | 135 | If everything was done correctly, service should start successfully. 136 | 137 | Enabling anchoring is a separate step. We will describe it below. 138 | 139 | ## Step 4. Enabling Anchoring 140 | 141 | - To get anchoring working, send some bitcoins to the anchoring node and 142 | setup a funding transaction. 143 | 144 | First of all, obtain the address of the anchoring wallet: 145 | 146 | ```sh 147 | curl 'http://127.0.0.1:8080/api/services/anchoring/address/actual' 148 | ``` 149 | 150 | - Then send some bitcoins to that address and obtain the raw transaction 151 | (replace the address with the obtained one): 152 | 153 | ```sh 154 | bitcoin-cli -regtest sendtoaddress bcrt1qn9vu0xjpvauyvd3j5zs3vn3393vh8pjahj06qwxxnly7ttm3u09qhpexa8 200.00 155 | ``` 156 | 157 | After invoking this method you will obtain the transaction hash. Convert it into a raw transaction 158 | (replace the hash with the obtained one): 159 | 160 | ```sh 161 | bitcoin-cli -regtest getrawtransaction 2c2faad68e056608c1f8a3cc8b5da0ca8f8846c42bc5e7152bff786882342b76 162 | ``` 163 | 164 | - Send this transaction to the anchoring node (replace the data with the obtained raw transaction): 165 | 166 | ```sh 167 | curl --header "Content-Type: application/json" \ 168 | --request POST \ 169 | --data '"0200000000010151a7dcd1c2829f9c0a93ae6b054e9777528e88e3e0403c4313cf8cf41b27d1730000000000feffffff0240420f0000000000220020f86c30b7ec3496572220f40b21096b74dc5182942b8811d1bb0b3ab21e52b1337007360000000000160014e16cbf1202193f7de0eb058e0dc2b57cbc63d4040247304402203e23349dcda80acc85e94ada52269baf09624afeb794b696fb53f0f37d130f850220599eaa9bb50d5e14269228f4f5d63826d5554275877b5ffd77eca3cd3b1c408e012102604e1c50f8bdaec165e0bc7b81e608709f510c5bf4b18b6aefaf3996317fd9cf77641900"' \ 170 | http://127.0.0.1:8081/api/services/anchoring/add-funds 171 | ``` 172 | 173 | After that step the following information will appear in the log of the example: 174 | 175 | ```sh 176 | [2019-10-17T09:52:51.482127809Z INFO exonum_btc_anchoring::blockchain::transactions] ====== ADD_FUNDS ====== 177 | [2019-10-17T09:52:51.482197097Z INFO exonum_btc_anchoring::blockchain::transactions] txid: 4b252989ed7596bf08107b3a07a5225b3f42db9bd71868d64ca09bab7ebcce89 178 | [2019-10-17T09:52:51.482206185Z INFO exonum_btc_anchoring::blockchain::transactions] balance: 20000000000 179 | ``` 180 | 181 | - Finally, run the sync tool: 182 | 183 | ```sh 184 | cd exonum-btc-anchoring 185 | RUST_LOG="exonum_btc_anchoring=info" cargo run --example btc_anchoring_sync run --config target/anchoring/sync.toml 186 | ``` 187 | 188 | `target/anchoring/` in the code above means the directory where `sync.toml` was generated earlier. 189 | 190 | On the `regtest` it will exit with an error, since blocks should be mined manually. 191 | The log of the example will show that anchoring was made: 192 | 193 | ```sh 194 | [2019-10-17T09:54:22.057856655Z INFO exonum_btc_anchoring::blockchain::transactions] ====== ANCHORING ====== 195 | [2019-10-17T09:54:22.057892594Z INFO exonum_btc_anchoring::blockchain::transactions] txid: 033f2d08720d7774e6a92cb6c6a9539d8bcf2a3ed0121555148cbd9cecb8cf0f 196 | [2019-10-17T09:54:22.057897939Z INFO exonum_btc_anchoring::blockchain::transactions] height: 0 197 | [2019-10-17T09:54:22.057903560Z INFO exonum_btc_anchoring::blockchain::transactions] hash: 10617dd0945cc9d0239b3f3cb36ac6fb0df7c23ff2dc0a6b0d0e8d372655c790 198 | [2019-10-17T09:54:22.057908057Z INFO exonum_btc_anchoring::blockchain::transactions] balance: 19999998470 199 | ``` 200 | 201 | Hooray! 202 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! BTC anchoring configuration data types. 16 | 17 | pub use crate::proto::{AnchoringKeys, Config}; 18 | 19 | use anyhow::ensure; 20 | use bitcoin::network::constants::Network; 21 | use btc_transaction_utils::{ 22 | multisig::{RedeemScript, RedeemScriptBuilder, RedeemScriptError}, 23 | p2wsh, 24 | }; 25 | use exonum::{ 26 | crypto::PublicKey, 27 | helpers::{Height, ValidateInput}, 28 | }; 29 | 30 | use crate::btc::{self, Address}; 31 | 32 | impl Default for Config { 33 | fn default() -> Self { 34 | Self { 35 | network: Network::Testnet, 36 | anchoring_keys: vec![], 37 | anchoring_interval: 5_000, 38 | transaction_fee: 10, 39 | } 40 | } 41 | } 42 | 43 | impl Config { 44 | /// Current limit on the number of keys in a redeem script on the Bitcoin network. 45 | const MAX_NODES_COUNT: usize = 20; 46 | /// Minimal fee in satoshis for Bitcoin transaction. 47 | const MIN_TOTAL_TX_FEE: u64 = 1000; 48 | /// Minimal total transaction size according to 49 | /// https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending-legacy-non-segwit-p2pkh-p2sh 50 | const MIN_TX_LEN: u64 = 10 + 146 + 33 + 81; 51 | /// Minimal enough transaction fee per byte. 52 | const MIN_TX_FEE: u64 = Self::MIN_TOTAL_TX_FEE / Self::MIN_TX_LEN + 1; // Round up. 53 | 54 | /// Creates Bitcoin anchoring config instance with default parameters for the 55 | /// given Bitcoin network and public keys of participants. 56 | pub fn with_public_keys( 57 | network: Network, 58 | keys: impl IntoIterator, 59 | ) -> Result { 60 | let anchoring_keys = keys.into_iter().collect::>(); 61 | if anchoring_keys.is_empty() { 62 | return Err(RedeemScriptError::NotEnoughPublicKeys); 63 | } 64 | 65 | Ok(Self { 66 | network, 67 | anchoring_keys, 68 | ..Self::default() 69 | }) 70 | } 71 | 72 | /// Tries to find bitcoin public key corresponding with the given service key. 73 | pub fn find_bitcoin_key(&self, service_key: &PublicKey) -> Option<(u16, btc::PublicKey)> { 74 | self.anchoring_keys.iter().enumerate().find_map(|(n, x)| { 75 | if &x.service_key == service_key { 76 | Some((n as u16, x.bitcoin_key)) 77 | } else { 78 | None 79 | } 80 | }) 81 | } 82 | 83 | /// Returns the corresponding Bitcoin address. 84 | pub fn anchoring_address(&self) -> Address { 85 | p2wsh::address(&self.redeem_script(), self.network).into() 86 | } 87 | 88 | /// Returns the corresponding redeem script. 89 | pub fn redeem_script(&self) -> RedeemScript { 90 | RedeemScriptBuilder::with_public_keys(self.anchoring_keys.iter().map(|x| x.bitcoin_key.0)) 91 | .quorum(self.byzantine_quorum()) 92 | .to_script() 93 | .unwrap() 94 | } 95 | 96 | /// Computes the P2WSH output corresponding to the actual redeem script. 97 | pub fn anchoring_out_script(&self) -> bitcoin::Script { 98 | self.redeem_script().as_ref().to_v0_p2wsh() 99 | } 100 | 101 | /// Returns the latest height below the given height which must be anchored. 102 | pub fn previous_anchoring_height(&self, current_height: Height) -> Height { 103 | Height(current_height.0 - current_height.0 % self.anchoring_interval) 104 | } 105 | 106 | /// Returns the nearest height above the given height which must be anchored. 107 | pub fn following_anchoring_height(&self, current_height: Height) -> Height { 108 | Height(self.previous_anchoring_height(current_height).0 + self.anchoring_interval) 109 | } 110 | 111 | /// Returns sufficient number of votes for the given anchoring nodes number. 112 | pub fn byzantine_quorum(&self) -> usize { 113 | exonum::helpers::byzantine_quorum(self.anchoring_keys.len()) 114 | } 115 | } 116 | 117 | impl ValidateInput for Config { 118 | type Error = anyhow::Error; 119 | 120 | fn validate(&self) -> Result<(), Self::Error> { 121 | ensure!( 122 | !self.anchoring_keys.is_empty(), 123 | "The list of anchoring keys must not be empty." 124 | ); 125 | ensure!( 126 | self.anchoring_keys.len() <= Self::MAX_NODES_COUNT, 127 | "Too many anchoring nodes: amount of anchoring nodes should be less or equal than the {}.", 128 | Self::MAX_NODES_COUNT 129 | ); 130 | ensure!( 131 | self.anchoring_interval > 0, 132 | "Anchoring interval should be greater than zero." 133 | ); 134 | ensure!( 135 | self.transaction_fee >= Self::MIN_TX_FEE, 136 | "Transaction fee should be greater than {}", 137 | Self::MIN_TX_FEE 138 | ); 139 | 140 | // Verify that the redeem script is suitable. 141 | RedeemScriptBuilder::with_public_keys(self.anchoring_keys.iter().map(|x| x.bitcoin_key.0)) 142 | .quorum(self.byzantine_quorum()) 143 | .to_script()?; 144 | Ok(()) 145 | } 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | use exonum::{ 151 | crypto, 152 | helpers::{Height, ValidateInput}, 153 | }; 154 | 155 | use bitcoin::network::constants::Network; 156 | use btc_transaction_utils::test_data::secp_gen_keypair; 157 | 158 | use crate::proto::AnchoringKeys; 159 | 160 | use super::Config; 161 | 162 | fn gen_anchoring_keys(network: bitcoin::Network, count: usize) -> Vec { 163 | (0..count) 164 | .map(|_| AnchoringKeys { 165 | bitcoin_key: secp_gen_keypair(network).0.into(), 166 | service_key: crypto::gen_keypair().0, 167 | }) 168 | .collect::>() 169 | } 170 | 171 | #[test] 172 | fn config_serde() { 173 | let public_keys = gen_anchoring_keys(Network::Bitcoin, 4); 174 | 175 | let config = Config::with_public_keys(Network::Bitcoin, public_keys).unwrap(); 176 | assert_eq!(config.redeem_script().content().quorum, 3); 177 | 178 | let json = serde_json::to_value(&config).unwrap(); 179 | let config2: Config = serde_json::from_value(json).unwrap(); 180 | assert_eq!(config2, config); 181 | } 182 | 183 | #[test] 184 | fn config_anchoring_height() { 185 | let public_keys = gen_anchoring_keys(Network::Bitcoin, 4); 186 | 187 | let mut config = Config::with_public_keys(Network::Bitcoin, public_keys).unwrap(); 188 | config.anchoring_interval = 1000; 189 | 190 | assert_eq!(config.previous_anchoring_height(Height(0)), Height(0)); 191 | assert_eq!(config.previous_anchoring_height(Height(999)), Height(0)); 192 | assert_eq!(config.previous_anchoring_height(Height(1000)), Height(1000)); 193 | assert_eq!(config.previous_anchoring_height(Height(1001)), Height(1000)); 194 | 195 | assert_eq!(config.following_anchoring_height(Height(0)), Height(1000)); 196 | assert_eq!(config.following_anchoring_height(Height(999)), Height(1000)); 197 | assert_eq!( 198 | config.following_anchoring_height(Height(1000)), 199 | Height(2000) 200 | ); 201 | assert_eq!( 202 | config.following_anchoring_height(Height(1001)), 203 | Height(2000) 204 | ); 205 | } 206 | 207 | // TODO test validation of the Bitcoin anchoring config 208 | 209 | #[test] 210 | fn config_validate_errors() { 211 | let test_cases = [ 212 | ( 213 | Config::default(), 214 | "The list of anchoring keys must not be empty", 215 | ), 216 | ( 217 | Config { 218 | anchoring_keys: gen_anchoring_keys(bitcoin::Network::Regtest, 30), 219 | ..Config::default() 220 | }, 221 | "Too many anchoring nodes: amount of anchoring nodes should be less or equal", 222 | ), 223 | ( 224 | Config { 225 | anchoring_keys: gen_anchoring_keys(bitcoin::Network::Regtest, 4), 226 | anchoring_interval: 0, 227 | ..Config::default() 228 | }, 229 | "Anchoring interval should be greater than zero", 230 | ), 231 | ( 232 | Config { 233 | anchoring_keys: gen_anchoring_keys(bitcoin::Network::Regtest, 4), 234 | transaction_fee: 0, 235 | ..Config::default() 236 | }, 237 | "Transaction fee should be greater than", 238 | ), 239 | ( 240 | Config { 241 | anchoring_keys: gen_anchoring_keys(bitcoin::Network::Regtest, 4), 242 | transaction_fee: 3, 243 | ..Config::default() 244 | }, 245 | "Transaction fee should be greater than", 246 | ), 247 | ]; 248 | 249 | for (config, expected_err) in &test_cases { 250 | let actual_err = config.validate().unwrap_err().to_string(); 251 | assert!(actual_err.contains(expected_err), actual_err); 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ## 1.0.0 - 2020-03-31 11 | 12 | - First stable release (#159) 13 | 14 | ## 1.0.0-rc.3 - 2020-03-25 15 | 16 | - Third release candidate (#158) 17 | 18 | ## 1.0.0-rc.2 - 2020-03-16 19 | 20 | ### Breaking changes 21 | 22 | - Updated to the [Exonum 1.0.0-rc.2](https://github.com/exonum/exonum/releases/tag/v1.0.0-rc.2) 23 | release with some major changes (#156). 24 | 25 | - `api::TransactionProof` layout has been simplified. 26 | - All methods of the following traits became async: 27 | `api::PublicApi`, `api::PrivateApi`, `sync::BitcoinRelay`. 28 | - Public methods of the `sync::AnchoringChainUpdateTask` and `sync::SyncWithBitcoinTask` 29 | also became async. You need to use these methods inside the future executor like 30 | `tokio` or `actix_rt`. 31 | - `btc_anchoring_sync` utility has been moved to examples. 32 | 33 | ## 1.0.0-rc.1 - 2020-02-13 34 | 35 | ### Breaking changes 36 | 37 | - Updated to the [Exonum 1.0.0-rc.1](https://github.com/exonum/exonum/releases/tag/v1.0.0-rc.1) 38 | release with some minor changes (#155). 39 | 40 | - `api::TransactionProof` layout has been refined. 41 | 42 | ## 0.13.0-rc.2 - 2019-12-04 43 | 44 | ### Breaking changes 45 | 46 | - Ported `exonum-btc-anchoring` to a new version of Exonum with support 47 | of dynamic services. (#145) 48 | 49 | In this way, a large number of changes have occurred in service business logic: 50 | 51 | - Nodes performing anchoring are no longer strictly associated with the 52 | validator nodes. It means that there may exist a validator node that does not 53 | perform anchoring, and vice versa an anchoring node that is not a validator. 54 | But we strongly recommend to keep one to one relationship between the 55 | anchoring and validator nodes. 56 | - The bootstrapping procedure has been completely changed in accordance with 57 | the fact that the service can be started at any time during the blockchain 58 | life. The implementation of the service has become stateless and all logic 59 | that previously was performed in the `after_commit` method was taken out 60 | in a separate `btc_anchoring_sync` utility. 61 | TODO Reference to the description in the README.md. 62 | - "v1" prefix has been removed from API endpoints and introduced a lot of 63 | private API endpoints for the `btc_anchoring_sync` util. 64 | - Removed cryptographic proofs for Exonum blocks feature. 65 | It will be implemented as separate service. 66 | - Funding transaction entry in config has been replaced by the `add-funds` 67 | endpoint in the anchoring private API. This means that you no longer need 68 | to use the configuration update procedure in order to add a new funding 69 | transaction. Now there is a simpler voting procedure with the help of the 70 | `add-funds` endpoint. A qualified majority of nodes (`2/3n+1`) just have 71 | to send the same transaction so that it is used as a funding one. 72 | 73 | ### Internal improvements 74 | 75 | - `exonum_bitcoinrpc` crate has been replaced in favor of `bitcoincore-rpc`. (#145) 76 | 77 | ## 0.12.0 - 2018-08-14 78 | 79 | - Updated to the [Exonum 0.12.0](https://github.com/exonum/exonum/releases/tag/v0.12) 80 | release (#144). 81 | 82 | ## 0.11.0 - 2018-03-15 83 | 84 | ### Internal improvements 85 | 86 | - Updated to the `Rust-bitcoin 0.17` release. (#142) 87 | - Crate has been updated to Rust 2018 edition. This means that it is required 88 | to use Rust 1.31 or newer for compilation. (#142) 89 | 90 | ## 0.10.0 - 2018-12-14 91 | 92 | ### Internal improvements 93 | 94 | - Updated to the `Rust-bitcoin 0.14` release (#134). 95 | 96 | ### Breaking changes 97 | 98 | - New anchoring implementation is based on native segwit transactions. Acceptance of an 99 | anchoring transaction still requires the majority of votes of the validators - 100 | `2/3n+1`. As the votes are now separated from the transaction body, they do not 101 | affect transaction hash and, correspondingly, its ID. (#136) 102 | 103 | In this way: 104 | 105 | - Anchoring transactions have become fully deterministic. The problem of transaction 106 | [malleability]( https://en.wikipedia.org/wiki/Malleability_(cryptography)) has, 107 | thus, been solved. The validators do not have to agree on the latest Exonum 108 | transaction anchored to Bitcoin blockchain any more to continue the anchoring 109 | chain. 110 | - The service can extract the anchoring chain or information on a particular 111 | anchoring transaction from its database any time by a simple API request. It does 112 | not need to use a separate observer functionality any more to extract information 113 | on the latest Exonum anchoring transaction from Bitcoin blockchain and rebuild the 114 | anchoring chain from of this transaction. 115 | - There is no need to connect each Exonum node to the `bitcoind`. The anchoring 116 | transactions are generated deterministically and independently from the connection 117 | to the Bitcoin blockchain. New anchoring transactions are monitored by a separate 118 | method and forwarded to the Bitcoin blockchain whenever they occur. 119 | 120 | For more details on the updated anchoring service operation you can visit the 121 | [readme](README.md) page. 122 | 123 | ## 0.9.0 - 2018-07-20 124 | 125 | ### Internal improvements 126 | 127 | - Anchoring transaction in memory pool now is considering as transaction with the `Some(0)` 128 | confirmations instead of `Null`. (#133) 129 | 130 | - Log level for "Insufficient funds" errors reduced from `error` to `trace`. (#133) 131 | 132 | ### Breaking changes 133 | 134 | - The anchoring chain observer logic has been moved to the `before_commit` stage. (#131) 135 | 136 | Thus additional thread in the public api handler has been no longer used. 137 | Thus now `anchoring-observer-check-interval` is measured in blocks instead of milliseconds. 138 | 139 | - The anchoring API has been ported to the new `actix-web` backend. (#132) 140 | 141 | Some of API endpoints have been changed, you can see updated API description in 142 | the [documentation](https://exonum.com/doc/advanced/bitcoin-anchoring/#available-api). 143 | 144 | ### Internal improvements 145 | 146 | - Added check that funding transaction in `anchoring-funding-txid` contains 147 | output to the anchoring address. (#130) 148 | 149 | ## 0.8.1 - 2018-06-06 150 | 151 | ### Internal improvements 152 | 153 | - Changed `btc::Network` (de)serializing into/from string (#128). 154 | 155 | - Updated to the `Rust-bitcoin 0.13.1` release (#128). 156 | 157 | ## 0.8 - 2018-06-01 158 | 159 | ### Breaking changes 160 | 161 | - The anchoring service has been switched to using p2wsh address format (#123). 162 | 163 | It now uses segwit addresses.... 164 | This change increases the limit on the number of validators and anchoring security 165 | as well as reduces fees for applying thereof. 166 | 167 | Note that the old format of anchoring transactions is incompatible with the new one. 168 | Hence, update of the existing blockchain to the new anchoring version is not possible. 169 | For use of a new anchoring format a new blockchain has to be launched. 170 | 171 | ### New features 172 | 173 | - Introduced a new API method `/v1/block_header_proof/:height` that provides cryptographic 174 | proofs for Exonum blocks including those anchored to Bitcoin blockchain. 175 | The proof is an apparent evidence of availability of a certain Exonum block 176 | in the blockchain (#124). 177 | 178 | ### Internal improvements 179 | 180 | - Updated to the [Rust-bitcoin 0.13](https://github.com/rust-bitcoin/rust-bitcoin/releases/tag/0.13) 181 | release (#123). 182 | 183 | ### Fixed 184 | 185 | - Fixed bug with the `nearest_lect` endpoint that sometimes didn't return actual data [ECR-1387] (#125). 186 | 187 | ## 0.7 - 2018-04-11 188 | 189 | ### Internal improvements 190 | 191 | - Updated to the [Rust-bitcoin 0.12](https://github.com/rust-bitcoin/rust-bitcoin/releases/tag/0.12) 192 | release (#122). 193 | 194 | - Updated to the [Exonum 0.7](https://github.com/exonum/exonum/releases/tag/v0.7) 195 | release (#122). 196 | 197 | ### Fixed 198 | 199 | - Fixed an issue with the identifiers of funding transactions with the witness data [ECR-1220] 200 | (#122). 201 | 202 | ## 0.6.1 - 2018-03-22 203 | 204 | ### Fixed 205 | 206 | - Fix txid for transactions with the witness data [ECR-986] (#119). 207 | 208 | Txid for transactions should be always computed without witness data. 209 | 210 | ### Internal improvements 211 | 212 | - Implement `Display` for the wrapped bitcoin types (#119). 213 | 214 | ## 0.6 - 2018-03-06 215 | 216 | ### Breaking changes 217 | 218 | - The `network` parameter became named (#114). 219 | 220 | Now, to generate template config, run the following command: 221 | 222 | ```shell 223 | anchoring generate-template ... 224 | --anchoring-network 225 | ``` 226 | 227 | ### Internal improvements 228 | 229 | - Error types now use `failure` instead of `derive-error`, 230 | which makes error messages more human-readable (#115). 231 | 232 | - Implemented error codes for incorrect anchoring messages (#117). 233 | 234 | - Updated to the [Exonum 0.6.0](https://github.com/exonum/exonum/releases/tag/v0.6) 235 | release (#117). 236 | 237 | ## 0.5 - 2018-01-30 238 | 239 | ### Changed 240 | 241 | - Update to the [Exonum 0.5.0](https://github.com/exonum/exonum/releases/tag/v0.5) 242 | release (#112). 243 | 244 | ## 0.4 - 2017-12-08 245 | 246 | ### Added 247 | 248 | - Added tests written on `exonum-testkit` (#101). 249 | 250 | ### Changed 251 | 252 | - Update to the [Exonum 0.4.0](https://github.com/exonum/exonum/releases/tag/v0.4) 253 | release (#104). 254 | 255 | ### Removed 256 | 257 | - Sandbox tests are removed (#101). 258 | 259 | ## 0.3.0 - 2017-11-03 260 | 261 | - Update to the [Exonum 0.3.0](https://github.com/exonum/exonum/releases/tag/v0.3) 262 | release (#93). 263 | 264 | ## 0.2.1 - 2017-10-13 265 | 266 | ### Fixed 267 | 268 | - Do not emit panic if lect does not found in bitcoin blockchain (#88). 269 | 270 | ## 0.2 - 2017-09-14 271 | 272 | ### Added 273 | 274 | - Add `anchoring-observer-check-interval` to clap fabric (#85). 275 | 276 | ### Changed 277 | 278 | - Run rpc tests only if the `rpc_tests` feature enabled (#84). 279 | - Update anchoring chain observer configuration layout (#85). 280 | 281 | ### Fixed 282 | 283 | - Fix typo in documentation (#83). 284 | 285 | ## 0.1 - 2017-07-17 286 | 287 | The first release of Exonum btc anchoring service. 288 | -------------------------------------------------------------------------------- /src/blockchain/transactions.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! BTC anchoring transactions. 16 | 17 | pub use crate::proto::{AddFunds, SignInput}; 18 | 19 | use btc_transaction_utils::{p2wsh::InputSigner, TxInRef}; 20 | use exonum::runtime::{CommonError, ExecutionError, ExecutionFail}; 21 | use exonum_derive::{exonum_interface, interface_method}; 22 | use exonum_rust_runtime::ExecutionContext; 23 | use log::{info, trace}; 24 | 25 | use crate::{btc, config::Config, BtcAnchoringService}; 26 | 27 | use super::{ 28 | data_layout::TxInputId, 29 | errors::Error, 30 | schema::{InputSignatures, Schema, TransactionConfirmations}, 31 | }; 32 | 33 | impl SignInput { 34 | // Check that input signature is correct. 35 | fn verify_signature( 36 | &self, 37 | input_signer: &InputSigner, 38 | public_key: &btc::PublicKey, 39 | proposal: &btc::Transaction, 40 | inputs: &[btc::Transaction], 41 | ) -> Result<(), ExecutionError> { 42 | // Check that input with the specified index exist. 43 | let input_transaction = inputs.get(self.input as usize).ok_or(Error::NoSuchInput)?; 44 | input_signer 45 | .verify_input( 46 | TxInRef::new(proposal.as_ref(), self.input as usize), 47 | input_transaction.as_ref(), 48 | &public_key.0, 49 | self.input_signature.as_ref(), 50 | ) 51 | .map_err(|e| Error::InputVerificationFailed.with_description(e)) 52 | } 53 | } 54 | 55 | impl InputSignatures { 56 | /// Returns the number of elements in the map. 57 | fn len(&self) -> usize { 58 | self.0.len() 59 | } 60 | 61 | /// Inserts a key-value pair into the map. 62 | fn insert(&mut self, id: u16, signature: btc::InputSignature) { 63 | self.0.insert(id, signature); 64 | } 65 | 66 | /// Gets an iterator over the values of the map, in order by key. 67 | fn values<'a>( 68 | &'a self, 69 | ) -> impl IntoIterator + 'a { 70 | self.0.values().map(|x| x.0.clone()) 71 | } 72 | } 73 | 74 | impl TransactionConfirmations { 75 | /// Adds confirmation from the specified anchoring node. 76 | fn confirm_by_node(&mut self, public_key: btc::PublicKey) { 77 | self.0.insert(public_key, ()); 78 | } 79 | 80 | /// Checks if there are enough confirmations to mark transaction as funding. 81 | pub(crate) fn has_enough_confirmations(&self, config: &Config) -> Result { 82 | let confirmations = self.0.len(); 83 | Ok(confirmations == config.byzantine_quorum()) 84 | } 85 | } 86 | 87 | /// Exonum BTC anchoring transactions. 88 | #[exonum_interface] 89 | pub trait BtcAnchoringInterface { 90 | /// Value output by the interface. 91 | type Output; 92 | 93 | /// Signs a single input of the anchoring transaction proposal. 94 | #[interface_method(id = 0)] 95 | fn sign_input(&self, context: Ctx, arg: SignInput) -> Self::Output; 96 | /// Add funds via suitable funding transaction. 97 | /// 98 | /// Bitcoin transaction should have output with value to the current anchoring address. 99 | /// The transaction will be applied if 2/3+1 anchoring nodes sent it. 100 | #[interface_method(id = 1)] 101 | fn add_funds(&self, context: Ctx, arg: AddFunds) -> Self::Output; 102 | } 103 | 104 | impl BtcAnchoringInterface> for BtcAnchoringService { 105 | type Output = Result<(), ExecutionError>; 106 | 107 | fn sign_input(&self, context: ExecutionContext<'_>, arg: SignInput) -> Self::Output { 108 | let author = context 109 | .caller() 110 | .author() 111 | .ok_or(CommonError::UnauthorizedCaller)?; 112 | 113 | let mut schema = Schema::new(context.service_data()); 114 | 115 | // Check that author is authorized to sign inputs of the anchoring proposal. 116 | let actual_config = schema.actual_config(); 117 | let (anchoring_node_id, public_key) = actual_config 118 | .find_bitcoin_key(&author) 119 | .ok_or(Error::UnauthorizedAnchoringKey)?; 120 | 121 | // Check that there is an anchoring proposal for the actual blockchain state. 122 | let (proposal, expected_inputs) = if let Some(proposal) = schema 123 | .actual_proposed_anchoring_transaction(context.data().for_core()) 124 | .transpose() 125 | .map_err(Error::anchoring_builder_error)? 126 | { 127 | proposal 128 | } else { 129 | // There is no anchoring request at the current blockchain state. 130 | // Make sure txid is equal to the identifier of the last anchoring transaction. 131 | let latest_anchoring_txid = schema 132 | .transactions_chain 133 | .last() 134 | // If the anchoring chain is not established, then the proposal must exist. 135 | .unwrap() 136 | .id(); 137 | if latest_anchoring_txid == arg.txid { 138 | return Ok(()); 139 | } else { 140 | return Err(Error::UnexpectedProposalTxId.into()); 141 | } 142 | }; 143 | 144 | // Make sure txid is equal to the identifier of the anchoring transaction proposal. 145 | if proposal.id() != arg.txid { 146 | return Err(Error::UnexpectedProposalTxId.into()); 147 | } 148 | 149 | // Check that input signature is correct. 150 | let redeem_script = actual_config.redeem_script(); 151 | let quorum = redeem_script.content().quorum; 152 | let input_signer = InputSigner::new(redeem_script); 153 | arg.verify_signature(&input_signer, &public_key, &proposal, &expected_inputs)?; 154 | 155 | // All preconditions are correct and we can use this signature. 156 | let input_id = TxInputId::new(proposal.id(), arg.input); 157 | let mut input_signatures = schema.input_signatures(&input_id); 158 | let mut input_signature_len = input_signatures.len(); 159 | // Check that we have not reached the quorum yet, otherwise we should not do anything. 160 | if input_signature_len < quorum { 161 | // Add signature to schema. 162 | input_signatures.insert(anchoring_node_id, arg.input_signature); 163 | schema 164 | .transaction_signatures 165 | .put(&input_id, input_signatures); 166 | input_signature_len += 1; 167 | } else { 168 | return Ok(()); 169 | } 170 | 171 | // If we have enough signatures for specific input we have to check that we also have 172 | // sufficient signatures to finalize proposal transaction. 173 | if input_signature_len == quorum { 174 | let mut finalized_tx: btc::Transaction = proposal.clone(); 175 | // Make sure we reach a quorum for each input. 176 | for index in 0..expected_inputs.len() { 177 | let input_id = TxInputId::new(proposal.id(), index as u32); 178 | let signatures_for_input = schema.input_signatures(&input_id); 179 | // We have not enough signatures for this input, so we can not finalize this 180 | // proposal at the moment. 181 | if signatures_for_input.len() != quorum { 182 | return Ok(()); 183 | } 184 | 185 | input_signer.spend_input( 186 | &mut finalized_tx.0.input[index], 187 | signatures_for_input.values(), 188 | ); 189 | } 190 | 191 | let payload = finalized_tx.anchoring_metadata().unwrap().1; 192 | 193 | info!("====== ANCHORING ======"); 194 | info!("txid: {}", finalized_tx.id().to_string()); 195 | info!("height: {}", payload.block_height); 196 | info!("hash: {}", payload.block_hash.to_hex()); 197 | info!("balance: {}", finalized_tx.0.output[0].value); 198 | trace!("Anchoring txhex: {}", finalized_tx.to_string()); 199 | 200 | // Add finalized transaction to the tail of anchoring transactions. 201 | schema.push_anchoring_transaction(finalized_tx); 202 | } 203 | Ok(()) 204 | } 205 | 206 | fn add_funds(&self, context: ExecutionContext<'_>, arg: AddFunds) -> Self::Output { 207 | let author = context 208 | .caller() 209 | .author() 210 | .ok_or(CommonError::UnauthorizedCaller)?; 211 | let mut schema = Schema::new(context.service_data()); 212 | 213 | // Check that author is authorized to sign inputs of the anchoring proposal. 214 | let actual_config = schema.actual_config(); 215 | let (_, public_key) = actual_config 216 | .find_bitcoin_key(&author) 217 | .ok_or(Error::UnauthorizedAnchoringKey)?; 218 | 219 | // Check that the given transaction is suitable. 220 | let (_, txout) = arg 221 | .transaction 222 | .find_out(&actual_config.anchoring_out_script()) 223 | .ok_or(Error::UnsuitableFundingTx)?; 224 | 225 | // Check that the transaction has not been used before 226 | let funding_txid = arg.transaction.id(); 227 | if schema.spent_funding_transactions.contains(&funding_txid) { 228 | return Err(Error::AlreadyUsedFundingTx.into()); 229 | } 230 | 231 | // Add confirmation from this node for this funding transaction. 232 | let mut confirmations = schema 233 | .unconfirmed_funding_transactions 234 | .get(&funding_txid) 235 | .unwrap_or_default(); 236 | confirmations.confirm_by_node(public_key); 237 | 238 | // Set this transaction as unspent funding if there are enough confirmations 239 | // otherwise just write confirmation to the schema. 240 | if confirmations.has_enough_confirmations(&actual_config)? { 241 | info!("====== ADD_FUNDS ======"); 242 | info!("txid: {}", arg.transaction.id().to_string()); 243 | info!("balance: {}", txout.value); 244 | 245 | schema.set_funding_transaction(arg.transaction); 246 | } else { 247 | schema 248 | .unconfirmed_funding_transactions 249 | .put(&funding_txid, confirmations); 250 | } 251 | Ok(()) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/blockchain/schema.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Information schema for the btc anchoring service. 16 | 17 | use exonum::{blockchain::Schema as CoreSchema, helpers::Height}; 18 | use exonum_derive::FromAccess; 19 | use exonum_merkledb::{ 20 | access::{Access, FromAccess, RawAccessMut}, 21 | Entry, ProofListIndex, ProofMapIndex, 22 | }; 23 | use log::{error, trace}; 24 | 25 | use crate::{ 26 | btc::{self, BtcAnchoringTransactionBuilder, BuilderError, Sha256d, Transaction}, 27 | config::Config, 28 | proto::BinaryMap, 29 | }; 30 | 31 | use super::{data_layout::*, BtcAnchoringState}; 32 | 33 | /// A set of signatures for a transaction input ordered by the anchoring node identifiers. 34 | pub type InputSignatures = BinaryMap; 35 | /// A set of funding transaction confirmations. 36 | pub type TransactionConfirmations = BinaryMap; 37 | 38 | /// Information schema for `exonum-btc-anchoring`. 39 | #[derive(Debug, FromAccess)] 40 | pub struct Schema { 41 | /// Complete chain of the anchoring transactions. 42 | pub transactions_chain: ProofListIndex, 43 | /// Already spent funding transactions. 44 | pub(crate) spent_funding_transactions: ProofMapIndex, 45 | /// Signatures for the given transaction input. 46 | pub(crate) transaction_signatures: ProofMapIndex, 47 | /// Actual anchoring configuration entry. 48 | pub(crate) actual_config: Entry, 49 | /// Following anchoring configuration entry. 50 | pub(crate) following_config: Entry, 51 | /// Confirmations for the corresponding funding transaction. 52 | pub(crate) unconfirmed_funding_transactions: 53 | ProofMapIndex, 54 | /// Entry that may contain an unspent funding transaction for the 55 | /// actual configuration. 56 | pub(crate) unspent_funding_transaction: Entry, 57 | } 58 | 59 | impl Schema { 60 | /// Returns a new schema instance. 61 | pub fn new(access: T) -> Self { 62 | Self::from_root(access).unwrap() 63 | } 64 | 65 | /// Returns an actual anchoring configuration. 66 | pub fn actual_config(&self) -> Config { 67 | self.actual_config.get().expect( 68 | "Actual configuration of anchoring is absent. \ 69 | If this error occurs, inform the service authors about it.", 70 | ) 71 | } 72 | 73 | /// Returns the nearest following configuration if it exists. 74 | pub fn following_config(&self) -> Option { 75 | self.following_config.get() 76 | } 77 | 78 | /// Returns the list of signatures for the given transaction input. 79 | pub fn input_signatures(&self, input: &TxInputId) -> InputSignatures { 80 | self.transaction_signatures.get(input).unwrap_or_default() 81 | } 82 | 83 | /// Returns an unspent funding transaction for the actual configurations if it exists. 84 | pub fn unspent_funding_transaction(&self) -> Option { 85 | self.unspent_funding_transaction.get() 86 | } 87 | 88 | /// Returns an actual state of anchoring. 89 | pub fn actual_state(&self) -> BtcAnchoringState { 90 | let actual_configuration = self.actual_config(); 91 | if let Some(following_configuration) = self.following_config() { 92 | if actual_configuration.redeem_script() != following_configuration.redeem_script() { 93 | return BtcAnchoringState::Transition { 94 | actual_configuration, 95 | following_configuration, 96 | }; 97 | } 98 | } 99 | 100 | BtcAnchoringState::Regular { 101 | actual_configuration, 102 | } 103 | } 104 | 105 | /// Returns the proposal of the next anchoring transaction for the given anchoring state. 106 | pub fn proposed_anchoring_transaction( 107 | &self, 108 | core_schema: CoreSchema, 109 | actual_state: &BtcAnchoringState, 110 | ) -> Option), BuilderError>> { 111 | let config = actual_state.actual_config(); 112 | let unspent_anchoring_transaction = self.transactions_chain.last(); 113 | let unspent_funding_transaction = self.unspent_funding_transaction.get(); 114 | 115 | let mut builder = BtcAnchoringTransactionBuilder::new(&config.redeem_script()); 116 | // First anchoring transaction doesn't have previous. 117 | if let Some(tx) = unspent_anchoring_transaction { 118 | let tx_id = tx.id(); 119 | 120 | // Check that latest anchoring transaction isn't a transition. 121 | if actual_state.is_transition() { 122 | let current_script_pubkey = &tx.0.output[0].script_pubkey; 123 | let outgoing_script_pubkey = &actual_state.script_pubkey(); 124 | if current_script_pubkey == outgoing_script_pubkey { 125 | trace!( 126 | "Waiting for the moment when the following configuration \ 127 | becomes actual." 128 | ); 129 | return None; 130 | } else { 131 | trace!( 132 | "Transition from {} to {}.", 133 | actual_state.actual_config().anchoring_address(), 134 | actual_state.output_address(), 135 | ); 136 | builder.transit_to(actual_state.script_pubkey()); 137 | } 138 | } 139 | 140 | // TODO Re-implement recovery business logic [ECR-3581] 141 | if let Err(e) = builder.prev_tx(tx) { 142 | if unspent_funding_transaction.is_none() { 143 | return Some(Err(e)); 144 | } 145 | error!("Anchoring is broken: '{}'. Will try to recover", e); 146 | builder.recover(tx_id); 147 | } 148 | } 149 | 150 | if let Some(tx) = unspent_funding_transaction { 151 | if let Err(e) = builder.additional_funds(tx) { 152 | return Some(Err(e)); 153 | } 154 | } 155 | 156 | // Add corresponding payload. 157 | let latest_anchored_height = self.latest_anchored_height(); 158 | let anchoring_height = actual_state.following_anchoring_height(latest_anchored_height); 159 | let anchoring_block_hash = core_schema.block_hash_by_height(anchoring_height)?; 160 | 161 | builder.payload(anchoring_height, anchoring_block_hash); 162 | builder.fee(config.transaction_fee); 163 | 164 | // Create anchoring proposal. 165 | Some(builder.create()) 166 | } 167 | 168 | /// Returns the proposal of the next anchoring transaction for the actual anchoring state. 169 | pub fn actual_proposed_anchoring_transaction( 170 | &self, 171 | core_schema: CoreSchema, 172 | ) -> Option), BuilderError>> { 173 | let actual_state = self.actual_state(); 174 | self.proposed_anchoring_transaction(core_schema, &actual_state) 175 | } 176 | 177 | /// Returns the height of the latest anchored block. 178 | pub fn latest_anchored_height(&self) -> Option { 179 | let tx = self.transactions_chain.last()?; 180 | Some( 181 | tx.anchoring_metadata() 182 | .expect( 183 | "Expected payload in the anchoring transaction. \ 184 | If this error occurs, inform the service authors about it.", 185 | ) 186 | .1 187 | .block_height, 188 | ) 189 | } 190 | } 191 | 192 | impl Schema 193 | where 194 | T: Access, 195 | T::Base: RawAccessMut, 196 | { 197 | /// Adds a finalized transaction to the tail of the anchoring transactions. 198 | pub(crate) fn push_anchoring_transaction(&mut self, tx: Transaction) { 199 | // An unspent funding transaction is always unconditionally added to the anchoring 200 | // transaction proposal, so we can simply move it to the list of spent. 201 | if let Some(funding_transaction) = self.unspent_funding_transaction.take() { 202 | self.spent_funding_transactions 203 | .put(&funding_transaction.id(), funding_transaction); 204 | } 205 | // Special case if we have an active following configuration. 206 | if let Some(config) = self.following_config() { 207 | // Check that the anchoring transaction is correct. 208 | let tx_out_script = tx 209 | .anchoring_metadata() 210 | .expect( 211 | "Unable to find metadata in the anchoring transaction. \ 212 | If this error occurs, inform the service authors about it.", 213 | ) 214 | .0; 215 | // If there is a following config, then the anchoring transaction's output should have 216 | // same script as in the following config. 217 | // Otherwise, this is a critical error in the logic of the anchoring. 218 | assert_eq!( 219 | config.anchoring_out_script(), 220 | *tx_out_script, 221 | "Malformed output address in the anchoring transaction. \ 222 | If this error occurs, inform the service authors about it." 223 | ); 224 | // If preconditions are correct, just reassign the config as an actual. 225 | self.following_config.remove(); 226 | self.actual_config.set(config); 227 | } 228 | self.transactions_chain.push(tx); 229 | } 230 | 231 | /// Sets the given transaction as the current unspent funding transaction. 232 | pub(crate) fn set_funding_transaction(&mut self, transaction: btc::Transaction) { 233 | debug_assert!( 234 | !self.spent_funding_transactions.contains(&transaction.id()), 235 | "Funding transaction must be unspent." 236 | ); 237 | // Remove confirmations for this transaction to avoid attack of re-setting 238 | // this transaction as funding. 239 | self.unconfirmed_funding_transactions 240 | .put(&transaction.id(), TransactionConfirmations::default()); 241 | self.unspent_funding_transaction.set(transaction); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /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 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/btc/payload.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use exonum::{crypto::Hash, helpers::Height}; 16 | 17 | use bitcoin::blockdata::{ 18 | opcodes::all::OP_RETURN, 19 | script::{Builder, Instruction, Script}, 20 | }; 21 | use byteorder::{ByteOrder, LittleEndian}; 22 | use serde_derive::{Deserialize, Serialize}; 23 | 24 | use super::Sha256d; 25 | 26 | const PAYLOAD_PREFIX: &[u8] = b"EXONUM"; 27 | const PAYLOAD_HEADER_LEN: usize = 8; 28 | const PAYLOAD_V1: u8 = 1; 29 | const PAYLOAD_V1_KIND_REGULAR: u8 = 0; 30 | const PAYLOAD_V1_KIND_RECOVER: u8 = 1; 31 | 32 | /// Anchoring transaction payload. 33 | /// 34 | /// Data layout in `OP_RETURN` script for `Payload` v.1: 35 | /// 36 | /// | Position in bytes | Description | 37 | /// |-----------------------|---------------------------------------------------| 38 | /// | 0..6 | ASCII-encoded prefix `EXONUM` | 39 | /// | 6 | Version byte, currently is 1 | 40 | /// | 7 | Payload kind: (0 is regular, 1 is recover) | 41 | /// | 8..16 | Block height | 42 | /// | 16..48 | Block hash | 43 | /// | 48..80 (Optionally) | Txid of previous tx chain (only for recover kind) | 44 | /// 45 | /// In this way the length of `regular` payload is 48, and for `recover` is 80. 46 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 47 | pub struct Payload { 48 | /// Anchored block height. 49 | pub block_height: Height, 50 | /// Anchored block hash. 51 | pub block_hash: Hash, 52 | /// `Txid` of previous transactions chain if it has been lost. 53 | pub prev_tx_chain: Option, 54 | } 55 | 56 | #[derive(Debug)] 57 | enum PayloadV1 { 58 | Regular(Height, Hash), 59 | Recover(Height, Hash, Sha256d), 60 | } 61 | 62 | #[derive(Debug, Default)] 63 | pub struct PayloadV1Builder { 64 | block_hash: Option, 65 | block_height: Option, 66 | prev_tx_chain: Option, 67 | } 68 | 69 | pub type PayloadBuilder = PayloadV1Builder; 70 | 71 | #[cfg_attr(feature = "cargo-clippy", allow(clippy::len_without_is_empty))] 72 | impl PayloadV1 { 73 | fn read(bytes: &[u8]) -> Option { 74 | let kind = bytes[0]; 75 | let data = &bytes[1..]; 76 | match kind { 77 | PAYLOAD_V1_KIND_REGULAR => { 78 | if data.len() != 40 { 79 | return None; 80 | } 81 | 82 | let block_height = LittleEndian::read_u64(&data[0..8]); 83 | let block_hash = Hash::from_slice(&data[8..40]).unwrap(); 84 | Some(PayloadV1::Regular(Height(block_height), block_hash)) 85 | } 86 | PAYLOAD_V1_KIND_RECOVER => { 87 | if data.len() != 72 { 88 | return None; 89 | } 90 | 91 | let block_height = LittleEndian::read_u64(&data[0..8]); 92 | let block_hash = Hash::from_slice(&data[8..40]).unwrap(); 93 | let txid = Sha256d::from_slice(&data[40..72]).unwrap(); 94 | Some(PayloadV1::Recover(Height(block_height), block_hash, txid)) 95 | } 96 | _ => None, 97 | } 98 | } 99 | 100 | fn write(&self, buf: &mut [u8]) { 101 | let kind = self.kind(); 102 | buf[0] = kind as u8; 103 | 104 | let buf = &mut buf[1..]; 105 | debug_assert_eq!(buf.len(), self.len()); 106 | // Serialize data 107 | match *self { 108 | PayloadV1::Regular(height, hash) => { 109 | LittleEndian::write_u64(&mut buf[0..8], height.0); 110 | buf[8..40].copy_from_slice(hash.as_ref()); 111 | } 112 | PayloadV1::Recover(height, hash, txid) => { 113 | LittleEndian::write_u64(&mut buf[0..8], height.0); 114 | buf[8..40].copy_from_slice(hash.as_ref()); 115 | buf[40..72].copy_from_slice(&txid.0[..]); 116 | } 117 | }; 118 | } 119 | 120 | fn len(&self) -> usize { 121 | match *self { 122 | PayloadV1::Regular(..) => 40, 123 | PayloadV1::Recover(..) => 72, 124 | } 125 | } 126 | 127 | fn kind(&self) -> u8 { 128 | match *self { 129 | PayloadV1::Regular(..) => PAYLOAD_V1_KIND_REGULAR, 130 | PayloadV1::Recover(..) => PAYLOAD_V1_KIND_RECOVER, 131 | } 132 | } 133 | 134 | fn into_script(self) -> Script { 135 | let len = self.len() + PAYLOAD_HEADER_LEN; 136 | let mut buf = vec![0; len]; 137 | // Serialize header 138 | buf[0..6].copy_from_slice(PAYLOAD_PREFIX); 139 | buf[6] = PAYLOAD_V1; 140 | self.write(&mut buf[7..]); 141 | // Build script 142 | Builder::new() 143 | .push_opcode(OP_RETURN) 144 | .push_slice(buf.as_ref()) 145 | .into_script() 146 | } 147 | } 148 | 149 | impl PayloadV1Builder { 150 | pub fn new() -> Self { 151 | Self { 152 | block_hash: None, 153 | block_height: None, 154 | prev_tx_chain: None, 155 | } 156 | } 157 | 158 | pub fn block_height(mut self, height: Height) -> Self { 159 | self.block_height = Some(height); 160 | self 161 | } 162 | 163 | pub fn block_hash(mut self, hash: Hash) -> Self { 164 | self.block_hash = Some(hash); 165 | self 166 | } 167 | 168 | pub fn prev_tx_chain(mut self, txid: Option) -> Self { 169 | self.prev_tx_chain = txid; 170 | self 171 | } 172 | 173 | pub fn into_script(self) -> Script { 174 | let block_height = self.block_height.expect("Block height is not set"); 175 | let block_hash = self.block_hash.expect("Block hash is not set"); 176 | 177 | let payload = match self.prev_tx_chain { 178 | Some(txid) => PayloadV1::Recover(block_height, block_hash, txid), 179 | None => PayloadV1::Regular(block_height, block_hash), 180 | }; 181 | payload.into_script() 182 | } 183 | } 184 | 185 | impl Payload { 186 | /// Tries to extract payload from given `Script`. 187 | pub fn from_script(script: &Script) -> Option { 188 | let mut instructions = script.iter(true); 189 | instructions 190 | .next() 191 | .and_then(|instr| { 192 | if instr == Instruction::Op(OP_RETURN) { 193 | instructions.next() 194 | } else { 195 | None 196 | } 197 | }) 198 | .and_then(|instr| { 199 | if let Instruction::PushBytes(bytes) = instr { 200 | if bytes.len() < PAYLOAD_HEADER_LEN { 201 | return None; 202 | } 203 | if &bytes[0..6] != PAYLOAD_PREFIX { 204 | return None; 205 | } 206 | // Parse metadata 207 | let version = bytes[6]; 208 | match version { 209 | PAYLOAD_V1 => PayloadV1::read(&bytes[7..]).map(Self::from), 210 | _ => None, 211 | } 212 | } else { 213 | None 214 | } 215 | }) 216 | } 217 | } 218 | 219 | impl From for Payload { 220 | fn from(v1: PayloadV1) -> Self { 221 | match v1 { 222 | PayloadV1::Regular(height, hash) => Self { 223 | block_height: height, 224 | block_hash: hash, 225 | prev_tx_chain: None, 226 | }, 227 | PayloadV1::Recover(height, hash, txid) => Self { 228 | block_height: height, 229 | block_hash: hash, 230 | prev_tx_chain: Some(txid), 231 | }, 232 | } 233 | } 234 | } 235 | 236 | #[cfg(test)] 237 | mod tests { 238 | use exonum::crypto::hash; 239 | use exonum::helpers::Height; 240 | 241 | use bitcoin::blockdata::script::Script; 242 | use hex; 243 | 244 | use crate::btc::Sha256d; 245 | 246 | use super::{Payload, PayloadBuilder}; 247 | 248 | trait HexValue { 249 | fn from_hex(hex: impl AsRef<[u8]>) -> Self; 250 | fn to_hex(&self) -> String; 251 | } 252 | 253 | impl HexValue for Script { 254 | fn from_hex(hex: impl AsRef<[u8]>) -> Self { 255 | let bytes = hex::decode(hex).unwrap(); 256 | Self::from(bytes) 257 | } 258 | 259 | fn to_hex(&self) -> String { 260 | let bytes = self[..].to_vec(); 261 | hex::encode(bytes) 262 | } 263 | } 264 | 265 | #[test] 266 | fn test_payload_regular_serialize() { 267 | let block_hash = hash(&[]); 268 | let payload_script = PayloadBuilder::new() 269 | .block_hash(block_hash) 270 | .block_height(Height(1234)) 271 | .into_script(); 272 | 273 | assert_eq!( 274 | payload_script.to_hex(), 275 | "6a3045584f4e554d0100d204000000000000e3b0c44298fc1c149afbf4c8996fb92427ae41e4649\ 276 | b934ca495991b7852b855" 277 | ); 278 | } 279 | 280 | #[test] 281 | fn test_payload_regular_deserialize() { 282 | let payload_script = Script::from_hex( 283 | "6a3045584f4e554d0100d204000000000000e3b0c44298fc1c14\ 284 | 9afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 285 | ); 286 | 287 | let block_hash = hash(&[]); 288 | let payload = Payload::from_script(&payload_script).unwrap(); 289 | assert_eq!(payload.block_hash, block_hash); 290 | assert_eq!(payload.block_height, Height(1234)); 291 | assert_eq!(payload.prev_tx_chain, None); 292 | } 293 | 294 | #[test] 295 | fn test_payload_recover_serizalize() { 296 | let block_hash = hash(&[]); 297 | let prev_txid = Sha256d::from_slice(block_hash.as_ref()).unwrap(); 298 | let payload_script = PayloadBuilder::new() 299 | .block_hash(block_hash) 300 | .block_height(Height(1234)) 301 | .prev_tx_chain(Some(prev_txid)) 302 | .into_script(); 303 | 304 | assert_eq!( 305 | payload_script.to_hex(), 306 | "6a4c5045584f4e554d0101d204000000000000e3b0c44298fc1c149afbf4c8996fb92427ae41e46\ 307 | 49b934ca495991b7852b855e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7\ 308 | 852b855" 309 | ); 310 | } 311 | 312 | #[test] 313 | fn test_payload_recover_deserialize() { 314 | let payload_script = Script::from_hex( 315 | "6a4c5045584f4e554d0101d204000000000000e3b0c44298fc1c\ 316 | 149afbf4c8996fb92427ae41e4649b934ca495991b7852b855e3\ 317 | b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca49599\ 318 | 1b7852b855", 319 | ); 320 | 321 | let block_hash = hash(&[]); 322 | let prev_txid = Sha256d::from_slice(block_hash.as_ref()).unwrap(); 323 | let payload = Payload::from_script(&payload_script).unwrap(); 324 | assert_eq!(payload.block_hash, block_hash); 325 | assert_eq!(payload.block_height, Height(1234)); 326 | assert_eq!(payload.prev_tx_chain, Some(prev_txid)); 327 | } 328 | 329 | #[test] 330 | fn test_payload_incorrect_deserialize() { 331 | // Payload from old anchoring transaction 332 | let payload_script = Script::from_hex( 333 | "6a2a0128f0b31a00000000008fb4879f1b7f332be1aee197f99f\ 334 | 7333c915570c6ad5c6eed641f33fe0199129", 335 | ); 336 | assert_eq!(Payload::from_script(&payload_script), None); 337 | } 338 | 339 | #[test] 340 | fn test_payload_non_op_return() { 341 | // Payload from old anchoring transaction 342 | let script_pubkey = Script::from_hex("a91472b7506704dc074fa46359251052e781d96f939a87"); 343 | assert_eq!(Payload::from_script(&script_pubkey), None); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/sync/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Building blocks of the anchoring sync utility. 16 | 17 | pub use self::bitcoin_relay::{BitcoinRelay, TransactionStatus}; 18 | 19 | use anyhow::anyhow; 20 | use btc_transaction_utils::{p2wsh, TxInRef}; 21 | 22 | use std::{collections::HashMap, fmt::Display, sync::Arc}; 23 | 24 | use crate::{ 25 | api::{AnchoringProposalState, PrivateApi}, 26 | blockchain::SignInput, 27 | btc, 28 | config::Config, 29 | }; 30 | 31 | mod bitcoin_relay; 32 | 33 | /// Anchoring transaction with its index in the anchoring chain. 34 | pub type TransactionWithIndex = (btc::Transaction, u64); 35 | 36 | type KeyPool = Arc>; 37 | 38 | /// Errors that occur when updating the anchoring chain. 39 | #[derive(Debug)] 40 | pub enum ChainUpdateError { 41 | /// Error occurred in the private API client. 42 | Client(C), 43 | /// Insufficient funds to create an anchoring transaction proposal. 44 | InsufficientFunds { 45 | /// Total transaction fee. 46 | total_fee: u64, 47 | /// Available balance. 48 | balance: u64, 49 | }, 50 | /// Initial funding transaction is absent. 51 | NoInitialFunds, 52 | /// Internal error. 53 | Internal(anyhow::Error), 54 | } 55 | 56 | /// Signs the inputs of the anchoring transaction proposal by the corresponding 57 | /// Bitcoin private keys. 58 | #[derive(Debug)] 59 | pub struct AnchoringChainUpdateTask 60 | where 61 | T: PrivateApi + 'static, 62 | { 63 | key_pool: KeyPool, 64 | api_client: T, 65 | } 66 | 67 | impl AnchoringChainUpdateTask 68 | where 69 | T: PrivateApi + 'static, 70 | T::Error: Display, 71 | { 72 | /// Creates a new anchoring chain updater instance. 73 | pub fn new( 74 | keys: impl IntoIterator, 75 | api_client: T, 76 | ) -> Self { 77 | Self { 78 | key_pool: Arc::new(keys.into_iter().collect()), 79 | api_client, 80 | } 81 | } 82 | 83 | /// Returns an actual anchoring configuration. 84 | pub async fn anchoring_config(&self) -> Result { 85 | self.api_client.config().await 86 | } 87 | 88 | /// Performs one attempt to sign an anchoring proposal, if any. 89 | pub async fn process(&self) -> Result<(), ChainUpdateError> { 90 | log::trace!("Perform an anchoring chain update"); 91 | 92 | match self 93 | .api_client 94 | .anchoring_proposal() 95 | .await 96 | .map_err(ChainUpdateError::Client)? 97 | { 98 | AnchoringProposalState::None => Ok(()), 99 | AnchoringProposalState::Available { 100 | transaction, 101 | inputs, 102 | } => { 103 | let config = self 104 | .anchoring_config() 105 | .await 106 | .map_err(ChainUpdateError::Client)?; 107 | self.handle_proposal(config, transaction, inputs).await 108 | } 109 | AnchoringProposalState::InsufficientFunds { balance, total_fee } => { 110 | Err(ChainUpdateError::InsufficientFunds { balance, total_fee }) 111 | } 112 | AnchoringProposalState::NoInitialFunds => Err(ChainUpdateError::NoInitialFunds), 113 | } 114 | } 115 | 116 | async fn handle_proposal( 117 | &self, 118 | config: Config, 119 | proposal: btc::Transaction, 120 | inputs: Vec, 121 | ) -> Result<(), ChainUpdateError> { 122 | log::trace!("Got an anchoring proposal: {:?}", proposal); 123 | // Find among the keys one from which we have a private part. 124 | // TODO What we have to do if we find more than one key? [ECR-3222] 125 | let keypair = if let Some(keypair) = 126 | self.find_private_key(config.anchoring_keys.iter().map(|x| x.bitcoin_key)) 127 | { 128 | keypair 129 | } else { 130 | return Ok(()); 131 | }; 132 | // Create `SignInput` transactions. 133 | let redeem_script = config.redeem_script(); 134 | let block_height = match proposal.anchoring_payload() { 135 | Some(payload) => payload.block_height, 136 | None => { 137 | return Err(ChainUpdateError::Internal(anyhow!( 138 | "Incorrect anchoring proposal found: {:?}", 139 | proposal 140 | ))) 141 | } 142 | }; 143 | 144 | log::info!( 145 | "Found a new unfinished anchoring transaction proposal for height: {}", 146 | block_height 147 | ); 148 | 149 | let mut signer = p2wsh::InputSigner::new(redeem_script); 150 | let sign_input_messages = inputs 151 | .iter() 152 | .enumerate() 153 | .map(|(index, proposal_input)| { 154 | let signature = signer.sign_input( 155 | TxInRef::new(proposal.as_ref(), index), 156 | proposal_input.as_ref(), 157 | &(keypair.1).0.key, 158 | )?; 159 | 160 | Ok(SignInput { 161 | input: index as u32, 162 | input_signature: signature.into(), 163 | txid: proposal.id(), 164 | }) 165 | }) 166 | .collect::>>() 167 | .map_err(ChainUpdateError::Internal)?; 168 | // Send sign input transactions to the Exonum node. 169 | for sign_input in sign_input_messages { 170 | self.api_client 171 | .sign_input(sign_input) 172 | .await 173 | .map_err(ChainUpdateError::Client)?; 174 | } 175 | Ok(()) 176 | } 177 | 178 | fn find_private_key( 179 | &self, 180 | anchoring_keys: impl IntoIterator, 181 | ) -> Option<(btc::PublicKey, btc::PrivateKey)> { 182 | anchoring_keys.into_iter().find_map(|public_key| { 183 | self.key_pool 184 | .get(&public_key) 185 | .cloned() 186 | .map(|private_key| (public_key, private_key)) 187 | }) 188 | } 189 | } 190 | 191 | /// Errors that occur when updating the sync with Bitcoin task. 192 | #[derive(Debug)] 193 | pub enum SyncWithBitcoinError { 194 | /// Error occurred in the private API client. 195 | Client(C), 196 | /// Error occurred in the Bitcoin relay. 197 | Relay(R), 198 | /// Internal error. 199 | Internal(anyhow::Error), 200 | /// Initial funding transaction is unconfirmed. 201 | UnconfirmedFundingTransaction(btc::Sha256d), 202 | } 203 | 204 | /// Pushes anchoring transactions to the Bitcoin blockchain. 205 | #[derive(Debug)] 206 | pub struct SyncWithBitcoinTask 207 | where 208 | T: PrivateApi + 'static, 209 | R: BitcoinRelay + 'static, 210 | { 211 | btc_relay: R, 212 | api_client: T, 213 | } 214 | 215 | impl SyncWithBitcoinTask 216 | where 217 | T: PrivateApi + 'static, 218 | R: BitcoinRelay + 'static, 219 | T::Error: Display, 220 | R::Error: Display, 221 | { 222 | /// Creates a new sync with Bitcoin task instance. 223 | pub fn new(btc_relay: R, api_client: T) -> Self { 224 | Self { 225 | api_client, 226 | btc_relay, 227 | } 228 | } 229 | 230 | /// Performs one attempt to send the first uncommitted anchoring transaction into the Bitcoin network, if any. 231 | /// sign an anchoring proposal, if any. Returns an index of the last committed transaction. 232 | pub async fn process( 233 | &self, 234 | latest_committed_tx_index: Option, 235 | ) -> Result, SyncWithBitcoinError> { 236 | log::trace!("Perform syncing with the Bitcoin network"); 237 | // Try to find a suitable transaction for sending to the Bitcoin network. 238 | let (index, transaction) = if let Some(index) = latest_committed_tx_index { 239 | // Check that the latest committed transaction was really sent into 240 | // the Bitcoin network. 241 | let transaction = self.get_transaction(index).await?; 242 | let status = self.transaction_status(transaction.id()).await?; 243 | if status.is_known() { 244 | let chain_len = self 245 | .api_client 246 | .transactions_count() 247 | .await 248 | .map_err(SyncWithBitcoinError::Client)? 249 | .value; 250 | 251 | if index + 1 == chain_len { 252 | return Ok(Some(index)); 253 | } 254 | let index = index + 1; 255 | (index, self.get_transaction(index).await?) 256 | } else { 257 | (index, transaction) 258 | } 259 | } 260 | // Perform to find the actual uncommitted transaction. 261 | else if let Some((transaction, index)) = self.find_first_uncommitted_transaction().await? 262 | { 263 | (index, transaction) 264 | } else { 265 | return Ok(None); 266 | }; 267 | 268 | // Send an actual uncommitted transaction into the Bitcoin network. 269 | self.btc_relay 270 | .send_transaction(&transaction) 271 | .await 272 | .map_err(SyncWithBitcoinError::Relay)?; 273 | 274 | log::info!( 275 | "Sent transaction to the Bitcoin network: {}", 276 | transaction.id() 277 | ); 278 | 279 | Ok(Some(index)) 280 | } 281 | 282 | /// Finds the first anchoring transaction and its index, which was not committed into 283 | /// the Bitcoin blockchain. 284 | pub async fn find_first_uncommitted_transaction( 285 | &self, 286 | ) -> Result, SyncWithBitcoinError> { 287 | let last_index = { 288 | let count = self 289 | .api_client 290 | .transactions_count() 291 | .await 292 | .map_err(SyncWithBitcoinError::Client)? 293 | .value; 294 | 295 | if count == 0 { 296 | return Ok(None); 297 | } 298 | count - 1 299 | }; 300 | // Check that the tail of anchoring chain is committed to the Bitcoin. 301 | let transaction = self.get_transaction(last_index).await?; 302 | let status = self.transaction_status(transaction.id()).await?; 303 | if status.is_known() { 304 | return Ok(None); 305 | } 306 | // Try to find the first of uncommitted transactions. 307 | for index in (1..=last_index).rev() { 308 | let transaction = self.get_transaction(index).await?; 309 | log::trace!( 310 | "Checking for transaction with index {} and id {}", 311 | index, 312 | transaction.id() 313 | ); 314 | 315 | let previous_tx_id = transaction.prev_tx_id(); 316 | // If the transaction previous to current one is committed, we found the first 317 | // uncommitted transaction (we've checked that the last one was not committed, 318 | // so scenario when all the transactions are committed is not possible). 319 | let status = self.transaction_status(previous_tx_id).await?; 320 | if status.is_known() { 321 | log::trace!("Found committed transaction"); 322 | // Note that we were checking the previous transaction to be committed, so 323 | // we return this transaction as the first not committed. 324 | return Ok(Some((transaction, index))); 325 | } 326 | } 327 | 328 | // If we reach this branch then the transaction previous to the first one was not 329 | // committed, but previous transaction for the first anchoring transaction always 330 | // is funding. This is special case and should be handled in specific way in order 331 | // to check the initial funding transaction confirmations. 332 | let transaction = self.get_transaction(0).await?; 333 | log::trace!( 334 | "Checking for initial anchoring transaction with id {}", 335 | transaction.id() 336 | ); 337 | let status = self.transaction_status(transaction.prev_tx_id()).await?; 338 | if status.confirmations().is_none() { 339 | // First funding transaction has no confirmations. 340 | Err(SyncWithBitcoinError::UnconfirmedFundingTransaction( 341 | transaction.prev_tx_id(), 342 | )) 343 | } else { 344 | // Initial funding transaction has confirmations and then we return the first 345 | // anchoring transaction which actually is uncommitted. 346 | Ok(Some((transaction, 0))) 347 | } 348 | } 349 | 350 | async fn get_transaction( 351 | &self, 352 | index: u64, 353 | ) -> Result> { 354 | self.api_client 355 | .transaction_with_index(index) 356 | .await 357 | .map_err(SyncWithBitcoinError::Client)? 358 | .ok_or_else(|| { 359 | SyncWithBitcoinError::Internal(anyhow!( 360 | "Transaction with index {} is absent in the anchoring chain", 361 | index 362 | )) 363 | }) 364 | } 365 | 366 | async fn transaction_status( 367 | &self, 368 | txid: btc::Sha256d, 369 | ) -> Result> { 370 | self.btc_relay 371 | .transaction_status(txid) 372 | .await 373 | .map_err(SyncWithBitcoinError::Relay) 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /tests/sync.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSEccccc// 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | use async_trait::async_trait; 15 | use exonum::{ 16 | blockchain::ApiSender, 17 | crypto::{Hash, KeyPair}, 18 | helpers::Height, 19 | merkledb::ObjectHash, 20 | messages::{AnyTx, Verified}, 21 | }; 22 | use exonum_btc_anchoring::{ 23 | api::{AnchoringChainLength, AnchoringProposalState, PrivateApi}, 24 | blockchain::{AddFunds, BtcAnchoringInterface, SignInput}, 25 | btc, 26 | config::Config, 27 | sync::{ 28 | AnchoringChainUpdateTask, BitcoinRelay, ChainUpdateError, SyncWithBitcoinError, 29 | SyncWithBitcoinTask, TransactionStatus, 30 | }, 31 | test_helpers::{get_anchoring_schema, AnchoringTestKit, ANCHORING_INSTANCE_ID}, 32 | }; 33 | use exonum_rust_runtime::api; 34 | use exonum_testkit::TestKitApiClient; 35 | 36 | use std::{ 37 | collections::VecDeque, 38 | sync::{Arc, Mutex}, 39 | }; 40 | 41 | #[derive(Debug, Clone)] 42 | enum FakeRelayRequest { 43 | SendTransaction { 44 | request: btc::Transaction, 45 | response: btc::Sha256d, 46 | }, 47 | TransactionStatus { 48 | request: btc::Sha256d, 49 | response: TransactionStatus, 50 | }, 51 | } 52 | 53 | impl FakeRelayRequest { 54 | fn into_send_transaction(self) -> (btc::Transaction, btc::Sha256d) { 55 | if let FakeRelayRequest::SendTransaction { request, response } = self { 56 | (request, response) 57 | } else { 58 | panic!( 59 | "Expected response for the `send_transaction` request. But got {:?}", 60 | self 61 | ) 62 | } 63 | } 64 | 65 | fn into_transaction_status(self) -> (btc::Sha256d, TransactionStatus) { 66 | if let FakeRelayRequest::TransactionStatus { request, response } = self { 67 | (request, response) 68 | } else { 69 | panic!( 70 | "Expected response for the `transaction_confirmations` request. But got {:?}", 71 | self 72 | ) 73 | } 74 | } 75 | } 76 | 77 | #[derive(Debug, Clone, Default)] 78 | struct FakeBitcoinRelay { 79 | requests: Arc>>, 80 | } 81 | 82 | impl FakeBitcoinRelay { 83 | fn enqueue_requests(&self, requests: impl IntoIterator) { 84 | self.requests.lock().unwrap().extend(requests) 85 | } 86 | 87 | fn dequeue_request(&self) -> FakeRelayRequest { 88 | self.requests 89 | .lock() 90 | .unwrap() 91 | .pop_front() 92 | .expect("Expected relay request") 93 | } 94 | } 95 | 96 | impl Drop for FakeBitcoinRelay { 97 | fn drop(&mut self) { 98 | if !std::thread::panicking() { 99 | assert!( 100 | self.requests.lock().unwrap().is_empty(), 101 | "Unhandled requests remained. {:?}", 102 | self 103 | ); 104 | } 105 | } 106 | } 107 | 108 | #[async_trait] 109 | impl BitcoinRelay for FakeBitcoinRelay { 110 | type Error = anyhow::Error; 111 | 112 | async fn send_transaction( 113 | &self, 114 | transaction: &btc::Transaction, 115 | ) -> Result { 116 | let (expected_request, response) = self.dequeue_request().into_send_transaction(); 117 | assert_eq!(&expected_request, transaction, "Unexpected data in request"); 118 | Ok(response) 119 | } 120 | 121 | async fn transaction_status(&self, id: btc::Sha256d) -> Result { 122 | let (expected_request, response) = self.dequeue_request().into_transaction_status(); 123 | assert_eq!(expected_request, id, "Unexpected data in request"); 124 | Ok(response) 125 | } 126 | } 127 | 128 | /// TODO Implement creating TestkitApi for an arbitrary TestNode. [ECR-3222] 129 | #[derive(Debug)] 130 | struct FakePrivateApi { 131 | service_keypair: KeyPair, 132 | client: TestKitApiClient, 133 | broadcaster: ApiSender, 134 | } 135 | 136 | impl FakePrivateApi { 137 | fn for_anchoring_node( 138 | testkit: &AnchoringTestKit, 139 | client: TestKitApiClient, 140 | bitcoin_key: &btc::PublicKey, 141 | ) -> Self { 142 | let service_keypair = testkit 143 | .find_anchoring_node(bitcoin_key) 144 | .unwrap() 145 | .service_keypair(); 146 | 147 | Self { 148 | service_keypair, 149 | client, 150 | broadcaster: testkit.inner.blockchain().sender().clone(), 151 | } 152 | } 153 | 154 | async fn send(&self, transaction: T) 155 | where 156 | T: Into>, 157 | { 158 | self.broadcaster 159 | .broadcast_transaction(transaction.into()) 160 | .await 161 | .expect("Cannot broadcast transaction"); 162 | } 163 | } 164 | 165 | #[async_trait] 166 | impl PrivateApi for FakePrivateApi { 167 | type Error = api::Error; 168 | 169 | async fn sign_input(&self, sign_input: SignInput) -> Result { 170 | let signed_tx = self 171 | .service_keypair 172 | .sign_input(ANCHORING_INSTANCE_ID, sign_input); 173 | let hash = signed_tx.object_hash(); 174 | self.send(signed_tx).await; 175 | Ok(hash) 176 | } 177 | 178 | async fn add_funds(&self, transaction: btc::Transaction) -> Result { 179 | let signed_tx = self 180 | .service_keypair 181 | .add_funds(ANCHORING_INSTANCE_ID, AddFunds { transaction }); 182 | let hash = signed_tx.object_hash(); 183 | self.send(signed_tx).await; 184 | Ok(hash) 185 | } 186 | 187 | async fn anchoring_proposal(&self) -> Result { 188 | self.client.anchoring_proposal().await 189 | } 190 | 191 | async fn config(&self) -> Result { 192 | self.client.config().await 193 | } 194 | 195 | async fn transaction_with_index( 196 | &self, 197 | index: u64, 198 | ) -> Result, Self::Error> { 199 | self.client.transaction_with_index(index).await 200 | } 201 | 202 | async fn transactions_count(&self) -> Result { 203 | self.client.transactions_count().await 204 | } 205 | } 206 | 207 | fn anchoring_transaction_payload(testkit: &AnchoringTestKit, index: u64) -> Option { 208 | get_anchoring_schema(&testkit.inner.snapshot()) 209 | .transactions_chain 210 | .get(index) 211 | .map(|tx| tx.anchoring_payload().unwrap()) 212 | } 213 | 214 | #[tokio::test] 215 | async fn chain_updater_normal() { 216 | let mut testkit = AnchoringTestKit::default(); 217 | let api = testkit.inner.api(); 218 | 219 | let anchoring_interval = testkit.actual_anchoring_config().anchoring_interval; 220 | // Commit several blocks. 221 | testkit 222 | .inner 223 | .create_blocks_until(Height(anchoring_interval)); 224 | // Perform a several anchoring chain updates. 225 | for i in 0..2 { 226 | for keypair in testkit.anchoring_keypairs() { 227 | let private_api = 228 | FakePrivateApi::for_anchoring_node(&testkit, api.client().clone(), &keypair.0); 229 | 230 | AnchoringChainUpdateTask::new(vec![keypair], private_api) 231 | .process() 232 | .await 233 | .unwrap(); 234 | } 235 | testkit.inner.create_block(); 236 | // Make sure the anchoring proposal has been finalized. 237 | assert_eq!( 238 | anchoring_transaction_payload(&testkit, i) 239 | .unwrap() 240 | .block_height, 241 | Height(i * anchoring_interval) 242 | ); 243 | } 244 | } 245 | 246 | #[tokio::test] 247 | async fn chain_updater_no_initial_funds() { 248 | let anchoring_interval = 5; 249 | let mut testkit = AnchoringTestKit::new(1, anchoring_interval); 250 | // Commit several blocks. 251 | testkit 252 | .inner 253 | .create_blocks_until(Height(anchoring_interval)); 254 | // Try to perform anchoring chain update. 255 | let api = testkit.inner.api(); 256 | let e = AnchoringChainUpdateTask::new(testkit.anchoring_keypairs(), api.client().clone()) 257 | .process() 258 | .await 259 | .unwrap_err(); 260 | 261 | match e { 262 | ChainUpdateError::NoInitialFunds => {} 263 | e => panic!("Unexpected error occurred: {:?}", e), 264 | } 265 | } 266 | 267 | #[tokio::test] 268 | async fn chain_updater_insufficient_funds() { 269 | let anchoring_interval = 5; 270 | let mut testkit = AnchoringTestKit::new(1, anchoring_interval); 271 | 272 | // Add an initial funding transaction to enable anchoring. 273 | testkit 274 | .inner 275 | .create_block_with_transactions(testkit.create_funding_confirmation_txs(200).0); 276 | 277 | // Commit several blocks. 278 | testkit 279 | .inner 280 | .create_blocks_until(Height(anchoring_interval)); 281 | // Try to perform anchoring chain update. 282 | let api = testkit.inner.api(); 283 | let e = AnchoringChainUpdateTask::new(testkit.anchoring_keypairs(), api.client().clone()) 284 | .process() 285 | .await 286 | .unwrap_err(); 287 | 288 | match e { 289 | ChainUpdateError::InsufficientFunds { balance, total_fee } => { 290 | assert_eq!(balance, 200); 291 | assert_eq!(total_fee, 1530); 292 | } 293 | e => panic!("Unexpected error occurred: {:?}", e), 294 | } 295 | } 296 | 297 | #[tokio::test] 298 | async fn sync_with_bitcoin_normal() { 299 | let mut testkit = AnchoringTestKit::default(); 300 | let anchoring_interval = testkit.actual_anchoring_config().anchoring_interval; 301 | // Create a several anchoring transactions 302 | for i in 0..2 { 303 | testkit 304 | .inner 305 | .create_blocks_until(Height(anchoring_interval * i)); 306 | 307 | testkit 308 | .inner 309 | .create_block_with_transactions(testkit.create_signature_txs().into_iter().flatten()); 310 | } 311 | 312 | // Check that sync with bitcoin works as expected. 313 | let snapshot = testkit.inner.snapshot(); 314 | let anchoring_schema = get_anchoring_schema(&snapshot); 315 | let tx_chain = anchoring_schema.transactions_chain; 316 | 317 | let fake_relay = FakeBitcoinRelay::default(); 318 | let api = testkit.inner.api(); 319 | let sync = SyncWithBitcoinTask::new(fake_relay.clone(), api.client().clone()); 320 | // Send first anchoring transaction. 321 | fake_relay.enqueue_requests(vec![ 322 | // Relay should see that we have only a funding transaction confirmed. 323 | FakeRelayRequest::TransactionStatus { 324 | request: tx_chain.get(1).unwrap().id(), 325 | response: TransactionStatus::Unknown, 326 | }, 327 | FakeRelayRequest::TransactionStatus { 328 | request: tx_chain.get(0).unwrap().id(), 329 | response: TransactionStatus::Unknown, 330 | }, 331 | FakeRelayRequest::TransactionStatus { 332 | request: tx_chain.get(0).unwrap().prev_tx_id(), 333 | response: TransactionStatus::Committed(10), 334 | }, 335 | // Ensure that relay sends first anchoring transaction to the Bitcoin network. 336 | FakeRelayRequest::SendTransaction { 337 | request: tx_chain.get(0).unwrap(), 338 | response: tx_chain.get(0).unwrap().id(), 339 | }, 340 | ]); 341 | let latest_committed_tx_index = sync 342 | .process(None) 343 | .await 344 | .unwrap() 345 | .expect("Transaction should be committed"); 346 | assert_eq!(latest_committed_tx_index, 0); 347 | // Send second anchoring transaction. 348 | fake_relay.enqueue_requests(vec![ 349 | FakeRelayRequest::TransactionStatus { 350 | request: tx_chain.get(1).unwrap().id(), 351 | response: TransactionStatus::Unknown, 352 | }, 353 | FakeRelayRequest::SendTransaction { 354 | request: tx_chain.get(1).unwrap(), 355 | response: tx_chain.get(1).unwrap().id(), 356 | }, 357 | ]); 358 | let latest_committed_tx_index = sync 359 | .process(Some(1)) 360 | .await 361 | .unwrap() 362 | .expect("Transaction should be committed"); 363 | assert_eq!(latest_committed_tx_index, 1); 364 | // Check second anchoring transaction. 365 | fake_relay.enqueue_requests(vec![FakeRelayRequest::TransactionStatus { 366 | request: tx_chain.get(1).unwrap().id(), 367 | response: TransactionStatus::Mempool, 368 | }]); 369 | let latest_committed_tx_index = sync 370 | .process(Some(1)) 371 | .await 372 | .unwrap() 373 | .expect("Transaction should be committed"); 374 | assert_eq!(latest_committed_tx_index, 1); 375 | } 376 | 377 | #[tokio::test] 378 | async fn sync_with_bitcoin_empty_chain() { 379 | let mut testkit = AnchoringTestKit::default(); 380 | let api = testkit.inner.api(); 381 | assert!( 382 | SyncWithBitcoinTask::new(FakeBitcoinRelay::default(), api.client().clone()) 383 | .process(None) 384 | .await 385 | .unwrap() 386 | .is_none() 387 | ); 388 | } 389 | 390 | #[tokio::test] 391 | async fn sync_with_bitcoin_err_unconfirmed_funding_tx() { 392 | let mut testkit = AnchoringTestKit::default(); 393 | // Establish anchoring transactions chain. 394 | testkit 395 | .inner 396 | .create_block_with_transactions(testkit.create_signature_txs().into_iter().flatten()); 397 | // Check that synchronization will cause an error if the funding transaction was not confirmed. 398 | let snapshot = testkit.inner.snapshot(); 399 | let anchoring_schema = get_anchoring_schema(&snapshot); 400 | let tx_chain = anchoring_schema.transactions_chain; 401 | 402 | let fake_relay = FakeBitcoinRelay::default(); 403 | let api = testkit.inner.api(); 404 | let sync = SyncWithBitcoinTask::new(fake_relay.clone(), api.client().clone()); 405 | fake_relay.enqueue_requests(vec![ 406 | FakeRelayRequest::TransactionStatus { 407 | request: tx_chain.get(0).unwrap().id(), 408 | response: TransactionStatus::Unknown, 409 | }, 410 | FakeRelayRequest::TransactionStatus { 411 | request: tx_chain.get(0).unwrap().prev_tx_id(), 412 | response: TransactionStatus::Unknown, 413 | }, 414 | ]); 415 | 416 | let e = sync.process(None).await.unwrap_err(); 417 | match e { 418 | SyncWithBitcoinError::UnconfirmedFundingTransaction(hash) => { 419 | assert_eq!(hash, tx_chain.get(0).unwrap().prev_tx_id()) 420 | } 421 | e => panic!("Unexpected error occurred: {:?}", e), 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /examples/btc_anchoring_sync.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use anyhow::{anyhow, bail}; 16 | use async_trait::async_trait; 17 | use bitcoincore_rpc::{Auth as BitcoinRpcAuth, Client as BitcoinRpcClient}; 18 | use exonum::crypto::Hash; 19 | use exonum_btc_anchoring::{ 20 | api::{AnchoringChainLength, AnchoringProposalState, IndexQuery, PrivateApi}, 21 | blockchain::SignInput, 22 | btc, 23 | config::Config as AnchoringConfig, 24 | sync::{AnchoringChainUpdateTask, ChainUpdateError, SyncWithBitcoinError, SyncWithBitcoinTask}, 25 | }; 26 | use serde::{de::DeserializeOwned, ser::Serialize}; 27 | use serde_derive::{Deserialize, Serialize}; 28 | use structopt::StructOpt; 29 | use tokio::time::delay_for; 30 | 31 | use std::{ 32 | collections::HashMap, 33 | convert::TryFrom, 34 | fs::{self, File}, 35 | io::prelude::*, 36 | path::{Path, PathBuf}, 37 | time::Duration, 38 | }; 39 | 40 | /// Client implementation for the API of the anchoring service instance. 41 | #[derive(Debug, Clone)] 42 | pub struct ApiClient { 43 | /// Complete prefix with the port and the anchoring instance name. 44 | prefix: String, 45 | /// Underlying HTTP client. 46 | client: reqwest::Client, 47 | } 48 | 49 | impl ApiClient { 50 | /// Create a new anchoring API relay with the specified host and name of instance. 51 | /// Hostname should be in form `{http|https}://{address}:{port}`. 52 | pub fn new(hostname: impl AsRef, instance_name: impl AsRef) -> Self { 53 | Self { 54 | prefix: format!( 55 | "{}/api/services/{}", 56 | hostname.as_ref(), 57 | instance_name.as_ref() 58 | ), 59 | client: reqwest::Client::new(), 60 | } 61 | } 62 | 63 | fn endpoint(&self, name: impl AsRef) -> String { 64 | format!("{}/{}", self.prefix, name.as_ref()) 65 | } 66 | 67 | async fn get(&self, endpoint: &str) -> Result 68 | where 69 | R: DeserializeOwned + Send + 'static, 70 | { 71 | self.client 72 | .get(&self.endpoint(endpoint)) 73 | .send() 74 | .await? 75 | .json() 76 | .await 77 | } 78 | 79 | async fn get_query(&self, endpoint: &str, query: &Q) -> Result 80 | where 81 | Q: Serialize, 82 | R: DeserializeOwned + Send + 'static, 83 | { 84 | self.client 85 | .get(&self.endpoint(endpoint)) 86 | .query(query) 87 | .send() 88 | .await? 89 | .json() 90 | .await 91 | } 92 | 93 | async fn post(&self, endpoint: &str, body: &Q) -> Result 94 | where 95 | Q: Serialize, 96 | R: DeserializeOwned + Send + 'static, 97 | { 98 | self.client 99 | .post(&self.endpoint(endpoint)) 100 | .json(&body) 101 | .send() 102 | .await? 103 | .json() 104 | .await 105 | } 106 | } 107 | 108 | #[async_trait] 109 | impl PrivateApi for ApiClient { 110 | type Error = reqwest::Error; 111 | 112 | async fn sign_input(&self, sign_input: SignInput) -> Result { 113 | self.post("sign-input", &sign_input).await 114 | } 115 | 116 | async fn add_funds(&self, transaction: btc::Transaction) -> Result { 117 | self.post("add-funds", &transaction).await 118 | } 119 | 120 | async fn anchoring_proposal(&self) -> Result { 121 | self.get("anchoring-proposal").await 122 | } 123 | 124 | async fn config(&self) -> Result { 125 | self.get("config").await 126 | } 127 | 128 | async fn transaction_with_index( 129 | &self, 130 | index: u64, 131 | ) -> Result, Self::Error> { 132 | self.get_query("transaction", &IndexQuery { index }).await 133 | } 134 | 135 | async fn transactions_count(&self) -> Result { 136 | self.get("transactions-count").await 137 | } 138 | } 139 | 140 | /// Generate initial configuration for the btc anchoring sync utility. 141 | #[derive(Debug, StructOpt)] 142 | struct GenerateConfigCommand { 143 | /// Path to a sync utility configuration file which will be created after 144 | /// running this command. 145 | #[structopt(long, short = "o", default_value = "btc_anchoring_sync.toml")] 146 | output: PathBuf, 147 | /// Anchoring node private API url address. 148 | #[structopt(long, short = "e", default_value = "http://localhost:8081")] 149 | exonum_private_api: String, 150 | /// Bitcoin network type. 151 | #[structopt(long, short = "n", default_value = "testnet")] 152 | bitcoin_network: bitcoin::Network, 153 | /// Name of the anchoring service instance. 154 | #[structopt(long, short = "i", default_value = "anchoring")] 155 | instance_name: String, 156 | /// Bitcoin RPC url. 157 | #[structopt(long)] 158 | bitcoin_rpc_host: Option, 159 | /// Bitcoin RPC username. 160 | #[structopt(long)] 161 | bitcoin_rpc_user: Option, 162 | /// Bitcoin RPC password. 163 | #[structopt(long)] 164 | bitcoin_rpc_password: Option, 165 | } 166 | 167 | #[derive(Debug, StructOpt)] 168 | struct RunCommand { 169 | /// Path to a sync utility configuration file. 170 | #[structopt(long, short = "c")] 171 | config: PathBuf, 172 | } 173 | 174 | /// Generates a new Bitcoin key pair and add them to the key pool of the specified 175 | /// configuration file. 176 | #[derive(Debug, StructOpt)] 177 | struct GenerateKeypairCommand { 178 | /// Path to a sync utility configuration file. 179 | #[structopt(long, short = "c")] 180 | config: PathBuf, 181 | } 182 | 183 | #[derive(Debug, StructOpt)] 184 | enum Commands { 185 | /// Generate initial configuration for the btc anchoring sync utility. 186 | GenerateConfig(GenerateConfigCommand), 187 | /// Run btc anchoring sync utility. 188 | Run(RunCommand), 189 | /// Generate a new Bitcoin key pair and add them to the key pool of the specified 190 | /// configuration file. 191 | GenerateKeypair(GenerateKeypairCommand), 192 | } 193 | 194 | #[derive(Debug, Serialize, Deserialize)] 195 | struct SyncConfig { 196 | exonum_private_api: String, 197 | instance_name: String, 198 | #[serde(with = "flatten_keypairs")] 199 | bitcoin_key_pool: HashMap, 200 | bitcoin_rpc_config: Option, 201 | } 202 | 203 | impl SyncConfig { 204 | /// Extracts Bitcoin network type from the one of Bitcoin private keys in this config. 205 | fn bitcoin_network(&self) -> Option { 206 | self.bitcoin_key_pool 207 | .values() 208 | .next() 209 | .map(|key| key.0.network) 210 | } 211 | 212 | fn load(path: impl AsRef) -> anyhow::Result { 213 | let mut file = File::open(path)?; 214 | let mut toml = String::new(); 215 | file.read_to_string(&mut toml)?; 216 | toml::de::from_str(&toml).map_err(From::from) 217 | } 218 | 219 | fn save(&self, path: impl AsRef) -> anyhow::Result<()> { 220 | let path = path.as_ref(); 221 | 222 | if let Some(dir) = path.parent() { 223 | fs::create_dir_all(dir)?; 224 | } 225 | 226 | let mut file = File::create(path)?; 227 | let value_toml = toml::Value::try_from(&self)?; 228 | file.write_all(value_toml.to_string().as_bytes())?; 229 | Ok(()) 230 | } 231 | } 232 | 233 | /// `Bitcoind` rpc configuration. 234 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] 235 | struct BitcoinRpcConfig { 236 | /// Bitcoin RPC url. 237 | host: String, 238 | /// Bitcoin RPC username. 239 | user: Option, 240 | /// Bitcoin RPC password. 241 | password: Option, 242 | } 243 | 244 | impl TryFrom for BitcoinRpcClient { 245 | type Error = bitcoincore_rpc::Error; 246 | 247 | fn try_from(value: BitcoinRpcConfig) -> Result { 248 | let auth = BitcoinRpcAuth::UserPass( 249 | value.user.unwrap_or_default(), 250 | value.password.unwrap_or_default(), 251 | ); 252 | Self::new(value.host, auth) 253 | } 254 | } 255 | 256 | impl GenerateConfigCommand { 257 | fn run(self) -> anyhow::Result<()> { 258 | let bitcoin_keypair = btc::gen_keypair(self.bitcoin_network); 259 | 260 | let bitcoin_rpc_config = self.bitcoin_rpc_config(); 261 | let sync_config = SyncConfig { 262 | exonum_private_api: self.exonum_private_api, 263 | bitcoin_key_pool: std::iter::once(bitcoin_keypair.clone()).collect(), 264 | instance_name: self.instance_name, 265 | bitcoin_rpc_config, 266 | }; 267 | 268 | sync_config.save(self.output)?; 269 | log::info!("Generated initial configuration for the btc anchoring sync util."); 270 | log::trace!( 271 | "Available Bitcoin keys in key pool: {:?}", 272 | sync_config.bitcoin_key_pool 273 | ); 274 | // Print the received Bitcoin public key to use it in scripts. 275 | println!("{}", bitcoin_keypair.0); 276 | Ok(()) 277 | } 278 | 279 | fn bitcoin_rpc_config(&self) -> Option { 280 | self.bitcoin_rpc_host.clone().map(|host| BitcoinRpcConfig { 281 | host, 282 | user: self.bitcoin_rpc_user.clone(), 283 | password: self.bitcoin_rpc_password.clone(), 284 | }) 285 | } 286 | } 287 | 288 | impl RunCommand { 289 | async fn run(self) -> anyhow::Result<()> { 290 | let sync_config = SyncConfig::load(self.config)?; 291 | let client = ApiClient::new(sync_config.exonum_private_api, sync_config.instance_name); 292 | let chain_updater = 293 | AnchoringChainUpdateTask::new(sync_config.bitcoin_key_pool, client.clone()); 294 | let bitcoin_relay = sync_config 295 | .bitcoin_rpc_config 296 | .map(BitcoinRpcClient::try_from) 297 | .transpose()? 298 | .map(|relay| SyncWithBitcoinTask::new(relay, client.clone())); 299 | 300 | let mut latest_synced_tx_index: Option = None; 301 | loop { 302 | match chain_updater.process().await { 303 | Ok(_) => {} 304 | // Client problems most often occurs due to network problems. 305 | Err(ChainUpdateError::Client(e)) => { 306 | log::error!("An error in the anchoring API client occurred. {}", e) 307 | } 308 | // Sometimes Bitcoin end in the anchoring wallet. 309 | Err(ChainUpdateError::InsufficientFunds { total_fee, balance }) => log::warn!( 310 | "Insufficient funds to construct a new anchoring transaction, \ 311 | total fee is {}, total balance is {}", 312 | total_fee, 313 | balance 314 | ), 315 | // For the work of anchoring you need to replenish anchoring wallet. 316 | Err(ChainUpdateError::NoInitialFunds) => { 317 | let address = match chain_updater.anchoring_config().await { 318 | Ok(config) => config.anchoring_address(), 319 | Err(e) => { 320 | log::error!("An error in the anchoring API client occurred. {}", e); 321 | continue; 322 | } 323 | }; 324 | 325 | log::warn!( 326 | "Initial funding transaction is absent, you should send some \ 327 | Bitcoins to the address {}", 328 | address 329 | ); 330 | log::warn!( 331 | "And then confirm this transaction using the private \ 332 | `add-funds` API method." 333 | ) 334 | } 335 | // Stop execution if an internal error occurred. 336 | Err(ChainUpdateError::Internal(e)) => return Err(e), 337 | } 338 | 339 | if let Some(relay) = bitcoin_relay.as_ref() { 340 | match relay.process(latest_synced_tx_index).await { 341 | Ok(index) => latest_synced_tx_index = index, 342 | 343 | Err(SyncWithBitcoinError::Client(e)) => { 344 | log::error!("An error in the anchoring API client occurred. {}", e) 345 | } 346 | 347 | Err(SyncWithBitcoinError::Relay(e)) => { 348 | log::error!("An error in the Bitcoin relay occurred. {}", e) 349 | } 350 | 351 | Err(SyncWithBitcoinError::UnconfirmedFundingTransaction(id)) => bail!( 352 | "Funding transaction with id {} is unconfirmed by Bitcoin network. \ 353 | This is a serious mistake that can break anchoring process.", 354 | id 355 | ), 356 | 357 | // Stop execution if an internal error occurred. 358 | Err(SyncWithBitcoinError::Internal(e)) => return Err(e), 359 | } 360 | } 361 | 362 | // Don't perform this actions too frequent to avoid DOS attack. 363 | delay_for(Duration::from_secs(5)).await 364 | } 365 | } 366 | } 367 | 368 | impl GenerateKeypairCommand { 369 | fn run(self) -> anyhow::Result<()> { 370 | let mut sync_config = SyncConfig::load(&self.config)?; 371 | 372 | let network = sync_config.bitcoin_network().ok_or_else(|| { 373 | anyhow!( 374 | "Unable to determine Bitcoin network type from config.\ 375 | Perhaps pool of keys in config is empty." 376 | ) 377 | })?; 378 | let bitcoin_keypair = btc::gen_keypair(network); 379 | let bitcoin_pub_key = bitcoin_keypair.0; 380 | 381 | sync_config 382 | .bitcoin_key_pool 383 | .extend(std::iter::once(bitcoin_keypair)); 384 | sync_config.save(self.config)?; 385 | // Print the received Bitcoin public key to use it in scripts. 386 | println!("{}", bitcoin_pub_key); 387 | Ok(()) 388 | } 389 | } 390 | 391 | impl Commands { 392 | async fn run(self) -> anyhow::Result<()> { 393 | match self { 394 | Commands::GenerateConfig(cmd) => cmd.run(), 395 | Commands::GenerateKeypair(cmd) => cmd.run(), 396 | Commands::Run(cmd) => cmd.run().await, 397 | } 398 | } 399 | } 400 | 401 | #[tokio::main] 402 | async fn main() -> anyhow::Result<()> { 403 | exonum::helpers::init_logger()?; 404 | Commands::from_args().run().await 405 | } 406 | 407 | mod flatten_keypairs { 408 | use crate::btc::{PrivateKey, PublicKey}; 409 | 410 | use serde_derive::{Deserialize, Serialize}; 411 | 412 | use std::collections::HashMap; 413 | 414 | /// The structure for storing the bitcoin keypair. 415 | /// It is required for reading data from the .toml file into memory. 416 | #[derive(Deserialize, Serialize)] 417 | struct BitcoinKeypair { 418 | /// Bitcoin public key. 419 | public_key: PublicKey, 420 | /// Corresponding private key. 421 | private_key: PrivateKey, 422 | } 423 | 424 | pub fn serialize(keys: &HashMap, ser: S) -> Result 425 | where 426 | S: serde::Serializer, 427 | { 428 | use serde::Serialize; 429 | 430 | let keypairs = keys 431 | .iter() 432 | .map(|(&public_key, private_key)| BitcoinKeypair { 433 | public_key, 434 | private_key: private_key.clone(), 435 | }) 436 | .collect::>(); 437 | keypairs.serialize(ser) 438 | } 439 | 440 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 441 | where 442 | D: serde::Deserializer<'de>, 443 | { 444 | use serde::Deserialize; 445 | Vec::::deserialize(deserializer).map(|keypairs| { 446 | keypairs 447 | .into_iter() 448 | .map(|keypair| (keypair.public_key, keypair.private_key)) 449 | .collect() 450 | }) 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /tests/api.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSEccccc// 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | use btc_transaction_utils::{p2wsh, TxInRef}; 15 | use exonum::helpers::Height; 16 | use exonum_btc_anchoring::{ 17 | api::{AnchoringProposalState, PrivateApi, PublicApi}, 18 | blockchain::SignInput, 19 | btc, 20 | test_helpers::{ 21 | create_fake_funding_transaction, get_anchoring_schema, AnchoringTestKit, ValidateProof, 22 | ANCHORING_INSTANCE_ID, 23 | }, 24 | }; 25 | use exonum_supervisor::ConfigPropose; 26 | use exonum_testkit::TestKitApi; 27 | 28 | fn init_testkit() -> (AnchoringTestKit, TestKitApi) { 29 | let mut testkit = AnchoringTestKit::default(); 30 | let api = testkit.inner.api(); 31 | (testkit, api) 32 | } 33 | 34 | async fn find_transaction( 35 | anchoring_testkit: &AnchoringTestKit, 36 | anchoring_api: &TestKitApi, 37 | height: Option, 38 | ) -> Option { 39 | let proof = anchoring_api 40 | .client() 41 | .find_transaction(height) 42 | .await 43 | .unwrap(); 44 | 45 | let validator_keys = anchoring_testkit 46 | .inner 47 | .consensus_config() 48 | .validator_keys 49 | .into_iter() 50 | .map(|key| key.consensus_key) 51 | .collect::>(); 52 | proof.validate(&validator_keys).unwrap().map(|(_, tx)| tx) 53 | } 54 | 55 | async fn transaction_with_index(api: &TestKitApi, index: u64) -> Option { 56 | api.client().transaction_with_index(index).await.unwrap() 57 | } 58 | 59 | #[tokio::test] 60 | async fn actual_address() { 61 | let (mut anchoring_testkit, anchoring_api) = init_testkit(); 62 | let anchoring_interval = anchoring_testkit 63 | .actual_anchoring_config() 64 | .anchoring_interval; 65 | 66 | assert!(anchoring_testkit.last_anchoring_tx().is_none()); 67 | 68 | anchoring_testkit.inner.create_block_with_transactions( 69 | anchoring_testkit 70 | .create_signature_txs() 71 | .into_iter() 72 | .flatten(), 73 | ); 74 | anchoring_testkit 75 | .inner 76 | .create_blocks_until(Height(anchoring_interval)); 77 | 78 | assert_eq!( 79 | anchoring_api.client().actual_address().await.unwrap(), 80 | anchoring_testkit 81 | .actual_anchoring_config() 82 | .anchoring_address() 83 | ); 84 | } 85 | 86 | #[tokio::test] 87 | async fn following_address() { 88 | let (mut anchoring_testkit, anchoring_api) = init_testkit(); 89 | let anchoring_interval = anchoring_testkit 90 | .actual_anchoring_config() 91 | .anchoring_interval; 92 | 93 | assert!(anchoring_testkit.last_anchoring_tx().is_none()); 94 | // Establish anchoring transactions chain. 95 | anchoring_testkit.inner.create_block_with_transactions( 96 | anchoring_testkit 97 | .create_signature_txs() 98 | .into_iter() 99 | .flatten(), 100 | ); 101 | 102 | // Skip the next anchoring height. 103 | anchoring_testkit 104 | .inner 105 | .create_blocks_until(Height(anchoring_interval * 2)); 106 | 107 | // Add an anchoring node. 108 | let mut new_cfg = anchoring_testkit.actual_anchoring_config(); 109 | new_cfg.anchoring_keys.push(anchoring_testkit.add_node()); 110 | let following_address = new_cfg.anchoring_address(); 111 | 112 | // Commit configuration with without last anchoring node. 113 | anchoring_testkit.inner.create_block_with_transaction( 114 | anchoring_testkit.create_config_change_tx( 115 | ConfigPropose::new(0, anchoring_testkit.inner.height().next()) 116 | .service_config(ANCHORING_INSTANCE_ID, new_cfg), 117 | ), 118 | ); 119 | anchoring_testkit.inner.create_block(); 120 | 121 | assert_eq!( 122 | anchoring_api.client().following_address().await.unwrap(), 123 | Some(following_address) 124 | ); 125 | } 126 | 127 | #[tokio::test] 128 | async fn find_transaction_regular() { 129 | let (mut anchoring_testkit, anchoring_api) = init_testkit(); 130 | let anchoring_interval = anchoring_testkit 131 | .actual_anchoring_config() 132 | .anchoring_interval; 133 | 134 | // Create a several anchoring transactions 135 | for i in 1..=5 { 136 | anchoring_testkit.inner.create_block_with_transactions( 137 | anchoring_testkit 138 | .create_signature_txs() 139 | .into_iter() 140 | .flatten(), 141 | ); 142 | anchoring_testkit 143 | .inner 144 | .create_blocks_until(Height(anchoring_interval * i)); 145 | } 146 | 147 | let snapshot = anchoring_testkit.inner.snapshot(); 148 | let anchoring_schema = get_anchoring_schema(&snapshot); 149 | let tx_chain = anchoring_schema.transactions_chain; 150 | // Find transactions by height. 151 | assert_eq!( 152 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(0))) 153 | .await 154 | .unwrap(), 155 | tx_chain.get(0).unwrap() 156 | ); 157 | assert_eq!( 158 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(3))) 159 | .await 160 | .unwrap(), 161 | tx_chain.get(1).unwrap() 162 | ); 163 | assert_eq!( 164 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(4))) 165 | .await 166 | .unwrap(), 167 | tx_chain.get(1).unwrap() 168 | ); 169 | assert_eq!( 170 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(1000))) 171 | .await 172 | .unwrap(), 173 | tx_chain.get(4).unwrap() 174 | ); 175 | assert_eq!( 176 | find_transaction(&anchoring_testkit, &anchoring_api, None) 177 | .await 178 | .unwrap(), 179 | tx_chain.last().unwrap() 180 | ); 181 | // Find transactions by index. 182 | for i in 0..=tx_chain.len() { 183 | assert_eq!( 184 | transaction_with_index(&anchoring_api, i).await, 185 | tx_chain.get(i) 186 | ); 187 | } 188 | } 189 | 190 | // Check come edge cases in the find_transaction api method. 191 | #[tokio::test] 192 | async fn find_transaction_configuration_change() { 193 | let (mut anchoring_testkit, anchoring_api) = init_testkit(); 194 | let anchoring_interval = anchoring_testkit 195 | .actual_anchoring_config() 196 | .anchoring_interval; 197 | 198 | assert!(anchoring_testkit.last_anchoring_tx().is_none()); 199 | // Establish anchoring transactions chain. 200 | anchoring_testkit.inner.create_block_with_transactions( 201 | anchoring_testkit 202 | .create_signature_txs() 203 | .into_iter() 204 | .flatten(), 205 | ); 206 | 207 | // Skip the next anchoring height. 208 | anchoring_testkit 209 | .inner 210 | .create_blocks_until(Height(anchoring_interval * 2)); 211 | 212 | // Add an anchoring node. 213 | let mut new_cfg = anchoring_testkit.actual_anchoring_config(); 214 | new_cfg.anchoring_keys.push(anchoring_testkit.add_node()); 215 | 216 | // Commit configuration with without last anchoring node. 217 | anchoring_testkit.inner.create_block_with_transaction( 218 | anchoring_testkit.create_config_change_tx( 219 | ConfigPropose::new(0, anchoring_testkit.inner.height().next()) 220 | .service_config(ANCHORING_INSTANCE_ID, new_cfg), 221 | ), 222 | ); 223 | anchoring_testkit.inner.create_block(); 224 | 225 | // Transit to the new address. 226 | anchoring_testkit.inner.create_block_with_transactions( 227 | anchoring_testkit 228 | .create_signature_txs() 229 | .into_iter() 230 | .flatten(), 231 | ); 232 | 233 | let snapshot = anchoring_testkit.inner.snapshot(); 234 | let anchoring_schema = get_anchoring_schema(&snapshot); 235 | assert_eq!( 236 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(0))).await, 237 | anchoring_schema.transactions_chain.get(1) 238 | ); 239 | assert_eq!( 240 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(1))).await, 241 | anchoring_schema.transactions_chain.get(1) 242 | ); 243 | assert_eq!( 244 | find_transaction(&anchoring_testkit, &anchoring_api, None).await, 245 | anchoring_schema.transactions_chain.get(1) 246 | ); 247 | 248 | // Resume regular anchoring (anchors block on height 5). 249 | anchoring_testkit 250 | .inner 251 | .create_blocks_until(Height(anchoring_interval * 2)); 252 | anchoring_testkit.inner.create_block_with_transactions( 253 | anchoring_testkit 254 | .create_signature_txs() 255 | .into_iter() 256 | .flatten(), 257 | ); 258 | 259 | let snapshot = anchoring_testkit.inner.snapshot(); 260 | let anchoring_schema = get_anchoring_schema(&snapshot); 261 | assert_eq!( 262 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(0))).await, 263 | anchoring_schema.transactions_chain.get(1) 264 | ); 265 | assert_eq!( 266 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(1))).await, 267 | anchoring_schema.transactions_chain.get(2) 268 | ); 269 | assert_eq!( 270 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(10))).await, 271 | anchoring_schema.transactions_chain.get(2) 272 | ); 273 | assert_eq!( 274 | find_transaction(&anchoring_testkit, &anchoring_api, None).await, 275 | anchoring_schema.transactions_chain.get(2) 276 | ); 277 | 278 | // Anchors block on height 10. 279 | anchoring_testkit.inner.create_block_with_transactions( 280 | anchoring_testkit 281 | .create_signature_txs() 282 | .into_iter() 283 | .flatten(), 284 | ); 285 | 286 | let snapshot = anchoring_testkit.inner.snapshot(); 287 | let anchoring_schema = get_anchoring_schema(&snapshot); 288 | assert_eq!( 289 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(0))).await, 290 | anchoring_schema.transactions_chain.get(1) 291 | ); 292 | assert_eq!( 293 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(1))).await, 294 | anchoring_schema.transactions_chain.get(2) 295 | ); 296 | assert_eq!( 297 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(5))).await, 298 | anchoring_schema.transactions_chain.get(2) 299 | ); 300 | assert_eq!( 301 | find_transaction(&anchoring_testkit, &anchoring_api, Some(Height(6))).await, 302 | anchoring_schema.transactions_chain.get(3) 303 | ); 304 | assert_eq!( 305 | find_transaction(&anchoring_testkit, &anchoring_api, None).await, 306 | anchoring_schema.transactions_chain.get(3) 307 | ); 308 | } 309 | 310 | #[tokio::test] 311 | async fn actual_config() { 312 | let (anchoring_testkit, anchoring_api) = init_testkit(); 313 | 314 | let cfg = anchoring_testkit.actual_anchoring_config(); 315 | 316 | let client = anchoring_api.client(); 317 | assert_eq!(PublicApi::config(client).await.unwrap(), cfg); 318 | assert_eq!(PrivateApi::config(client).await.unwrap(), cfg); 319 | } 320 | 321 | #[tokio::test] 322 | async fn anchoring_proposal_ok() { 323 | let (anchoring_testkit, anchoring_api) = init_testkit(); 324 | let proposal = anchoring_testkit.anchoring_transaction_proposal().unwrap(); 325 | 326 | assert_eq!( 327 | anchoring_api.client().anchoring_proposal().await.unwrap(), 328 | AnchoringProposalState::Available { 329 | transaction: proposal.0, 330 | inputs: proposal.1, 331 | } 332 | ); 333 | } 334 | 335 | #[tokio::test] 336 | async fn anchoring_proposal_none() { 337 | let (mut anchoring_testkit, anchoring_api) = init_testkit(); 338 | 339 | // Establish anchoring transactions chain. 340 | anchoring_testkit.inner.create_block_with_transactions( 341 | anchoring_testkit 342 | .create_signature_txs() 343 | .into_iter() 344 | .flatten(), 345 | ); 346 | 347 | assert_eq!( 348 | anchoring_api.client().anchoring_proposal().await.unwrap(), 349 | AnchoringProposalState::None 350 | ); 351 | } 352 | 353 | #[tokio::test] 354 | async fn anchoring_proposal_err_without_initial_funds() { 355 | let mut anchoring_testkit = AnchoringTestKit::new(4, 5); 356 | 357 | let api = anchoring_testkit.inner.api(); 358 | let state = api.client().anchoring_proposal().await.unwrap(); 359 | assert_eq!(state, AnchoringProposalState::NoInitialFunds); 360 | } 361 | 362 | #[tokio::test] 363 | async fn anchoring_proposal_err_insufficient_funds() { 364 | let mut anchoring_testkit = AnchoringTestKit::new(4, 5); 365 | 366 | // Add an initial funding transaction to enable anchoring. 367 | anchoring_testkit 368 | .inner 369 | .create_block_with_transactions(anchoring_testkit.create_funding_confirmation_txs(20).0); 370 | 371 | let api = anchoring_testkit.inner.api(); 372 | let state = api.client().anchoring_proposal().await.unwrap(); 373 | assert_eq!( 374 | state, 375 | AnchoringProposalState::InsufficientFunds { 376 | total_fee: 1530, 377 | balance: 20 378 | } 379 | ); 380 | } 381 | 382 | #[tokio::test] 383 | async fn sign_input() { 384 | let (mut anchoring_testkit, anchoring_api) = init_testkit(); 385 | 386 | let config = anchoring_testkit.actual_anchoring_config(); 387 | let bitcoin_public_key = config 388 | .find_bitcoin_key(&anchoring_testkit.inner.us().service_keypair().public_key()) 389 | .unwrap() 390 | .1; 391 | let bitcoin_private_key = anchoring_testkit.node_private_key(&bitcoin_public_key); 392 | // Create sign input transaction 393 | let redeem_script = config.redeem_script(); 394 | 395 | let (proposal, proposal_inputs) = anchoring_testkit.anchoring_transaction_proposal().unwrap(); 396 | let proposal_input = &proposal_inputs[0]; 397 | 398 | let signature = p2wsh::InputSigner::new(redeem_script) 399 | .sign_input( 400 | TxInRef::new(proposal.as_ref(), 0), 401 | proposal_input.as_ref(), 402 | &bitcoin_private_key.0.key, 403 | ) 404 | .unwrap(); 405 | 406 | p2wsh::InputSigner::new(config.redeem_script()) 407 | .verify_input( 408 | TxInRef::new(proposal.as_ref(), 0), 409 | proposal_input.as_ref(), 410 | &bitcoin_public_key.0, 411 | &signature, 412 | ) 413 | .unwrap(); 414 | 415 | let tx_hash = anchoring_api 416 | .client() 417 | .sign_input(SignInput { 418 | input: 0, 419 | input_signature: signature.into(), 420 | txid: proposal.id(), 421 | }) 422 | .await 423 | .unwrap(); 424 | 425 | anchoring_testkit 426 | .inner 427 | .create_block_with_tx_hashes(&[tx_hash])[0] 428 | .status() 429 | .expect("Transaction should be successful"); 430 | } 431 | 432 | #[tokio::test] 433 | async fn add_funds_ok() { 434 | let anchoring_interval = 5; 435 | let mut anchoring_testkit = AnchoringTestKit::new(1, anchoring_interval); 436 | let anchoring_api = anchoring_testkit.inner.api(); 437 | 438 | let config = anchoring_testkit.actual_anchoring_config(); 439 | let funding_transaction = create_fake_funding_transaction(&config.anchoring_address(), 10_000); 440 | 441 | let tx_hash = anchoring_api 442 | .client() 443 | .add_funds(funding_transaction) 444 | .await 445 | .unwrap(); 446 | 447 | anchoring_testkit 448 | .inner 449 | .create_block_with_tx_hashes(&[tx_hash])[0] 450 | .status() 451 | .expect("Transaction should be successful"); 452 | } 453 | 454 | #[tokio::test] 455 | async fn add_funds_err_already_used() { 456 | let anchoring_interval = 5; 457 | let mut anchoring_testkit = AnchoringTestKit::new(1, anchoring_interval); 458 | 459 | // Add an initial funding transaction to enable anchoring. 460 | let (txs, funding_transaction) = anchoring_testkit.create_funding_confirmation_txs(2000); 461 | anchoring_testkit.inner.create_block_with_transactions(txs); 462 | 463 | // Establish anchoring transactions chain. 464 | anchoring_testkit.inner.create_block_with_transactions( 465 | anchoring_testkit 466 | .create_signature_txs() 467 | .into_iter() 468 | .flatten(), 469 | ); 470 | 471 | anchoring_testkit 472 | .inner 473 | .api() 474 | .client() 475 | .add_funds(funding_transaction) 476 | .await 477 | .expect_err("Add funds must fail"); 478 | } 479 | 480 | #[tokio::test] 481 | async fn add_funds_err_unsuitable() { 482 | let anchoring_interval = 5; 483 | let mut anchoring_testkit = AnchoringTestKit::new(4, anchoring_interval); 484 | let anchoring_api = anchoring_testkit.inner.api(); 485 | 486 | let mut config = anchoring_testkit.actual_anchoring_config(); 487 | config.anchoring_keys.swap(1, 3); 488 | let funding_transaction = create_fake_funding_transaction(&config.anchoring_address(), 10_000); 489 | 490 | anchoring_api 491 | .client() 492 | .add_funds(funding_transaction) 493 | .await 494 | .expect_err("Add funds must fail"); 495 | } 496 | -------------------------------------------------------------------------------- /src/test_helpers/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Set of helpers for btc anchoring testing. 16 | 17 | use async_trait::async_trait; 18 | use bitcoin::{self, network::constants::Network}; 19 | use bitcoin_hashes::{sha256d::Hash as Sha256dHash, Hash as BitcoinHash}; 20 | use btc_transaction_utils::{p2wsh, TxInRef}; 21 | use exonum::{ 22 | crypto::{Hash, KeyPair, PublicKey}, 23 | helpers::Height, 24 | keys::Keys, 25 | messages::{AnyTx, Verified}, 26 | runtime::{InstanceId, SnapshotExt, SUPERVISOR_INSTANCE_ID}, 27 | }; 28 | use exonum_merkledb::{access::Access, Snapshot}; 29 | use exonum_rust_runtime::api; 30 | use exonum_supervisor::{ConfigPropose, Supervisor, SupervisorInterface}; 31 | use exonum_testkit::{ApiKind, Spec, TestKit, TestKitApiClient, TestKitBuilder, TestNode}; 32 | use rand::{thread_rng, Rng}; 33 | 34 | use std::collections::BTreeMap; 35 | 36 | use crate::{ 37 | api::{ 38 | AnchoringChainLength, AnchoringProposalState, FindTransactionQuery, IndexQuery, PrivateApi, 39 | PublicApi, TransactionProof, 40 | }, 41 | blockchain::{AddFunds, BtcAnchoringInterface, Schema, SignInput}, 42 | btc, 43 | config::Config, 44 | proto::AnchoringKeys, 45 | BtcAnchoringService, 46 | }; 47 | 48 | /// Default anchoring instance ID. 49 | pub const ANCHORING_INSTANCE_ID: InstanceId = 14; 50 | /// Default anchoring instance name. 51 | pub const ANCHORING_INSTANCE_NAME: &str = "btc_anchoring"; 52 | 53 | /// Generates a fake funding transaction. 54 | pub fn create_fake_funding_transaction(address: &btc::Address, value: u64) -> btc::Transaction { 55 | // Generate random transaction id. 56 | let mut rng = thread_rng(); 57 | let mut data = [0_u8; 32]; 58 | rng.fill(&mut data); 59 | // Create fake funding transaction. 60 | bitcoin::Transaction { 61 | version: 2, 62 | lock_time: 0, 63 | input: vec![bitcoin::TxIn { 64 | previous_output: bitcoin::OutPoint { 65 | vout: 0, 66 | txid: Sha256dHash::from_slice(&data).unwrap().into(), 67 | }, 68 | script_sig: bitcoin::Script::new(), 69 | sequence: 0, 70 | witness: vec![], 71 | }], 72 | output: vec![bitcoin::TxOut { 73 | value, 74 | script_pubkey: address.0.script_pubkey(), 75 | }], 76 | } 77 | .into() 78 | } 79 | 80 | fn gen_validator_keys() -> Keys { 81 | let consensus_keypair = KeyPair::random(); 82 | let service_keypair = KeyPair::random(); 83 | Keys::from_keys(consensus_keypair, service_keypair) 84 | } 85 | 86 | #[derive(Debug, Default)] 87 | struct AnchoringNodes { 88 | key_pool: BTreeMap, 89 | inner: BTreeMap, 90 | } 91 | 92 | impl AnchoringNodes { 93 | fn from_keys(network: Network, keys: &[Keys]) -> Self { 94 | let mut nodes = Self::default(); 95 | keys.iter().map(Keys::service_pk).for_each(|sk| { 96 | nodes.add_node(network, sk); 97 | }); 98 | nodes 99 | } 100 | 101 | fn add_node(&mut self, network: Network, service_key: PublicKey) -> btc::PublicKey { 102 | let btc_keypair = btc::gen_keypair(network); 103 | self.key_pool.insert(btc_keypair.0, btc_keypair.1); 104 | self.inner.insert(service_key, btc_keypair.0); 105 | btc_keypair.0 106 | } 107 | 108 | fn anchoring_keys(&self) -> Vec { 109 | self.inner 110 | .iter() 111 | .map(|(&service_key, &bitcoin_key)| AnchoringKeys { 112 | bitcoin_key, 113 | service_key, 114 | }) 115 | .collect() 116 | } 117 | 118 | fn anchoring_keypairs(&self) -> Vec<(btc::PublicKey, btc::PrivateKey)> { 119 | self.inner 120 | .iter() 121 | .map(|(_, &bitcoin_key)| (bitcoin_key, self.private_key(&bitcoin_key))) 122 | .collect() 123 | } 124 | 125 | fn private_key(&self, pk: &btc::PublicKey) -> btc::PrivateKey { 126 | self.key_pool[pk].clone() 127 | } 128 | } 129 | 130 | /// Convenient wrapper around testkit with the built-in bitcoin key pool for the each 131 | /// anchoring node. 132 | #[derive(Debug)] 133 | pub struct AnchoringTestKit { 134 | /// Underlying testkit instance. 135 | pub inner: TestKit, 136 | anchoring_nodes: AnchoringNodes, 137 | } 138 | 139 | /// Returns an anchoring schema instance used in Testkit. 140 | pub fn get_anchoring_schema<'a>(snapshot: &'a dyn Snapshot) -> Schema { 141 | Schema::new(snapshot.for_service(ANCHORING_INSTANCE_NAME).unwrap()) 142 | } 143 | 144 | impl AnchoringTestKit { 145 | /// Creates an anchoring testkit instance for the specified number of anchoring nodes, 146 | /// and interval between anchors. 147 | pub fn new(nodes_num: u16, anchoring_interval: u64) -> Self { 148 | let validator_keys = (0..nodes_num) 149 | .map(|_| gen_validator_keys()) 150 | .collect::>(); 151 | 152 | let network = Network::Testnet; 153 | let anchoring_nodes = AnchoringNodes::from_keys(Network::Testnet, &validator_keys); 154 | 155 | let anchoring_config = Config { 156 | network, 157 | anchoring_keys: anchoring_nodes.anchoring_keys(), 158 | anchoring_interval, 159 | ..Config::default() 160 | }; 161 | 162 | let inner = TestKitBuilder::validator() 163 | .with_keys(validator_keys) 164 | .with(Supervisor::simple()) 165 | .with(Spec::new(BtcAnchoringService).with_instance( 166 | ANCHORING_INSTANCE_ID, 167 | ANCHORING_INSTANCE_NAME, 168 | anchoring_config, 169 | )) 170 | .build(); 171 | 172 | Self { 173 | inner, 174 | anchoring_nodes, 175 | } 176 | } 177 | 178 | /// Returns the actual anchoring configuration. 179 | pub fn actual_anchoring_config(&self) -> Config { 180 | get_anchoring_schema(&self.inner.snapshot()).actual_config() 181 | } 182 | 183 | /// Returns the latest anchoring transaction. 184 | pub fn last_anchoring_tx(&self) -> Option { 185 | get_anchoring_schema(&self.inner.snapshot()) 186 | .transactions_chain 187 | .last() 188 | } 189 | 190 | /// Returns the proposal of the next anchoring transaction for the actual anchoring state. 191 | pub fn anchoring_transaction_proposal( 192 | &self, 193 | ) -> Option<(btc::Transaction, Vec)> { 194 | get_anchoring_schema(&self.inner.snapshot()) 195 | .actual_proposed_anchoring_transaction(self.inner.snapshot().for_core()) 196 | .map(Result::unwrap) 197 | } 198 | 199 | /// Creates signatures for each input of the proposed anchoring transaction signed by the 200 | /// specified node. 201 | pub fn create_signature_tx_for_node( 202 | &self, 203 | node: &TestNode, 204 | ) -> Result>, btc::BuilderError> { 205 | let service_keypair = node.service_keypair(); 206 | let snapshot = self.inner.snapshot(); 207 | let schema = get_anchoring_schema(&snapshot); 208 | 209 | let mut signatures = Vec::new(); 210 | if let Some(p) = schema.actual_proposed_anchoring_transaction(snapshot.for_core()) { 211 | let (proposal, proposal_inputs) = p?; 212 | 213 | let actual_config = schema.actual_state().actual_config().clone(); 214 | let bitcoin_key = actual_config 215 | .find_bitcoin_key(&service_keypair.public_key()) 216 | .unwrap() 217 | .1; 218 | let btc_private_key = self.anchoring_nodes.private_key(&bitcoin_key); 219 | 220 | let redeem_script = actual_config.redeem_script(); 221 | let mut signer = p2wsh::InputSigner::new(redeem_script); 222 | for (index, proposal_input) in proposal_inputs.iter().enumerate() { 223 | let signature = signer 224 | .sign_input( 225 | TxInRef::new(proposal.as_ref(), index), 226 | proposal_input.as_ref(), 227 | &btc_private_key.0.key, 228 | ) 229 | .unwrap(); 230 | 231 | signatures.push(service_keypair.sign_input( 232 | ANCHORING_INSTANCE_ID, 233 | SignInput { 234 | input: index as u32, 235 | input_signature: signature.into(), 236 | txid: proposal.id(), 237 | }, 238 | )); 239 | } 240 | } 241 | Ok(signatures) 242 | } 243 | 244 | /// Creates signatures for each input of the proposed anchoring transaction signed by all of 245 | /// anchoring nodes. 246 | pub fn create_signature_txs(&self) -> Vec>> { 247 | let mut signatures = Vec::new(); 248 | 249 | for anchoring_keys in self.actual_anchoring_config().anchoring_keys { 250 | let node = self 251 | .find_node_by_service_key(anchoring_keys.service_key) 252 | .unwrap(); 253 | 254 | signatures.push(self.create_signature_tx_for_node(node).unwrap()); 255 | } 256 | signatures 257 | } 258 | 259 | /// Creates the confirmation transactions with a funding transaction to the current address 260 | /// with a given amount of Satoshi. 261 | pub fn create_funding_confirmation_txs( 262 | &self, 263 | satoshis: u64, 264 | ) -> (Vec>, btc::Transaction) { 265 | let funding_transaction = create_fake_funding_transaction( 266 | &self.actual_anchoring_config().anchoring_address(), 267 | satoshis, 268 | ); 269 | ( 270 | self.create_funding_confirmation_txs_with(funding_transaction.clone()), 271 | funding_transaction, 272 | ) 273 | } 274 | 275 | /// Creates the confirmation transactions with a specified funding transaction. 276 | pub fn create_funding_confirmation_txs_with( 277 | &self, 278 | transaction: btc::Transaction, 279 | ) -> Vec> { 280 | let add_funds = AddFunds { transaction }; 281 | self.actual_anchoring_config() 282 | .anchoring_keys 283 | .into_iter() 284 | .map(move |anchoring_keys| { 285 | let node_keypair = self 286 | .find_node_by_service_key(anchoring_keys.service_key) 287 | .expect("Unable to find node by service key") 288 | .service_keypair(); 289 | 290 | node_keypair.add_funds(ANCHORING_INSTANCE_ID, add_funds.clone()) 291 | }) 292 | .collect() 293 | } 294 | 295 | /// Creates configuration change transaction for simple supervisor. 296 | pub fn create_config_change_tx(&self, proposal: ConfigPropose) -> Verified { 297 | let initiator_id = self.inner.network().us().validator_id().unwrap(); 298 | let keypair = self.inner.validator(initiator_id).service_keypair(); 299 | keypair.propose_config_change(SUPERVISOR_INSTANCE_ID, proposal) 300 | } 301 | 302 | /// Adds a new auditor node to the testkit network and create Bitcoin keypair for it. 303 | pub fn add_node(&mut self) -> AnchoringKeys { 304 | let service_key = self 305 | .inner 306 | .network_mut() 307 | .add_node() 308 | .service_keypair() 309 | .public_key(); 310 | let bitcoin_key = self 311 | .anchoring_nodes 312 | .add_node(self.actual_anchoring_config().network, service_key); 313 | 314 | AnchoringKeys { 315 | bitcoin_key, 316 | service_key, 317 | } 318 | } 319 | 320 | /// Returns a corresponding private Bitcoin key. 321 | pub fn node_private_key(&self, public_key: &btc::PublicKey) -> btc::PrivateKey { 322 | self.anchoring_nodes.private_key(public_key) 323 | } 324 | 325 | /// Generates bitcoin keypair and adds them to the key pool. 326 | pub fn gen_bitcoin_key(&mut self) -> btc::PublicKey { 327 | let keypair = btc::gen_keypair(self.actual_anchoring_config().network); 328 | self.anchoring_nodes.key_pool.insert(keypair.0, keypair.1); 329 | keypair.0 330 | } 331 | 332 | /// Returns the block hash for the given blockchain height. 333 | pub fn block_hash_on_height(&self, height: Height) -> Hash { 334 | self.inner 335 | .snapshot() 336 | .for_core() 337 | .block_hashes_by_height() 338 | .get(height.0) 339 | .unwrap() 340 | } 341 | 342 | /// Returns Bitcoin key pairs of anchoring nodes. 343 | pub fn anchoring_keypairs( 344 | &self, 345 | ) -> impl IntoIterator { 346 | self.anchoring_nodes.anchoring_keypairs() 347 | } 348 | 349 | /// Finds anchoring node with the specified bitcoin key. 350 | pub fn find_anchoring_node(&self, bitcoin_key: &btc::PublicKey) -> Option<&TestNode> { 351 | self.anchoring_nodes 352 | .inner 353 | .iter() 354 | .find_map(|keypair| { 355 | if keypair.1 == bitcoin_key { 356 | Some(*keypair.0) 357 | } else { 358 | None 359 | } 360 | }) 361 | .and_then(|service_key| self.find_node_by_service_key(service_key)) 362 | } 363 | 364 | fn find_node_by_service_key(&self, service_key: PublicKey) -> Option<&TestNode> { 365 | self.inner 366 | .network() 367 | .nodes() 368 | .iter() 369 | .find(|node| node.service_keypair().public_key() == service_key) 370 | } 371 | } 372 | 373 | impl Default for AnchoringTestKit { 374 | /// Creates anchoring testkit instance with the unspent funding transaction. 375 | /// 376 | /// To add funds, this instance commit a block with transactions, so in addition to the 377 | /// genesis block this instance contains one more. 378 | fn default() -> Self { 379 | let mut testkit = Self::new(4, 5); 380 | testkit 381 | .inner 382 | .create_block_with_transactions(testkit.create_funding_confirmation_txs(700_000).0); 383 | testkit 384 | } 385 | } 386 | 387 | #[async_trait] 388 | impl PublicApi for TestKitApiClient { 389 | type Error = api::Error; 390 | 391 | async fn actual_address(&self) -> api::Result { 392 | self.public(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 393 | .get("address/actual") 394 | .await 395 | } 396 | 397 | async fn following_address(&self) -> api::Result> { 398 | self.public(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 399 | .get("address/following") 400 | .await 401 | } 402 | 403 | async fn find_transaction(&self, height: Option) -> api::Result { 404 | self.public(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 405 | .query(&FindTransactionQuery { height }) 406 | .get("find-transaction") 407 | .await 408 | } 409 | 410 | async fn config(&self) -> api::Result { 411 | self.public(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 412 | .get("config") 413 | .await 414 | } 415 | } 416 | 417 | #[async_trait] 418 | impl PrivateApi for TestKitApiClient { 419 | type Error = api::Error; 420 | 421 | async fn sign_input(&self, sign_input: SignInput) -> api::Result { 422 | self.private(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 423 | .query(&sign_input) 424 | .post("sign-input") 425 | .await 426 | } 427 | 428 | async fn add_funds(&self, transaction: btc::Transaction) -> api::Result { 429 | self.private(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 430 | .query(&transaction) 431 | .post("add-funds") 432 | .await 433 | } 434 | 435 | async fn anchoring_proposal(&self) -> api::Result { 436 | self.private(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 437 | .get("anchoring-proposal") 438 | .await 439 | } 440 | 441 | async fn config(&self) -> api::Result { 442 | self.private(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 443 | .get("config") 444 | .await 445 | } 446 | 447 | async fn transaction_with_index(&self, index: u64) -> api::Result> { 448 | self.private(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 449 | .query(&IndexQuery { index }) 450 | .get("transaction") 451 | .await 452 | } 453 | 454 | async fn transactions_count(&self) -> api::Result { 455 | self.private(ApiKind::Service(ANCHORING_INSTANCE_NAME)) 456 | .get("transactions-count") 457 | .await 458 | } 459 | } 460 | 461 | /// Proof validation extension. 462 | pub trait ValidateProof { 463 | /// Output value. 464 | type Output; 465 | /// Perform the proof validation procedure with the given exonum blockchain configuration. 466 | fn validate(self, validator_keys: &[PublicKey]) -> anyhow::Result; 467 | } 468 | 469 | impl ValidateProof for TransactionProof { 470 | type Output = Option<(u64, btc::Transaction)>; 471 | 472 | fn validate(self, validator_keys: &[PublicKey]) -> anyhow::Result { 473 | self.index_proof.verify(validator_keys)?; 474 | 475 | let entry = self 476 | .transaction_proof 477 | .check()? 478 | .entries() 479 | .iter() 480 | .cloned() 481 | .next(); 482 | Ok(entry) 483 | } 484 | } 485 | --------------------------------------------------------------------------------