diff --git a/.circleci/config.yml b/.circleci/config.yml index af52f87c53..34a04e858b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2 defaults: - rust_image: &rust_image quay.io/tarilabs/rust_tari-build-zmq:nightly-2019-03-08 + rust_image: &rust_image quay.io/tarilabs/rust_tari-build-with-deps:nightly-2019-08-21 jobs: test-docs: @@ -70,10 +70,11 @@ jobs: - run: name: Tari source code command: | - TOOLCHAIN_VERSION=nightly-2019-03-08 + TOOLCHAIN_VERSION=nightly-2019-08-21 rustup component add --toolchain $TOOLCHAIN_VERSION rustfmt cargo fmt --all -- --check cargo test --all + cargo test --release workflows: version: 2 diff --git a/.gitignore b/.gitignore index 814895f79a..0984cbb3de 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,7 @@ report Cargo.lock *.log + +# Ignore DataStore and Database files +*.mdb +/data/ diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index a1917725cf..0000000000 --- a/Cargo.lock +++ /dev/null @@ -1,1263 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "arrayvec" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "atty" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "autocfg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "backtrace" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "backtrace-sys" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "base64" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "bincode" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "bitflags" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "bitflags" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "blake2" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "block-buffer" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "block-padding 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "block-padding" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "blockchain" -version = "0.0.1" -dependencies = [ - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_core 0.0.1", -] - -[[package]] -name = "bulletproofs" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "curve25519-dalek 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "merlin 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "subtle 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "byteorder" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "case" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "cast" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "cc" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "cfg-if" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "chrono" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "clap" -version = "2.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "clear_on_drop" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "criterion" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", - "criterion-plot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "csv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_xoshiro 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon-core 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", - "tinytemplate 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "walkdir 2.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "criterion-plot" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-deque" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-queue" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-utils" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crypto-mac" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "csv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "csv-core 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "csv-core" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "curve25519-dalek" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "packed_simd 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "subtle 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "da_core" -version = "0.0.1" - -[[package]] -name = "derive-error" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "case 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "digest" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "either" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "error-chain" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "failure" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "backtrace 0.3.30 (registry+https://github.com/rust-lang/crates.io-index)", - "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "failure_derive" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)", - "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "gcc" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "generic-array" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "itertools" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "itoa" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "keccak" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "keymanager" -version = "0.0.1" -dependencies = [ - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_crypto 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_utilities 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "lazy_static" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "libc" -version = "0.2.50" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "liblmdb-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "lmdb-zero" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "liblmdb-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "supercow 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "log" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "log" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "memchr" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "memoffset" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "mempool" -version = "0.0.1" - -[[package]] -name = "merklemountainrange" -version = "0.0.1" -dependencies = [ - "blake2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_utilities 0.0.2", -] - -[[package]] -name = "merlin" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "metadeps" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mining" -version = "0.0.1" - -[[package]] -name = "nodrop" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "num-integer" -version = "0.1.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "num-traits" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "num-traits" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "num_cpus" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "numtoa" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "opaque-debug" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "p2p" -version = "0.0.1" - -[[package]] -name = "packed_simd" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "pkg-config" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "proc-macro2" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "quote" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "quote" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_core" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_xoshiro" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rayon" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", - "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon-core 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rayon-core" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "redox_syscall" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "redox_termios" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rmp" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rmp-serde" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rmp 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "ryu" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "same-file" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "scopeguard" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "serde" -version = "1.0.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "serde_derive" -version = "1.0.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "serde_json" -version = "1.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "sha2" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "block-buffer 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "sha3" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "block-buffer 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "subtle" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "subtle" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "supercow" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "syn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "syn" -version = "0.15.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "synom" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "synstructure" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tari_comms" -version = "0.0.1" -dependencies = [ - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_crypto 0.0.2", - "zmq 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tari_core" -version = "0.0.1" -dependencies = [ - "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "curve25519-dalek 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "rmp-serde 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_crypto 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_infra_derive 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_utilities 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tari_crypto" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "blake2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "curve25519-dalek 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_utilities 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tari_crypto" -version = "0.0.2" -dependencies = [ - "bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "blake2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bulletproofs 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "curve25519-dalek 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "merlin 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tari_utilities 0.0.2", -] - -[[package]] -name = "tari_infra_derive" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tari_storage" -version = "0.0.1" -dependencies = [ - "bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lmdb-zero 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "rmp 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)", - "rmp-serde 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tari_utilities" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tari_utilities" -version = "0.0.2" -dependencies = [ - "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "rmp-serde 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "termion" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "time" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tinytemplate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "toml" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "typenum" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unicode-width" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unicode-xid" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "walkdir" -version = "2.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "winapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winapi-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "zmq" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "zmq-sys 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "zmq-sys" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", - "metadeps 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[metadata] -"checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71" -"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" -"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799" -"checksum backtrace 0.3.30 (registry+https://github.com/rust-lang/crates.io-index)" = "ada4c783bb7e7443c14e0480f429ae2cc99da95065aeab7ee1b81ada0419404f" -"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6" -"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -"checksum bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9f04a5e50dc80b3d5d35320889053637d15011aed5e66b66b37ae798c65da6f7" -"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" -"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" -"checksum blake2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "91721a6330935673395a0607df4d49a9cb90ae12d259f1b3e0a3f6e1d486872e" -"checksum block-buffer 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49665c62e0e700857531fa5d3763e91b539ff1abeebd56808d378b495870d60d" -"checksum block-padding 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d75255892aeb580d3c566f213a2b6fdc1c66667839f45719ee1d30ebf2aea591" -"checksum bulletproofs 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3bc1edb4bc09df7d4afdcf9576bcdc70c80aeec096042bee80ea6b5d62e1621f" -"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -"checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb" -"checksum case 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e88b166b48e29667f5443df64df3c61dc07dc2b1a0b0d231800e07f09a33ecc1" -"checksum cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "926013f2860c46252efceabb19f4a6b308197505082c609025aa6706c011d427" -"checksum cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)" = "c9ce8bb087aacff865633f0bd5aeaed910fe2fe55b55f4739527f2e023a2e53d" -"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" -"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" -"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" -"checksum clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "97276801e127ffb46b66ce23f35cc96bd454fa311294bced4bbace7baa8b1d17" -"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -"checksum criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0363053954f3e679645fc443321ca128b7b950a6fe288cf5f9335cc22ee58394" -"checksum criterion-plot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76f9212ddf2f4a9eb2d401635190600656a1f88a932ef53d06e7fa4c7e02fb8e" -"checksum crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "05e44b8cf3e1a625844d1750e1f7820da46044ff6d28f4d43e455ba3e5bb2c13" -"checksum crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "04c9e3102cc2d69cd681412141b390abd55a362afc1540965dad0ad4d34280b4" -"checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" -"checksum crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f8306fcef4a7b563b76b7dd949ca48f52bc1141aa067d2ea09565f3e2652aa5c" -"checksum crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" -"checksum csv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9044e25afb0924b5a5fc5511689b0918629e85d68ea591e5e87fbf1e85ea1b3b" -"checksum csv-core 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa5cdef62f37e6ffe7d1f07a381bc0db32b7a3ff1cac0de56cb0d81e71f53d65" -"checksum curve25519-dalek 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e1f8a6fc0376eb52dc18af94915cc04dfdf8353746c0e8c550ae683a0815e5c1" -"checksum derive-error 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ec098440b29ea3b1ece3e641bac424c19cf996779b623c9e0f2171495425c2c8" -"checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c" -"checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b" -"checksum error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" -"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" -"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" -"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" -"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" -"checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" -"checksum generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c0f28c2f5bfb5960175af447a2da7c18900693738343dc896ffbcabd9839592" -"checksum itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8467d9c1cebe26feb08c640139247fac215782d35371ade9a2136ed6085358" -"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" -"checksum keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" -"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" -"checksum libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "aab692d7759f5cd8c859e169db98ae5b52c924add2af5fbbca11d12fefb567c1" -"checksum liblmdb-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "feed38a3a580f60bf61aaa067b0ff4123395966839adeaf67258a9e50c4d2e49" -"checksum lmdb-zero 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "13416eee745b087c22934f35f1f24da22da41ba2a5ce197143d168ce055cc58d" -"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" -"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" -"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39" -"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3" -"checksum merlin 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8c39467de91b004f5b9c06fac5bbc8e7d28309a205ee66905166b70804a71fea" -"checksum metadeps 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b122901b3a675fac8cecf68dcb2f0d3036193bc861d1ac0e1c337f7d5254c2" -"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" -"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" -"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" -"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" -"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" -"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" -"checksum opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "93f5bb2e8e8dec81642920ccff6b61f1eb94fa3020c5a325c9851ff604152409" -"checksum packed_simd 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a85ea9fc0d4ac0deb6fe7911d38786b32fc11119afd9e9d38b84ff691ce64220" -"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" -"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915" -"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" -"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1" -"checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" -"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -"checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0" -"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -"checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -"checksum rand_xoshiro 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "03b418169fb9c46533f326efd6eed2576699c44ca92d3052a066214a8d828929" -"checksum rayon 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a4b0186e22767d5b9738a05eab7c6ac90b15db17e5b5f9bd87976dd7d89a10a4" -"checksum rayon-core 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbe0df8435ac0c397d467b6cad6d25543d06e8a019ef3f6af3c384597515bd2" -"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85" -"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" -"checksum rmp 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a3d45d7afc9b132b34a2479648863aa95c5c88e98b32285326a6ebadc80ec5c9" -"checksum rmp-serde 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)" = "011e1d58446e9fa3af7cdc1fb91295b10621d3ac4cb3a85cc86385ee9ca50cd3" -"checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" -"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" -"checksum same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8f20c4be53a8a1ff4c1f1b2bd14570d2f634628709752f0702ecdd2b3f9a5267" -"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" -"checksum serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "92514fb95f900c9b5126e32d020f5c6d40564c27a5ea6d1d7d9f157a96623560" -"checksum serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6eabf4b5914e88e24eea240bb7c9f9a2cbc1bbbe8d961d381975ec3c6b806c" -"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d" -"checksum sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4d8bfd0e469f417657573d8451fb33d16cfe0989359b93baf3a1ffc639543d" -"checksum sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" -"checksum subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" -"checksum subtle 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "702662512f3ddeb74a64ce2fbbf3707ee1b6bb663d28bb054e0779bbc720d926" -"checksum supercow 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "171758edb47aa306a78dfa4ab9aeb5167405bd4e3dc2b64e88f6a84bbe98bd63" -"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" -"checksum syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1825685f977249735d510a242a6727b46efe914bb67e38d30c071b1b72b1d5c2" -"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" -"checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" -"checksum tari_crypto 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cc90db87150c5d6575d7a5560ff6121d9e4067e183cca756462a1dd0d849c273" -"checksum tari_infra_derive 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f347f846e83aa14207a979a0591d54e8a06168566f6e68f9900e6d671c352a82" -"checksum tari_utilities 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0d09a588c21d8dcc826d3c1fc0ada39a4242bf436452c3cae8f7e8b588821431" -"checksum termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8fb22f7cde82c8220e5aeacb3258ed7ce996142c77cba193f203515e26c330" -"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" -"checksum tinytemplate 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4574b75faccaacddb9b284faecdf0b544b80b6b294f3d062d325c5726a209c20" -"checksum toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "736b60249cb25337bc196faa43ee12c705e426f3d55c214d73a4e7be06f92cb4" -"checksum typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "612d636f949607bdf9b123b4a6f6d966dedf3ff669f7f045890d3a4a73948169" -"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" -"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" -"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" -"checksum walkdir 2.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c7904a7e2bb3cdf0cf5e783f44204a85a37a93151738fa349f06680f59a98b45" -"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum zmq 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e6e33f05ebc9a1cb360e5db1f8ed6e5512ece86aed271654b0f171d04c24c23" -"checksum zmq-sys 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3cc251d25f3c6ffc54dfa3e8d808598825f8ccfee3a008dfc7866ffe325dcb3" diff --git a/Cargo.toml b/Cargo.toml index aa095fbae3..0498c51b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,17 @@ [workspace] members = [ - "base_layer/blockchain", "base_layer/core", "base_layer/keymanager", - "base_layer/mempool", + "base_layer/mmr", "base_layer/mining", "base_layer/p2p", + "base_layer/service_framework", + "base_layer/wallet", + "comms", "digital_assets_layer/core", - "infrastructure/comms", "infrastructure/crypto", "infrastructure/storage", - "infrastructure/merklemountainrange" + "applications/grpc_wallet", + "applications/console_text_messenger", ] diff --git a/Contributing.md b/Contributing.md new file mode 100644 index 0000000000..0df31c606a --- /dev/null +++ b/Contributing.md @@ -0,0 +1,15 @@ +# Formatting + +PRs are checked for formatting using the command: +``` +TOOLCHAIN_VERSION=nightly-2019-07-15 +rustup component add --toolchain $TOOLCHAIN_VERSION rustfmt +cargo fmt --all -- --check +``` + +You can automatically format the code using the command: +``` +cargo fmt --all +``` + +The toolchain version may change from time to time, so check in `.circlecit/config.yml` for the latest toolchain version \ No newline at end of file diff --git a/Makefile b/Makefile index ed658c1f89..cdd453dd21 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ +PACKAGES = tari_crypto tari_core tari_utilities tari_comms doc: - cargo rustdoc -p crypto --open -- --html-in-header meta/assets/rustdoc-include-katex-header.html + $(foreach p,$(PACKAGES),cargo rustdoc -p $(p) -- --html-in-header meta/assets/rustdoc-include-js-header.html;) doc-internal: - cargo rustdoc -p crypto -- --html-in-header docs/assets/rustdoc-include-katex-header.html --document-private-items - + $(foreach p,$(PACKAGES),cargo rustdoc -p $(p) -- --html-in-header docs/assets/rustdoc-include-js-header.html --document-private-items;) diff --git a/README.md b/README.md index c9448c0fea..fe73d15e51 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -[![Waffle.io - Columns and their card count](https://badge.waffle.io/tari-project/tari.svg?columns=Inbox,Backlog,In%20Progress,Review,Done)](https://waffle.io/tari-project/tari) [![Build](https://circleci.com/gh/tari-project/tari.svg?style=svg)](https://circleci.com/gh/tari-project/tari) # The Tari protocol diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index c496d06f80..b1d36eb0b8 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -5,8 +5,8 @@ THings to do before pushing a new commit to `master`: * Create new `rc` branch off development. * Update crate version numbers * Check that all tests pass in development (`cargo test`, `cargo test --release`) -* Publish new crates to crates.io (`./scripts/publish_crates.sh`) * Rebase onto master +* Publish new crates to crates.io (`./scripts/publish_crates.sh`) * Tag commit * Write release notes on GitHub. * Merge back into development (where appropriate) diff --git a/RFC/src/RFC-0001_overview.md b/RFC/src/RFC-0001_overview.md index 8a659fc96a..e6088ab181 100644 --- a/RFC/src/RFC-0001_overview.md +++ b/RFC/src/RFC-0001_overview.md @@ -1,14 +1,14 @@ # RFC-0001/Overview -## An overview of the Tari network +## Overview of Tari Network ![status: draft](theme/images/status-draft.svg) **Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2018 The Tari Development Community @@ -22,35 +22,35 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -The aim of this proposal is to provide a very high-level perspective for the moving parts involved in the Tari protocol. +The aim of this proposal is to provide a very high-level perspective of the moving parts of the Tari protocol. -## Related RFCs +## Related Requests for Comment * [RFC-0100: Base layer](RFC-0100_BaseLayer.md) * [RFC-0300: Digital asset network](RFC-0300_DAN.md) @@ -60,27 +60,27 @@ The aim of this proposal is to provide a very high-level perspective for the mov ### Abstract -The Tari network is composed of three layers: +The Tari network comprises three layers: -1. The base layer deals with [Tari coin] [transaction]s. It governed by a proof-of-work blockchain that is merged-mined with -Monero. The base layer is highly secure, decentralised and relatively slow. -2. A multiparty payments channel allows rapid, secure, low cost off-chain payments that are periodically settled on the +1. A base layer that deals with [Tari coin] [transaction]s. It governed by a proof-of-work (PoW) blockchain that is merged-mined with +Monero. The base layer is highly secure, decentralized and relatively slow. +2. A multiparty payments channel that allows rapid, secure, low-cost, off-chain payments that are periodically settled on the base layer. -3. The digital assets network. This layer manages all things to do with native digital assets. It is built for liveness, - speed and scalability at the expense of decentralisation. +3. A digital assets network (DAN) that manages all things to do with native digital assets. It is built for liveness, + speed and scalability at the expense of decentralization. ![Tari Network Overview](theme/images/tari_network_overview.png) -### Currency tokens and digital assets +### Currency Tokens and Digital Assets -There are two major digital entities on the Tari network: The coins that are the unit of transfer for the Tari +There are two major digital entities on the Tari network: the coins that are the unit of transfer for the Tari cryptocurrency, and the digital assets that could represent anything from tickets to in-game items. -Tari coins are the fuel that drives the entire Tari ecosystem. They share many of the properties of money, and so +Tari coins are the fuel that drives the entire Tari ecosystem. They share many of the properties of money, so security is a non-negotiable requirement. In a cryptocurrency context, this is usually achieved by employing a -decentralised network running a censorship-resistant protocol like Nakamoto consensus over a proof of work blockchain. -As we know, proof of work blockchains are not scalable, or terribly fast. +decentralized network running a censorship-resistant protocol such as Nakamoto consensus over a proof-of-work blockchain. +As we know, PoW blockchains are not scalable or very fast. On the other hand, the Tari network will be used to create and manage digital assets. @@ -88,146 +88,146 @@ In Tari parlance, a digital asset is defined as a finite set of digital stateful rules. A single digital asset may define anything from one to thousands of tokens within its scope. For example, in a ticketing context, an _event_ will be an asset. The asset definition will allocate tokens representing -the tickets for that event. The ticket tokens will have state, such as its current owner and whether it has been -redeemed or not. Users might be interacting with digital assets hundreds of times a second, and state updates need to be +the tickets for that event. The ticket tokens will have state, such as its current owner and whether or not it has been +redeemed. Users might be interacting with digital assets hundreds of times a second, and state updates need to be propagated and agreed upon by the network very quickly. A blockchain-enabled ticketing system is practically useless if -a user has to wait for "3 block confirmations" before the bouncer will let her into a venue. Users expect near-instant -state updates because centralised solutions offer them that today. +a user has to wait for "three block confirmations" before the bouncer will let her into a venue. Users expect near-instant +state updates because centralized solutions offer them that today. -Therefore the Tari digital assets network must offer speed and scalability. +Therefore the Tari DAN must offer speed and scalability. -#### Multiple layers +#### Multiple Layers The [distributed system trilemma](https://en.wikipedia.org/wiki/CAP_theorem) tells us that these requirements are - mutually exclusive. +mutually exclusive. -We can't have fast, cheap digital assets and also highly secure and decentralised currency tokens on a single system. +We can't have fast, cheap digital assets and also highly secure and decentralized currency tokens on a single system. Tari overcomes this constraint by building three layers: -* A base layer that provides a public ledger of Tari coin transactions, secured by proof of work to maximise security, -* A multiparty payment channel, allowing funds to be sent to parties in the channel instantly, securely and with very - low fees. -* A digital asset layer that manages digital asset state that is very fast and cheap, at the expense of - decentralisation. +1. A base layer that provides a public ledger of Tari coin transactions, secured by PoW to maximize security. +2. A multiparty payment channel that allows funds to be sent to parties in the channel instantly, securely and with very + low fees. +3. A DAN that manages the state of digital assets. It is very fast and cheap, at the expense of + decentralization. -If required, the digital asset layer can refer back to the base layer to temporarily give up speed in exchange for -increased security. This fallback is used to resolve consensus issues on the digital asset layer that may crop up from -time to time as a result of the lower degree of decentralisation. +If required, the digital assets layer can refer back to the base layer to temporarily give up speed in exchange for +increased security. This fallback is used to resolve consensus issues on the digital assets layer that may crop up from +time to time as a result of the lower degree of decentralization. -### The Base Layer +### Base Layer _Refer to [RFC-0100/BaseLayer](RFC-0100_BaseLayer.md) for more detail_. The Tari base layer has the following primary features: -* Proof of work-based blockchain using Nakamoto consensus +* PoW-based blockchain using Nakamoto consensus * Transactions and blocks based on the [Mimblewimble] protocol [Mimblewimble] is an exciting new blockchain protocol that offers some key advantages over other [UTXO]-based -cryptocurrencies like Bitcoin: +cryptocurrencies such as Bitcoin: * Transactions are private. This means that casual observers cannot ascertain the amounts being transferred or the identities of the parties involved. -* Mimblewimble employs a novel blockchain "compression" method called cut-through that dramatically reduces the +* Mimblewimble employs a novel blockchain "compression" method called cut-through, which dramatically reduces the storage requirements for blockchain nodes. * Multi-signature transactions can be easily aggregated, making such transactions very compact, and completely hiding the parties involved, or the fact that there were multiple parties involved at all. > "Mimblewimble is the most sound, scalable 'base layer' protocol we know" -- @fluffypony -#### Proof of work +#### Proof of Work -There are a few options for the proof of work mechanism for Tari: +There are a few options for the PoW mechanism for Tari: * Implement an existing PoW mechanism. This is a bad idea, because a nascent cryptocurrency that uses a non-unique mining algorithm is incredibly vulnerable to a 51% attack from miners from other currencies using the same algorithm. Bitcoin Gold and Verge have already experienced this, and it's a [matter of time](https://www.crypto51.app/) before it happens to others. -* Implement a unique PoW algorithm. This is a risky approach and comes close to breaking the #1 rule of - cryptocurrency design: Never roll your own crypto. -* [Merge mining](https://tari-labs.github.io/tari-university/merged-mining/merged-mining-scene/MergedMiningIntroduction.html). - This approach is not without its own risks but offers the best trade-offs in terms of bootstrapping the network. It - typically provides high levels of hash rate from day one along with 51% attack resistance assuming mining pools are +* Implement a unique PoW algorithm. This is a risky approach and comes close to breaking the number one rule of + cryptocurrency design: never roll your own crypto. +* [Merged mining](https://tari-labs.github.io/tari-university/merged-mining/merged-mining-scene/MergedMiningIntroduction.html). + This approach is not without its own risks, but offers the best trade-offs in terms of bootstrapping the network. It + typically provides high levels of hash rate from day one, along with 51% attack resistance, assuming mining pools are well-distributed. -* A hybrid approach, utilising two or more of the above mechanisms. +* A hybrid approach, utilizing two or more of the above mechanisms. -Given Tari's relationship with Monero, a merge-mined strategy with Monero makes the most sense, but the PoW mechanism +Given Tari's relationship with Monero, a merged-mining strategy with Monero makes the most sense. However, the PoW mechanism SHOULD be written in a way that makes it relatively easy to code, implement and switch to a different strategy in the future. -### Multiparty Payment Channels +### Multiparty Payment Channel Further details about the Tari multiparty payment channel technology are given in [RFC-500/PaymentChannels](RFC-0500_PaymentChannels.md). -### The Digital Assets Network +### Digital Assets Network -A more detailed proposal for the digital assets network is presented in [RFC-0300/DAN](RFC-0300_DAN.md). Digital assets +A more detailed proposal for the DAN is presented in [RFC-0300/DAN](RFC-0300_DAN.md). Digital assets _are discussed in more detail in [RFC-0310/Assets](RFC-0311_AssetTemplates.md)._ -The Tari digital assets network (DAN) consists of a peer-to-peer network of [Validator nodes]. These nodes ensure the +The Tari DAN consists of a peer-to-peer network of [Validator nodes]. These nodes ensure the safe and efficient operation of all native digital assets on the Tari network. -Validator nodes are responsible for +Validator nodes are responsible for: -* _registering_ themselves on the base layer. -* validating and executing the contracts that _create_ and issue _new digital assets_ on the network. -* validating and executing _instructions_ for _changes in state_ of digital assets, for example allowing the transfer of +* _Registering_ themselves on the base layer. +* Validating and executing the contracts that _create_ and issue _new digital assets_ on the network. +* Validating and executing _instructions_ for _changes in state_ of digital assets, e.g. allowing the transfer of ownership of a token from one person to another. * _Maintaining consensus_ with other validator nodes managing the same asset. -* submitting periodic _checkpoints_ to the base layer for the state of assets under their management. +* Submitting periodic _checkpoints_ to the base layer for the state of assets under their management. The DAN is focused on achieving high speed and scalability, without compromising on security. To achieve -this we make the explicit trade-off of sacrificing decentralisation. +this, we make the explicit trade-off of sacrificing decentralization. In many ways this is desirable, since the vast majority of assets (and their issuers) don't need or want _the entire network_ to validate every state change in their asset contracts. -Digital assets necessarily have _state_. Therefore the digital assets layer must have a means of synchronising and -agreeing on state that is managed simultaneously by multiple servers (a.k.a. reaching consensus). +Digital assets necessarily have _state_. Therefore the digital assets layer must have a means of synchronizing and +agreeing on state that is managed simultaneously by multiple servers, i.e. reaching consensus. -Please refer to Tari Labs University for detailed discussions on +Please refer to Tari Labs University (TLU) for detailed discussions on [layer 2 scaling solutions](https://tlu.tarilabs.com/layer2scaling/layer2scaling-landscape/layer2scaling-survey.html) and [consensus mechanisms](https://tlu.tarilabs.com/consensus-mechanisms/BFT-consensus-mechanisms-applications/Introduction.html). -### Interaction between the base layer and the DAN +### Interaction between Base Layer and Digital Assets Network -The base layer provides supporting services to the digital asset network. In general, the base layer only knows about +The base layer provides supporting services to the DAN. In general, the base layer only knows about Tari coin transactions. It knows nothing about the details of any digital assets and their state. -This is by design: The network cannot scale if details of digital asset contracts have to be tracked on the base layer. +This is by design: the network cannot scale if details of digital asset contracts have to be tracked on the base layer. We envisage that there could be tens of thousands of contracts deployed on Tari. Some of those contracts may be enormous; -imagine controlling every piece of inventory and their live statistics for a MMORPG. The base layer is also too slow. If -_any_ state relies on base layer transactions being confirmed, there is an immediate lag before that state change can be -considered final, which kills the liveness properties we seek for the DAN. +imagine controlling every piece of inventory and their live statistics for a massively multiplayer online role-playing +game (MMORPG). The base layer is also too slow. If _any_ state relies on base layer transactions being confirmed, there +is an immediate lag before that state change can be considered final, which kills the liveness properties we seek for the DAN. -It's better to keep the two networks almost totally decoupled from the outset, and allow each network to play to its +It is better to keep the two networks almost totally decoupled from the outset, and allow each network to play to its strength. -That said, there are key interactions between the two layers. The base layer is a ledger and so it can be used as a +That said, there are key interactions between the two layers. The base layer is a ledger and can be used as a source of truth for the DAN to use as a type of registrar as well as final court of appeal in the case of consensus -disputes. This is what gives the DAN a secure fallback in case bad actors try to manipulate asset state by taking -advantage of its non-decentralisation. +disputes. This is what gives the DAN a secure fallback should bad actors try to manipulate asset state by taking +advantage of its non-decentralization. These interactions require making provision for additional transaction types, in addition to payment and coinbase -transactions, that mark validator node registrations, contract collateral and so on. +transactions, which mark validator node registrations, contract collateral, etc. -The interplay between base layer and DAN is what incentivises every actor in the system to maintain an efficient and -well-functioning network even while acting in their own self-interests. +The interplay between base layer and DAN is what incentivizes every actor in the system to maintain an efficient and +well-functioning network, even while acting in their own self-interest. ### Summary -Table 1 summarises the defining characteristics of the Tari network layers: +The following table summarizes the defining characteristics of the Tari network layers: -| | Base layer | Payment Channels | DAN | +| | Base Layer | Payment Channels | Digital Assets Network | |:-------------------------------------|:-----------------|:-----------------|:-----------------------| | Speed | Slow | Fast | Fast | | Scalability | Moderate | High | Very high | -| Security | High | High | Mod (High w/ fallback) | -| Decentralisation | High | Low - Med | Low - Med | +| Security | High | High | Moderate (High with fallback) | +| Decentralization | High | Low - Medium | Low - Med | | Processes Tari coin transactions | Yes | Yes | No | | Processes digital asset instructions | Only checkpoints | No | Yes | diff --git a/RFC/src/RFC-0010_CodeStructure.md b/RFC/src/RFC-0010_CodeStructure.md index 445596cb9b..a1849bd64c 100644 --- a/RFC/src/RFC-0010_CodeStructure.md +++ b/RFC/src/RFC-0010_CodeStructure.md @@ -1,16 +1,16 @@ # RFC-0010/CodeStructure -## Tari code structure and organisation +## Tari Code Structure and Organization ![status: draft](theme/images/status-draft.svg) **Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). -Copyright +Copyright 2018 The Tari Development Community Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -22,102 +22,101 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This RFC describes and explains the Tari codebase layout. +The aim of this Request for Comment (RFC) is to describe and explain the Tari codebase layout. -## Related RFCs +## Related Requests for Comment None. ## Description -The code follows a Domain-Driven Design layout ([DDD]), with top-level directories falling into infrastructure, domain +The code follows a Domain-driven Design ([DDD]) layout, with top-level directories falling into infrastructure, domain and application layers. -### Infrastructure layer +### Infrastructure Layer -This layer provides a set of crates which have general infrastructural utility. The rest of the Tari codebase can make use +The infrastructure layer provides a set of crates that have general infrastructural utility. The rest of the Tari codebase can make use of these crates to obtain persistence, communication and cryptographic services. The infrastructure layer doesn't know -anything about blockchains, transactions, or digital assets. +anything about blockchains, transactions or digital assets. -We recommend that code in this layer generalises infrastructure services behind abstraction layers as much as is +We recommend that code in this layer generalizes infrastructure services behind abstraction layers as much as is reasonable, so that specific implementations can be swapped out with relative ease. -### Domain layer +### Domain Layer -The Domain layer houses the Tari "business logic". All protocol-related concepts and procedures are defined and +The domain layer houses the Tari "business logic". All protocol-related concepts and procedures are defined and implemented here. -This entails that any and all terms defined in the [Glossary] will have a software implementation here, and only here. -They can be _used_ in the Application layer, but must be implemented in the Domain layer. +This means that any and all terms defined in the [Glossary] will have a software implementation here, and only here. +They can be _used_ in the application layer, but must be *implemented* in the domain layer. The domain layer can make use of crates in the infrastructure layer to achieve its goals. -### Application layer +### Application Layer -Applications build on top of the domain layer to produce the executable software that is deployed as part of the Tari -network. +In the application layer, applications build on top of the domain layer to produce the executable software that is +deployed as part of the Tari network. As an example, the following base layer applications may be developed as part of the Tari protocol release: * A standalone miner (tari_miner) * A pool miner (tari_pool_miner) -* A CLI wallet for the Tari cryptocurrency (cli_wallet) +* A Command Line Interface (CLI) wallet for the Tari cryptocurrency (cli_wallet) * A base node executable (tari_basenode) -* A REST API server for the base node +* A REST Application Programming Interface (API) server for the base node -### Code layout +### Code Layout The top-level source code directories in this repository reflect the respective [DDD] layers; except that there are two domain layer directories, corresponding to the two network layers that make up the Tari network. 1. The `infrastructure` directory contains application-layer code and is not Tari-specific. It holds the following crates: - - `comms`: The networking and messaging subsystem - - `crypto`: All cryptographic services, including a Curve25519 implementation - - `storage`: Data persistence services, including an LMDB persistence implementation - - `merklemountainrange`: An independant implementation of a merkle mountain range - - `derive`: A crate to contain `derive(...)` macros + - `comms` - the networking and messaging subsystem; + - `crypto` - all cryptographic services, including a Curve25519 implementation; + - `storage` - data persistence services, including a Lightning Memory-mapped Database (LMDB) persistence implementation; + - `merklemountainrange` - an independent implementation of a Merkle Mountain Range; + - `derive` - a crate to contain `derive(...)` macros. 1. `base_layer` is a domain-layer directory and contains: - - `blockchain`: The Tari consensus code - - `core`: common classes and traits, such as [Transaction]s and [Block]s - - `mempool`: The unconfirmed transaction pool implementation - - `mining`: The merge-mining modules - - `p2p`: The block and transaction propagation module - - `api`: interfaces for clients and wallets to interact with the base layer components + - `blockchain` - the Tari consensus code; + - `core` - common classes and traits, such as [Transaction]s and [Block]s; + - `mempool` - the unconfirmed transaction pool implementation; + - `mining` - the merged-mining modules; + - `p2p` - the block and transaction propagation module; + - `api` - interfaces for clients and wallets to interact with the base layer components. 1. `digital_assets_layer` is a domain-layer directory. It contains code related to the management of native Tari digital - assets. - - Its sub-structure is TBD. + assets. Its substructure is to be determined. 1. `applications` contains crates for all the application-layer executables that form part of the Tari codebase. [Glossary]: ../Glossary.md "Glossary" -[DDD]: https://en.wikipedia.org/wiki/Domain-driven_design 'Wikipedia: Domain Driven Design' +[DDD]: https://en.wikipedia.org/wiki/Domain-driven_design "Wikipedia: Domain Driven Design" [transaction]: ../Glossary.md#transaction [block]: ../Glossary.md#block diff --git a/RFC/src/RFC-0100_BaseLayer.md b/RFC/src/RFC-0100_BaseLayer.md index 13ffbdb122..4827d7ef60 100644 --- a/RFC/src/RFC-0100_BaseLayer.md +++ b/RFC/src/RFC-0100_BaseLayer.md @@ -6,9 +6,9 @@ **Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2018 The Tari Development Community @@ -22,35 +22,35 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document describes the major software components of the Tari Base Layer network. +The aim of this Request for Comment (RFC) is to describe the major software components of the Tari Base Layer network. -## Related RFCs +## Related Requests for Comment * [RFC-0001: Overview](RFC-0001_overview.md) * [RFC-0110: Base Nodes](./RFC-0110_BaseNodes.md) @@ -62,19 +62,20 @@ This document describes the major software components of the Tari Base Layer net The Tari Base Layer network comprises the following major pieces of software: -* Base Layer full node implementation - The base layer full nodes are the consensus-critical pieces of software for the - Tari base layer and cryptocurrency. The base nodes validate and transmit transactions and blocks and maintain +* Base Layer full node implementation. The base layer full nodes are the consensus-critical pieces of software for the + Tari base layer and cryptocurrency. The base nodes validate and transmit transactions and blocks, and maintain consensus about the longest valid proof-of-work blockchain. -* Mining software - Mining nodes perform proof-of-work to secure the base layer and compete to submit the +* Mining software. Mining nodes perform proof-of-work to secure the base layer and compete to submit the next valid block into the Tari blockchain. Tari is merge-mined with Monero. The Tari source provides two alternatives for Tari miners: * A standalone miner * A stratum-compatible pool miner. -* Wallet software - Client software and APIs offering means to construct transactions, query nodes for information and - maintain personal private keys +* Wallet software. Client software and Application Programming Interfaces (APIs) offering means to construct transactions, query nodes for information and + maintain personal private keys. -These three main pieces of software make use of common functionality provided by the following libraries within the Tari +These three major pieces of software make use of common functionality provided by the following libraries within the Tari project source code: + * Local data storage * Cryptography services * Peer-to-peer networking and messaging services diff --git a/RFC/src/RFC-0110_BaseNodes.md b/RFC/src/RFC-0110_BaseNodes.md index eac8dc6c0d..27f84fe9d9 100644 --- a/RFC/src/RFC-0110_BaseNodes.md +++ b/RFC/src/RFC-0110_BaseNodes.md @@ -4,11 +4,11 @@ ![status: draft](theme/images/status-draft.svg) -**Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) [SW van heerden](https://github.com/SWvheerden) +**Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) and [S W van heerden](https://github.com/SWvheerden) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2019 The Tari Development Community @@ -22,131 +22,137 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document describes the roles that [base node]s play in the Tari network and their general approach for doing so. +The aim of this Request for Comment (RFC) is to describe the roles that [base node]s play in the Tari network as well as +their general approach for doing so. -## Related RFCs +## Related Requests for Comment -* [RFC-0100: Base layer](RFC-0100_BaseLayer.md) +* [RFC-0100: Base Layer](RFC-0100_BaseLayer.md) +* [RFC-0140: SyncAndSeeding](RFC-0140_Syncing_and_seeding.md) ## Description -### Broad requirements +### Broad Requirements Tari Base Nodes form a peer-to-peer network for a proof-of-work based blockchain running the [Mimblewimble] -protocol. The proof of work is performed via merge mining with Monero. Arguments for this design are +protocol. The proof-of-work is performed via merge mining with Monero. Arguments for this design are presented [in the overview](RFC-0001_overview.md#proof-of-work). Tari Base Nodes MUST carry out the following tasks: -* Validate all [Tari coin] [transaction]s. -* Propagate valid transactions to peer nodes. -* Validate all new [block]s received. -* Propagate validated new blocks to peer nodes. -* Connect to peer nodes to catch up (sync) its blockchain state. -* Provide historical block information to peers that are syncing. +* validate all [Tari coin] transactions; +* propagate valid [transaction]s to peer nodes; +* validate all new [block]s received; +* propagate validated new blocks to peer nodes; +* connect to peer nodes to catch up (sync) with their blockchain state; +* provide historical block information to peers that are syncing. -Once the Digital Assets Network goes live, Base Nodes will also need to support the tasks described in +Once the Digital Assets Network (DAN) goes live, Base Nodes will also need to support the tasks described in [RFC-0300/Assets](RFC-0300_DAN.md). These requirements are omitted for the moment. To carry out these tasks effectively, Base Nodes SHOULD: -* Save the [blockchain] into a indexed local database. -* Maintain an index of all unspent outputs ([UTXO]s). -* Maintain a list of all pending, valid transactions that have not yet been mined (the [mempool]). -* Manage a list of Base Node peers present on the network. +* save the [blockchain] into an indexed local database; +* maintain an index of all Unspent Transaction Outputs ([UTXO]s); +* maintain a list of all pending, valid transactions that have not yet been mined (the [mempool]); +* manage a list of Base Node peers present on the network. -Tari Base nodes MAY implement chain pruning strategies that are features of Mimblewimble, including transaction +Tari Base Nodes MAY implement chain pruning strategies that are features of Mimblewimble, including transaction [cut-through and block compaction techniques](https://tlu.tarilabs.com/protocols/grin-protocol-overview/MainReport.html#mimblewimble-protocol-overview). -Tari Base Nodes MAY also implement the following services via an API to clients. Such clients may include "light" -clients, block explorers, wallets, and Tari applications: +Tari Base Nodes MAY also implement the following services via an Application Programming Interface (API) to clients: - * Block queries. - * Kernel data queries. - * Transaction queries. - * Submission of new transactions. +- Block queries +- Kernel data queries +- Transaction queries +- Submission of new transactions -### Transaction validation and propagation + Such clients may include "light" clients, block explorers, wallets and Tari applications. -Base nodes can be notified of new transactions by -* connected peers. +### Transaction Validation and Propagation + +Base nodes can be notified of new transactions by: +* connected peers; * clients via APIs. When a new transaction has been received, it has the `unvalidated` [ValidationState]. The transaction is then passed -to the transaction validation service, where its state will become either `rejected`, `timelocked` or `validated`. +to the transaction validation service, where its state will become `rejected`, `timelocked` or `validated`. The transaction validation service checks that: -* all inputs to the transaction are valid [UTXO]s. -* no inputs are duplicated. -* all inputs are able to be spent (they're not time-locked). -* all inputs are signed by their owners. -* all outputs have valid [range proof]s. -* no outputs currently exist in the [UTXO] set. -* the transaction does not have a [timelock](timelocks) applied, limiting it from being mined and added to the blockchain before a specified block height or timestamp has been reached. -* the transaction excess has a valid signature. -* the transaction excess is a valid public key. This proves that: - $$ \Sigma \left( \mathrm{inputs} - \mathrm{outputs} - \mathrm{fees} \right) = 0 $$ +* All inputs to the transaction are valid [UTXO]s. +* No inputs are duplicated. +* All inputs are able to be spent (they are not time-locked). +* All inputs are signed by their owners. +* All outputs have valid [range proof]s. +* No outputs currently exist in the [UTXO] set. +* The transaction does not have [timelocks] applied, limiting it from being mined and added to the blockchain before a + specified block height or timestamp has been reached. +* The transaction excess has a valid signature. +* The transaction excess is a valid public key. This proves that: + $$ \Sigma \left( \mathrm{inputs} - \mathrm{outputs} - \mathrm{fees} \right) = 0 $$. `Rejected` transactions are dropped silently. `Timelocked` transactions are: -* marked with a timelocked status and gets added to the [mempool]. -* will be evaluated again at a later state to determine if the timelock has passed and if it can be upgraded to 'Validated' status. -* More detailed information in [RFC-0230](timelocks) +* marked with a timelocked status and get added to the [mempool]; +* will be evaluated again at a later state to determine if the timelock has passed and if it can be upgraded to "Validated" status. + +**Note:** More detailed information is available in the [timelocks] RFC document. `Validated` transactions are: -* Added to the [mempool]. -* forwarded to peers using the transaction [BroadcastStrategy]. -### Block validation and propagation +* added to the [mempool]; +* forwarded to peers using the transaction [BroadcastStrategy]. -The Block validation and propagation process is analogous to that of transactions. +### Block Validation and Propagation -New blocks are received from the peer-to-peer network, or from an API call if the Base Node is connected to a Miner. +The block validation and propagation process is analogous to that of transactions. New blocks are received from the +peer-to-peer network, or from an API call if the Base Node is connected to a Miner. When a new block is received, it is assigned the `unvalidated` [ValidationState]. The block is then passed to the -block validation service. The validation service checks that - -* the block hasn't been processed before. -* every [transaction] in the block is valid. -* the proof-of-work is valid. -* the block header is well-formed. -* the block is being added to the chain with the highest accumulated proof-of-work. - * it is possible for the chain to temporarily fork; base nodes SHOULD account for forks up to some configured depth. - * it is possible that blocks may be received out of order; particularly while syncing. Base Nodes SHOULD keep blocks. - that have block heights greater than the current chain tip in memory for some preconfigured period. -* the sum of all excesses is a valid public key. This proves that: - $$ \Sigma \left( \mathrm{inputs} - \mathrm{outputs} - \mathrm{fees} \right) = 0$$ -* check if [cut-through] was applied. If a block contains already spent outputs, reject that block. - -Because MimbleWimble blocks can be simple be seen as large transactions with multiple inputs and outputs, the block validation service checks all transaction verification on the block as well. +block validation service. The validation service checks that: + +* The block has not been processed before. +* Every [transaction] in the block is valid. +* The proof-of-work is valid. +* The block header is well-formed. +* The block is being added to the chain with the highest accumulated proof-of-work. + * It is possible for the chain to temporarily fork; Base Nodes SHOULD account for forks up to some configured depth. + * It is possible that blocks may be received out of order, particularly while syncing. Base Nodes SHOULD keep blocks + that have block heights greater than the current chain tip in memory for some preconfigured period. +* The sum of all excesses is a valid public key. This proves that: + $$ \Sigma \left( \mathrm{inputs} - \mathrm{outputs} - \mathrm{fees} \right) = 0$$. +* Check if [cut-through] was applied and, if a block contains already spent outputs, reject that block. + +Because Mimblewimble blocks can simply be seen as large transactions with multiple inputs and outputs, the block +validation service checks all transaction verification on the block as well. `Rejected` blocks are dropped silently. @@ -156,25 +162,26 @@ Base Nodes are not obliged to accept connections from any peer node on the netwo * Base Nodes MAY be configured to exclusively connect to a given set of peer nodes. `Validated` blocks are -* added to the [blockchain]. +* added to the [blockchain]; * forwarded to peers using the block [BroadcastStrategy]. In addition, when a block has been validated and added to the blockchain: * The mempool MUST also remove all transactions that are present in the newly validated block. -* The UTXO set MUST be updated; removing all inputs in the block, and adding all the new outputs in it. - -### Synchronising and pruning of the chain +* The UTXO set MUST be updated by removing all inputs in the block, and adding all the new outputs into it. -Syncing, pruning and cut-through is discussed in detail in [RFC-0140](RFC-0140_Syncing.md) +### Synchronizing and Pruning of the Chain -### Archival nodes +Syncing, pruning and cut-through are discussed in detail in [RFC-0140](RFC-0140_Syncing_and_seeding.md). -[Archival nodes](archivenode) are used to keep a complete history of the blockchain since genesis block, they do not employ pruning at all. These nodes will allow full syncing of the blockchain because normal nodes will not keep the full history to enable this. +### Archival Nodes +[Archival nodes] are used to keep a complete history of the blockchain since genesis block. They do not employ pruning +at all. These nodes will allow full syncing of the blockchain, because normal nodes will not keep the full history to +enable this. -[archivenode]: Glossary.md#archivenode +[archival nodes]: Glossary.md#archive-node [tari coin]: Glossary.md#tari-coin [blockchain]: Glossary.md#blockchain [transaction]: Glossary.md#transaction @@ -189,5 +196,5 @@ Syncing, pruning and cut-through is discussed in detail in [RFC-0140](RFC-0140_S [SynchronisationStrategy]: Glossary.md#synchronisationstrategy [SynchronisationState]: Glossary.md#synchronisationstate [mining server]: Glossary.md#mining-server -[cut-through]: RFC-0140_Syncing.md#Pruning-and-cut-through -[timelocks]: RFC-0230_HTLC.md#Time-Locked-contracts +[cut-through]: RFC-0140_Syncing_and_seeding.md#pruning-and-cut-through +[timelocks]: RFC-0230_HTLC.md#time-locked-contracts diff --git a/RFC/src/RFC-0111_BaseNodeArchitecture.md b/RFC/src/RFC-0111_BaseNodeArchitecture.md new file mode 100644 index 0000000000..9c46b60605 --- /dev/null +++ b/RFC/src/RFC-0111_BaseNodeArchitecture.md @@ -0,0 +1,186 @@ +# RFC-0111/BaseNodesArchitecture + +## Base Node Architecture + +![status: draft](theme/images/status-draft.svg) + +**Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) + +# Licence + +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). + +Copyright 2019 The Tari Development Community + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: + +1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following + disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## Language + +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in +[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as +shown here. + +## Disclaimer + +This document and its content are intended for information purposes only and may be subject to change or update +without notice. + +This document may include preliminary concepts that may or may not be in the process of being developed by the Tari +community. The release of this document is intended solely for review and discussion by the community of the +technological merits of the potential system outlined herein. + +## Goals + +The aim of this Request for Comment (RFC) is to describe the high-level Base Node architecture. + +## Architectural Layout + +The Base Node architecture is designed to be modular, robust and performant. + +![Base Layer architecture](theme/images/base_layer_arch.png) + +The major components are separated into separate modules. Each module exposes a public Application Programming Interface +(API), which communicates with other modules using asynchronous messages via futures. + +### Base Node Service + +The Base Node Service is an instantiation of a Tari Comms Service, which subscribes to and handles specific messages +coming from the P2P Tari network via the Comms Module of a live Tari communications node. The Base Node Service's job is +to delegate the jobs required by those messages to its sub-modules, consisting primarily of the Transaction Validation +Service, the Block Validation Service and the Block synchronisation service, using an asynchronous Request-Response +pattern. + +The Base Node Service will pass messages back to the P2P network via the Comms Module, based on the results of its +actions. + +The primary messages that a Base Node will subscribe to are: + +* **NewTransaction.** A New Transaction is being propagated over the network. If it has not seen the transaction before, + the Base Node will validate the transaction and, if it is valid: + * add it to its mempool; + * pass the transaction on to peers. + + Otherwise, the transaction is dropped. +* **NewBlock.** A newly mined block is being propagated over the network. If the node has not seen the block before, the + node will validate it. Its action depends on the validation outcome: + * _Invalid block_ - drop the block. + * _Valid block appending to the longest chain_ - add the block to the local state; propagate the block to peers. + * _Valid block forking off main chain_ - add the block to the local state; propagate the block to peers. + * _Valid block building off unknown block_ - add the orphan block to the local state. + +* **Sync Request.** A peer is synchronizing state and is asking for block data. The node can decide to: + * Ignore or ban the peer (based on previous behaviour heuristics). + * Try and provide the data, returning an appropriate response. Note that most nodes can only offer block data up until + their pruning horizon. Only full archival nodes can return the full block history. Refer to + [RFC-0140](RFC-0140_Syncing_and_seeding.md) for more details. + +The validation procedures are complex and are thus encapsulated in their own sub-services. These services hold +references to the blockchain state API, the mempool API, a range proof service and whatever other modules they need to +complete their work. Each validation module has a single primary method, `validate_xxx()`, which takes in the +transaction or block to be validated and returns a future that resolves once the validation task is complete. + +### Distributed Hash Table (DHT) Service + +Peer discovery is a key service that blockchain nodes provide so that the peer mesh network can be navigated by the full +nodes making up the network. + +In Tari, the peer-to-peer network is not only used by full nodes (Base Nodes), but also by Validator Nodes, and + +Tari and Digital Assets Network (DAN) clients. + +For this reason, peer management is handled internally by the Comms layer. If a Base Node wants to propagate a message, +new block or transaction, for example, it simply selects a `BROADCAST` strategy for the message and the Comms layer +will do the rest. + +When a node wishes to query a peer for its peer list, this request will be handled by the `DHTService`. It will +communicate with its Comms module's Peer Manager, and provide that information to the peer. + +### Blockchain State Module + +The blockchain state module is responsible for providing a persistent storage solution for blockchain state data. For +Tari, this is delivered using the Lightning Memory-mapped Database (LMDB). LMDB is highly performant, intelligent and +straightforward to use. An LMDB is essentially treated as a hash map data structure that transparently handles +memory caching, disk Input/Output (I/O) and multi-threaded access. + +The blockchain module is able to run as a standalone service, but must be thread-safe. Block and transaction validation +requests are futures-based. These are asynchronous requests, which means that multiple validation requests can and +should be handled in parallel, in separate threads. Initially, all the logic for a single block or transaction +validation can be executed in sequence, wrapped inside a single future. However, there is scope to optimise this in +future; for example: Validating a block entails checking the proof-of-work (very slow), checking signatures (fast, but +many of them), and checking the accounting (slow). Each of these sub-tasks could also be spun off as a future, with a +master future co-ordinating the sub-futures and assembling the final results. + +Tokio is becoming the _de facto_ standard for asynchronous programming in Rust. + +Tokio's default task executor provides multi-threaded work-stealing work queues and CPU-bound worker threads out of the +box. This is a good fit for the type of work that base nodes must perform. In addition, the +[Tower project](https://github.com/tower-rs) provides a set of traits and middleware that will be very useful in Tari +services, and so it is recommended to follow the Services pattern as used by that project. + +This RFC proposes that the 0.1 version of tokio is used in the Tari project until the standard +[futures](https://doc.rust-lang.org/std/future/index.html) library has stabilised before making a switch. + + +### Mempool Module + +The mempool module tracks (valid) transactions that the node knows about, but that have not yet been included in a +block. The mempool is ephemeral and non-consensus critical, and as such may be a memory-only data structure. Maintaining +a large mempool is far more important for Base Nodes serving miners than those serving wallets. A mempool will slowly +rebuild after a node reboots. + +That said, the mempool module must be thread safe. The Tari mempool module handles requests in the same way as the +Blockchain state module: via futures. The mempool structure itself is a set of hash maps as described in [RFC-0190]. For +performance reasons, it may be worthwhile using a [concurrent hash map] implementation. + +### gRPC Interface + +Base Nodes need to provide a local communication interface in addition to the P2P communication interface. This is +best achieved using [gRPC]. The Base Node gRPC interface provides access to the public API methods of the Base Node +Service, the mempool module and the blockchain state module, as discussed above. + +gRPC access is useful for tools such as local User Interfaces (UIs) to a running Base Node; client wallets running on +the same machine as the Base Node that want a more direct communication interface to the node than the P2P network +provides; third-party applications such as block explorers; and, of course, miners. + +A non-exhaustive list of methods the base node module API will expose includes: + +* Blockchain state calls, including: + * checking whether a given Unspent Transaction Output (UTXO) is in the current UTXO set; + * requesting the latest block height; + * requesting the total accumulated work on the longest chain; + * requesting a specific block at a given height; + * requesting the Merklish root commitment of the current UTXO set; + * requesting a block header for a given height; + * requesting the block header for the chain tip; + * validating signatures for a given transaction kernel; + * validating a new block without adding it to the state tree; + * validating and adding a (validated) new block to the state, and informing of the result (orphaned, fork, re-org, etc.). +* Mempool calls + * The number of unconfirmed transactions + * The number of orphaned transactions + * Returning a list of transaction ranked by some criterion (of interest to miners) + * The current size of the mempool (in transaction weight) +* Block and transaction validation calls +* Block synchronisation calls + + +[concurrent hash map]: https://crates.io/crates/chashmap +[gRPC]: https://grpc.io/ +[RFC-0190]: RFC-0190_Mempool.md \ No newline at end of file diff --git a/RFC/src/RFC-0130_Mining.md b/RFC/src/RFC-0130_Mining.md index 7248b9a4d5..2b0cb3e43c 100644 --- a/RFC/src/RFC-0130_Mining.md +++ b/RFC/src/RFC-0130_Mining.md @@ -1,14 +1,14 @@ # RFC-0130/Mining -## Full-node mining on Tari base layer +## Full-node Mining on Tari Base Layer ![status: draft](theme/images/status-draft.svg) **Maintainer(s)**: [Yuko Roodt] (https://github.com/neonknight64) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2018 The Tari Development Community @@ -22,76 +22,80 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in -[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all -capitals, as shown here. +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in +[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as +shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. -This document may include preliminary concepts that may or may not be in theprocess of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +This document may include preliminary concepts that may or may not be in the process of being developed by the Tari +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document will provide a brief overview of the Tari merged mining process and will introduce the primary -functionality required of the Mining Server and Mining Worker. +The aim of this Request for Comment (RFC) is to provide a brief overview of the Tari merged mining process and introduce +the primary functionality required of the Mining Server and Mining Worker. -## Related RFCs +## Related Requests for Comment -* [RFC-0100: Base layer](RFC-0100_BaseLayer.md) -* [RFC-0110: Base nodes](RFC-0110_BaseNodes.md) +* [RFC-0100: Base Layer](RFC-0100_BaseLayer.md) +* [RFC-0110: Base Nodes](RFC-0110_BaseNodes.md) ## Description ### Assumptions -- That the Tari [blockchain] will be merged mined with Monero. +- The Tari [blockchain] will be merged mined with Monero. - The Tari [Base Layer] has a network of [Base Node]s that verify and propagate valid [transaction]s and [block]s. ### Abstract The process of merged mining Tari with Monero on the Tari Base Layer is performed by [Mining Server]s and [Mining Worker]s. Mining Servers are responsible for constructing new blocks by bundling transactions from the [mempool] -of a connected Base Node. They then distribute Proof-of-Work(PoW) tasks to Mining Workers in an attempt to solve -newly created blocks. Solved solutions and shares are sent by the Mining Workers to the Mining Server, who in turns +of a connected Base Node. They then distribute Proof-of-Work (PoW) tasks to Mining Workers in an attempt to solve +newly created blocks. Solved solutions and shares are sent by the Mining Workers to the Mining Server, which in turn verifies the solution and distributes the newly created blocks to the Base Node and Monero Node for inclusion in their respective blockchains. -### Merged mining on the Tari Base Layer +### Merged Mining on Tari Base Layer -This document is divided into three parts. First, a brief overview of the merged mining process and the interactions -between the Base Node, Mining Server and Mining Worker will be provided, then the primary functionality required -of the Mining Server and Mining Worker will be proposed. +This document is divided into three parts: -#### Overview of the Tari merged mining process using Mining Servers and Mining Workers +- A brief overview of the merged mining process and the interactions between the Base Node, Mining + Server and Mining Worker. +- The primary functionality required of the Mining Server. +- The primary functionality required of the Mining Worker. + +#### Overview of Tari Merged Mining Process using Mining Servers and Mining Workers Mining on the Tari Base Layer consists of three primary entities: the Base Nodes, Mining Servers and Mining Workers. -A description of the Base Node is provided in [RFC-0110/Base Nodes] (https://tari-project.github.io/tari/RFC-0110_BaseNodes.html). +A description of the Base Node is provided in [RFC-0110/Base Nodes](https://tari-project.github.io/tari/RFC-0110_BaseNodes.html). A Mining Server is connected locally or remotely to a Tari Base Node and a Monero Node, and is responsible for constructing Tari and Monero Blocks from their respective mempools. The Mining Server should retrieve transactions from the mempool of the connected Base Node and assemble a new Tari block by bundling transactions together. Mining servers may re-verify transactions before including them in a new Tari block, but this enforcement of -verification and transaction rules such as signatures and timelocks are the responsibility of the connected Base node. Mining servers are responsible for [cut-through] as this is required for scalability and privacy. +verification and transaction rules such as signatures and timelocks is the responsibility of the connected Base Node. +Mining Servers are responsible for [cut-through], as this is required for scalability and privacy. -To enable Merged mining of Tari with Monero, both a Tari and a Monero block needs to be created and linked. First, +To enable merged mining of Tari with Monero, both a Tari and a Monero block need to be created and linked. First, a new Tari block is created and then the block header hash of the new Tari block is included in the coinbase -transaction of the new Monero block. Once a new merged mined Monero block has been constructed, PoW tasks can then -be sent to the connected Mining Workers that will attempt to solve the block by performing the latest released +transaction of the new Monero block. Once a new merged mined Monero block has been constructed, PoW tasks can +be sent to the connected Mining Workers, which will attempt to solve the block by performing the latest released version of the PoW algorithm selected by Monero. Assuming the Tari difficulty is less than the Monero difficulty, miners get rewarded for solving the PoW at any @@ -103,43 +107,46 @@ If the PoW solution was sufficient to meet the difficult level of both the Tari individual blocks for each blockchain can be sent from the Mining Server to the Base Node and Monero Node to be added to the respective blockchains. -Every Tari block must include the solved Monero block’s information (block header hash, Merkle tree branch, and -hash of the coinbase transaction) into the PoW summary section of the Tari block header. +Every Tari block must include the solved Monero block's information (block header hash, Merkle tree branch and +hash of the coinbase transaction) in the PoW summary section of the Tari block header. If the PoW solution found by the Mining Workers only solved the problem at the Tari difficulty, the Monero block can be discarded. This process will ensure that the Tari difficulty remains independent. Adjusting the difficulty will ensure that -the Tari block times are preserved. Also, the Tari block time can be less than, equal -or greater than the Monero block times. A more detailed description of the Merged Mining process between a Primary -and Auxiliary blockchain is provided in the [Merged Mining TLU report] (https://tlu.tarilabs.com/merged-mining/merged-mining.html). +the Tari block times are preserved. Also, the Tari block time can be less than, equal to or greater than the Monero block +times. A more detailed description of the merged mining process between a Primary and Auxiliary blockchain is provided +in the [Merged Mining TLU report](https://tlu.tarilabs.com/merged-mining/merged-mining.html). -#### Functionality required by the Tari Mining Server +#### Functionality Required by Tari Mining Server - The Tari blockchain MUST have the ability to be merged mined with Monero. -- The Mining Server MUST maintain a local or remote connection with a Base Node and a Monero Node. -- It MUST have a mechanism to construct a new Tari and Monero block by selecting transactions from the different - Tari and Monero mempools that need to be included in the different blocks. -- It MUST apply [cut-through] when mining Tari transactions from the [mempool] and only add the excess to the list of new Tari block transactions. -- It MAY have a configurable transaction selection mechanism for the block construction process. -- It MAY have the ability to re-verify transactions before including them in a new Tari block. -- It MUST have the ability to include the block header hash of the new Tari block into the coinbase section of a - newly created Monero block to enable merged mining. -- It MUST be able to include the Monero block header hash, Merkle tree branch and hash of the coinbase transaction - of the Monero block into the PoW summary field of the new Tari block header. -- It MUST have the ability to transmit and distribute PoW tasks for the newly created Monero block, that contains - the Tari block information, to connected Mining Workers. -- It MUST verify PoW solutions received from Mining Workers and it MUST reject and discard invalid solutions or - solutions that do not meet the minimum required difficulty. -- The Mining Server MAY keep track of mining share contributions of the connected Mining Workers. -- It MUST submit completed Tari blocks to the Tari Base Node. -- It MUST submit completed Monero blocks to the Monero Network. - -#### Functionality required by the Tari Mining Worker - -- It MUST maintain a local or remote connection to a Mining Server. -- It MUST have the ability to receive PoW tasks from the connected Mining Server. -- It MUST have the ability to perform the latest released version of Monero's PoW algorithm on the received PoW tasks. -- It MUST attempt to solve the PoW task at the difficulty specified by the Mining Server. -- It MUST submit completed shares to the connected Mining Server. +- The Tari Mining Server: + - MUST maintain a local or remote connection with a Base Node and a Monero Node. + - MUST have a mechanism to construct a new Tari and Monero block by selecting transactions from the different + Tari and Monero mempools that need to be included in the different blocks. + - MUST apply [cut-through] when mining Tari transactions from the [mempool] and only add the excess to the list of new Tari block transactions. + - MAY have a configurable transaction selection mechanism for the block construction process. + - MAY have the ability to re-verify transactions before including them in a new Tari block. + - MUST have the ability to include the block header hash of the new Tari block in the coinbase section of a + newly created Monero block to enable merged mining. + - MUST be able to include the Monero block header hash, Merkle tree branch and hash of the coinbase transaction + of the Monero block into the PoW summary field of the new Tari block header. + - MUST have the ability to transmit and distribute PoW tasks for the newly created Monero block, which contains + the Tari block information, to connected Mining Workers. + - MUST verify PoW solutions received from Mining Workers and MUST reject and discard invalid solutions or + solutions that do not meet the minimum required difficulty. + - MAY keep track of mining share contributions of the connected Mining Workers. + - MUST submit completed Tari blocks to the Tari Base Node. + - MUST submit completed Monero blocks to the Monero Network. + +#### Functionality Required by Tari Mining Worker + +The Tari Mining Worker: + +- MUST maintain a local or remote connection to a Mining Server. +- MUST have the ability to receive PoW tasks from the connected Mining Server. +- MUST have the ability to perform the latest released version of Monero's PoW algorithm on the received PoW tasks. +- MUST attempt to solve the PoW task at the difficulty specified by the Mining Server. +- MUST submit completed shares to the connected Mining Server. [blockchain]: Glossary.md#blockchain @@ -150,5 +157,4 @@ and Auxiliary blockchain is provided in the [Merged Mining TLU report] (https:// [mining worker]: Glossary.md#mining-worker [block]: Glossary.md#block [mempool]: Glossary.md#mempool - -[cut-through]: RFC-0140_Syncing.md#Pruning-and-cut-through +[cut-through]: RFC-0140_Syncing_and_seeding.html#pruning-and-cut-through diff --git a/RFC/src/RFC-0140_Syncing_and_seeding.md b/RFC/src/RFC-0140_Syncing_and_seeding.md index 8077f8cee0..60acd5659d 100644 --- a/RFC/src/RFC-0140_Syncing_and_seeding.md +++ b/RFC/src/RFC-0140_Syncing_and_seeding.md @@ -1,14 +1,14 @@ # RFC-0140/SyncAndSeeding -## Syncing strategies and objectives +## Syncing Strategies and Objectives ![status: draft](theme/images/status-draft.svg) -**Maintainer(s)**: [SW van heerden](https://github.com/SWvheerden) +**Maintainer(s)**: [S W van Heerden](https://github.com/SWvheerden) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2018 The Tari Development Community @@ -22,35 +22,35 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document describes the process of Syncing, seeding, pruning and cut-through. +The aim of this Request for Comment (RFC) is to describe the syncing, seeding, pruning and cut-through process. -## Related RFCs +## Related Requests for Comment * [RFC-0110: Base Nodes](RFC-0110_BaseNodes.md) @@ -58,68 +58,95 @@ This document describes the process of Syncing, seeding, pruning and cut-through ### Syncing -When a new node comes online, loses connection or encounters a chain re-organisation that is longer than it can tolerate, it must enter syncing mode. This will allow it to recover its state to the newest up to date state. Syncing can be divided into 2 [SynchronisationStrategy]s, complete sync and sync. Complete sync will mean that the node communicates with an archive node to get the complete history of every single block from genesis block. Sync will involve the node getting every block from its [pruning horizon](pruninghorizon) to [current head](current head), as well as every block header from genesis block. +When a new node comes online, loses connection or encounters a chain reorganization that is longer than it can tolerate, +it must enter syncing mode. This will allow it to recover its state to the newest up-to-date state. Syncing can be +divided into two [SynchronizationStrategy]s: complete sync and sync. Complete sync means that the node communicates with +an archive node to get the complete history of every single block from genesis block. Sync involves the node getting +every block from its [pruning horizon] to [current head], as well as every block header +from genesis block. #### Complete Sync -Complete sync is only available from archive nodes, as these will be the only nodes that will be able to supply the complete history required to sync every block with every transaction from genesis block up onto [current head](currenthead). +Complete sync is only available from archive nodes, as these will be the only nodes that will be able to supply the +complete history required to sync every block with every transaction from genesis block up onto [current head]. -#### Syncing process +#### Syncing Process The syncing process MUST be done in the following steps: -1. Set [SynchronisationState] to `Synchronising`. -2. Asks peers for their latest block, so it can get the total proof of work. +1. Set [SynchronizationState] to `Synchronizing`. +2. Ask peers for their latest block, so it can get the total Proof-of-Work (PoW). 3. Choose the longest chain based on total PoW done on that chain. -4. Selects a connected peer with the longest chain to sync from, this is based on the following criteria: - 1. Does the peer have a high enough [pruning horizon](pruninghorizon). - 2. Does the peer allow syncing. - 3. Does the peer have a low latency. -5. Download all headers from genesis block up onto [current head](currenthead), and validate the headers as you receive them. -6. Download [UTXO](utxo) set at [pruning horizon](pruninghorizon). -7. Download all blocks from [pruning horizon](pruninghorizon) up to [current head](currenthead), if the node is doing a complete sync, the [pruning horizon](pruninghorizon) will just be infinite, which means you will download all blocks ever created. -8. Validate blocks as if they where just mined and then received, in chronological order. +4. Select a connected peer with the longest chain to sync from. This is based on the following criteria: + - Does the peer have a high enough [pruning horizon]? + - Does the peer allow syncing? + - Does the peer have a low latency? +5. Download all headers from genesis block up onto [current head], and validate the headers as you receive them. +6. Download Unspent Transaction Output ([UTXO]) set at [pruning horizon]. +7. Download all blocks from [pruning horizon] up to [current head]. If the node is doing a +complete sync, the [pruning horizon] will be infinite, which means you will download all blocks ever +created. +8. Validate blocks as if they were just mined and then received, in chronological order. -After this process the node will be in sync and able to process blocks and transaction normally. +After this process, the node will be in sync, and will be able to process blocks and transactions normally. -#### Keeping in sync +#### Keeping in Sync -The node SHOULD periodically test its peers with ping messages to ensure that they are alive. When a node sends a ping message, it MUST include the current total PoW, hash of the [current head](currenthead) and genesis block hash of its own current longest chain in the ping message. The receiving node MUST reply with a pong message also including the total PoW, [current head](currenthead) and genesis block hash of its longest chain. If the two chains don't match up, the node with the lowest PoW has the responsibility to ask the peer for syncing information and set [SynchronisationState] to `Synchronising`. +The node SHOULD periodically test its peers with ping messages to ensure that they are alive. When a node sends a ping +message, it MUST include the current total PoW, hash of the [current head] and genesis block hash of its +own current longest chain in the ping message. The receiving node MUST reply with a pong message, also including the total +PoW, [current head] and genesis block hash of its longest chain. If the two chains do not match up, the node +with the lowest PoW is responsible for asking the peer for syncing information and set [SynchronizationState] to `Synchronizing`. -If the genesis block hashes don't match, the node is removed from its peer list as this node is running a different blockchain. +If the genesis block hashes do not match, the node is removed from its peer list, as this node is running a different +blockchain. -This will be handled by the node asking for each block header from the [current head](currenthead) going backward for older blocks until a known block is found. If a known block is found, and it has missing blocks it MUST set [SynchronisationState] to `Synchronising` while it is busy catching up those blocks. +This will be handled by the node asking for each block header from the [current head], going backward for +older blocks, until a known block is found. If a known block is found, and if it has missing blocks, it MUST set +[SynchronizationState] to `Synchronizing` while it is busy catching up those blocks. -If no block is found, the node will enter sync mode and resync. It cannot recover from its state as the fork is older than its [pruning horizon](pruninghorizon). +If no block is found, the node will enter sync mode and resync. It cannot recover from its state, as the fork is older +than its [pruning horizon]. -#### Chain forks +#### Chain Forks -Chain forks can be a problem since in Mimblewimble not all nodes keep the complete transaction history, the design philosophy is more along the lines of only keeping the current [Blockchain state](blockchainstate). However, if such a node only maintains only the current [Blockchain state](blockchainstate) it is not possible for the node to "rewind" its state to handle forks in the chain history. In this case, a mode must re-sync its chain to recover the necessary transaction history up onto its [pruning horizon](pruninghorizon). +Chain forks can be a problem, since in Mimblewimble not all nodes keep the complete transaction history. The design +philosophy is more along the lines of only keeping the current [Blockchain state]. However, if such a +node only maintains the current [Blockchain state], it is not possible for the node to "rewind" its +state to handle forks in the chain history. In this case, a mode must resync its chain to recover the necessary +transaction history up onto its [pruning horizon]. -To counter this problem we use [pruning horizon](pruninghorizon), this allows every [node](base node) to be a "light" [archival node](archivenode). This in effect means that the node will keep a full history for a short while. If the node encounters a fork it can easily rewind its state to apply the fork. If the fork is longer than the [pruning horizon](pruninghorizon), the node will enter a sync state where it will resync. +To counter this problem, we use [pruning horizon]. This allows every node ([Base Node]) to be a "light" +[archival node](archivenode). This in effect means that the node will keep a full history for a short while. If the node +encounters a fork, it can easily rewind its state to apply the fork. If the fork is longer than the [pruning horizon], +the node will enter a sync state, where it will resync. -### Pruning and cut-through +### Pruning and Cut-through -[Pruning and cut-through]: #Pruning-and-cut-through "Remove already spent outputs from the [utxo]" +In Mimblewimble, the state can be completely verified using the current [UTXO] set +(which contains the output commitments and range proofs), the set of excess signatures (contained in the transaction kernels) +and the PoW. The full block and transaction history is not required. This allows base layer nodes to remove old used +inputs from the [blockchain] and or the [mempool]. [Cut-through] happens in the [mempool] while pruning +happens in the [blockchain] with already confirmed transactions. This will remove the spent inputs and outputs, but will +retain the excesses of each [transaction]. -In Mimblewimble, the state can be completely verified using the current [UTXO](utxo) set (which contains the output commitments and range proofs), the set of excess signatures (contained in the transaction kernels) and the proof-of-work. The full block and transaction history is not required. This allows base layer nodes to remove old used inputs from the [blockchain] and or the [mempool]. [Cut-through](cut-through) happens in the [mempool] while pruning happens in the [blockchain] with already confirmed transactions. This will remove the spent inputs and outputs, but will retain the excesses of each [transaction]. +Pruning is only for the benefit of the local Base Node, as it reduces the local blockchain size. Pruning only happens +after the block is older than the [pruning horizon] height. A Base Node will either run in archive mode +or prune mode. If the Base Node is running in archive mode, it MUST NOT prune. -Pruning is only for the benefit of the local base node as it reduces the local blockchain size. Pruning only happens after the block is older than the [pruning horizon](pruninghorizon) height. A Base node will either run in archive mode or prune mode, if the base node is running in archive mode it MUST NOT prune. +When running in pruning mode, [Base Node]s MUST remove all spent outputs that are older than the +[pruning horizon]in their current stored [UTXO] when a new block is received from another [Base Node]. -When running in pruning mode, [base node]s have the following responsibilities: -1. MUST remove all spent outputs that is older than the [pruning horizon](pruninghorizon) in it's current stored [UTXO](utxo) when a new block is received from another [base node]. - - -[archivenode]: Glossary.md#archivenode -[blockchainstate]: Glossary.md#blockchainstate -[pruninghorizon]: Glossary.md#pruninghorizon +[archivenode]: Glossary.md#archive-node +[blockchainstate]: Glossary.md#blockchain-state +[pruning horizon]: Glossary.md#pruning-horizon [tari coin]: Glossary.md#tari-coin [blockchain]: Glossary.md#blockchain -[currenthead]: Glossary.md#currenthead +[current head]: Glossary.md#current-head [block]: Glossary.md#block [transaction]: Glossary.md#transaction [base node]: Glossary.md#base-node @@ -129,7 +156,7 @@ When running in pruning mode, [base node]s have the following responsibilities: [ValidationState]: Glossary.md#validationstate [BroadcastStrategy]: Glossary.md#broadcaststrategy [range proof]: Glossary.md#range-proof -[SynchronisationStrategy]: Glossary.md#synchronisationstrategy -[SynchronisationState]: Glossary.md#synchronisationstate +[SynchronizationStrategy]: Glossary.md#synchronisationstrategy +[SynchronizationState]: Glossary.md#synchronisationstate [mining server]: Glossary.md#mining-server [cut-through]: RFC-0110_BaseNodes.md#Pruning-and-cut-through \ No newline at end of file diff --git a/RFC/src/RFC-0150_Wallets.md b/RFC/src/RFC-0150_Wallets.md index ab5ef168c1..0441b291c3 100644 --- a/RFC/src/RFC-0150_Wallets.md +++ b/RFC/src/RFC-0150_Wallets.md @@ -1,14 +1,14 @@ # RFC-0150/Wallets -## Base layer wallet module +## Base Layer Wallet Module ![status: draft](https://github.com/tari-project/tari/raw/master/RFC/src/theme/images/status-draft.svg) **Maintainer(s)**: [Yuko Roodt](https://github.com/neonknight64), [Cayle Sharrock](https://github.com/CjS77) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2019 The Tari Development Community @@ -22,86 +22,95 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document will propose the functionality and techniques required by the [Base Layer] Tari [wallet] module. The module -exposes the core wallet functionality that user-facing wallet applications may be built on. +The aim of this Request for Comment (RFC) is to propose the functionality and techniques required by the [Base Layer] +Tari [wallet] module. The module exposes the core wallet functionality on which user-facing wallet applications may be built. -## Related RFCs +## Related Requests for Comment -* [RFC-0100: Base layer](https://github.com/tari-project/tari/blob/master/RFC/src/RFC-0100_BaseLayer.md) +* [RFC-0100: Base Layer](./RFC-0100_BaseLayer.md) This RFC is derived from a proposal first made in [this issue](https://github.com/tari-project/tari/issues/17). ## Description -### Key responsibilities +### Key Responsibilities -The wallet software is responsible for constructing and negotiating [transaction]s for transferring and receiving [Tari coin]s on the Base Layer. It should also provide functionality to generate, store and recover a master seed key and derived cryptographic key pairs that can be used for Base Layer addresses and signing of transactions. +The wallet software is responsible for constructing and negotiating [transaction]s for transferring and receiving +[Tari coin]s on the Base Layer. It should also provide functionality to generate, store and recover a master seed key +and derived cryptographic key pairs that can be used for Base Layer addresses and signing of transactions. -### Details of functionality +### Details of Functionality A detailed description of the required functionality of the Tari software wallet is provided in three parts: -* Basic transaction functionality, -* Key management features and -* the different wallet recovery methods. +* basic transaction functionality; +* key management features; and +* the different methods for recovering the wallet state of the Tari software wallet. -#### Basic transaction functionality +#### Basic Transaction Functionality - It MUST be able to send and receive Tari coins using [Mimblewimble] transactions. - It SHOULD be able to establish a connection between different user wallets to negotiate: - - the construction of a transaction and - - the signing of multi signature transactions. -- The Tari software wallet SHOULD be implemented as a library or API so that GUI or CLI applications can be developed on top of it. + - the construction of a transaction; and + - the signing of multi-signature transactions. +- The Tari software wallet SHOULD be implemented as a library or Application Programming Interface (API) so that Graphic +User Interface (GUI) or Command Line Interface (CLI) applications can be developed on top of it. - It MUST be able to establish a connection to a [Base Node] to submit transactions and monitor the Tari [blockchain]. - It SHOULD maintain an internal ledger to keep track of the Tari coin balance of the wallet. -- It MAY offer transaction fee estimation taking into account: - - transaction byte size, - - network congestion and - - the desired transaction priority. -- It SHOULD be able to monitor and return the states (Spent, Unspent or Unconfirmed) of previously submitted transactions by querying information from the connected base node. -- The Wallet SHOULD present the total Spent, Unspent or Unconfirmed transactions in summarised form. -- The wallet software SHOULD be able to update its software to patch potential security vulnerabilities. Automatic updating SHOULD be selected by default, but users can decide to opt out. -- Wallet features requiring querying a base node for information, SHOULD have caching capabilities to reduce bandwidth consumption. - -#### Key management features - -- It MUST be able to generate a master seed key for the wallet by using one of: - - input from a user (for example, when restoring a wallet, or in testing), - - a user defined set of mnemonic word sequences using known word lists, +- It MAY offer transaction fee estimation, taking into account: + - transaction byte size; + - network congestion; and + - desired transaction priority. +- It SHOULD be able to monitor and return the states (Spent, Unspent or Unconfirmed) of previously submitted transactions +by querying information from the connected Base Node. +- It SHOULD present the total Spent, Unspent or Unconfirmed transactions in summarized form. +- It SHOULD be able to update its software to patch potential security vulnerabilities. +Automatic updating SHOULD be selected by default, but users can decide to opt out. +- Wallet features requiring querying a base node for information SHOULD have caching capabilities to reduce bandwidth consumption. + +#### Key Management Features + +- It MUST be able to generate a master seed key for the wallet by using: + - input from a user (e.g. when restoring a wallet, or in testing); or + - a user-defined set of mnemonic word sequences using known word lists; or - a cryptographically secure random number generator. -- It SHOULD be able to generate derived transactional cryptographic key pairs from the master seed key using deterministic key pair generation. +- It SHOULD be able to generate derived transactional cryptographic key pairs from the master seed key using deterministic +key pair generation. - It SHOULD store the wallet state using a password or passphrase encrypted persistent key-value database. -- It SHOULD provide the ability to backup the wallet state to a single encrypted file to simplify wallet recovery and reconstruction at a later stage. -- It MAY provide the ability to export the master seed key or wallet state as a printable paper wallet using coded markers. +- It SHOULD provide the ability to back up the wallet state to a single encrypted file to simplify wallet recovery and +reconstruction at a later stage. +- It MAY provide the ability to export the master seed key or wallet state as a printable paper wallet, using coded markers. -#### Different methods for recovering the wallet state of the Tari software wallet: -- It MUST be able to reconstruction the wallet state from a manually entered master seed key. -- It MUST have a mechanism to systematically search through the Tari blockchain and mempool for unspent and unconfirmed transactions using the keys derived from the master key. +#### Different Methods for Recovering Wallet State of Tari Software Wallet + +- It MUST be able to reconstruct the wallet state from a manually entered master seed key. +- It MUST have a mechanism to systematically search through the Tari blockchain and mempool for unspent and unconfirmed +transactions, using the keys derived from the master key. - The master seed key SHOULD be derivable from a specific set of mnemonic word sequences using known word lists. - It MAY enable the reconstruction of the master seed key by scanning a coded marker of a paper wallet. diff --git a/RFC/src/RFC-0151_TransactionProtocol.md b/RFC/src/RFC-0151_TransactionProtocol.md index c19c0592d0..468756d3cd 100644 --- a/RFC/src/RFC-0151_TransactionProtocol.md +++ b/RFC/src/RFC-0151_TransactionProtocol.md @@ -1,16 +1,16 @@ # TransactionProtocol -## Transaction protocol +## Transaction Protocol ![status: draft](theme/images/status-draft.svg) **Maintainer(s)**: Cayle Sharrock -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). -Copyright 2019. The Tari Development Community +Copyright 2019 The Tari Development Community Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -22,42 +22,42 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in -[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in +[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document describes the transaction protocol for peer-to-peer Tari payments using the Mimblewimble protocol. It also -considers some attacks that may be launched against the protocol and offers some discussion around those attacks and -potential alternatives to the protocol. +This Request for Comment (RFC) describes the transaction protocol for peer-to-peer Tari payments using +the Mimblewimble protocol. It also considers some attacks that may be launched against the protocol and offers some +discussion around those attacks and potential alternatives to the protocol. -The goal is to describe a transaction protocol that -* permits multiple recipients, -* preserves privacy regarding how many parties are involved in the transaction, +The goal is to describe a transaction protocol that: +* permits multiple recipients; +* preserves privacy regarding how many parties are involved in the transaction; and * is secure against all reasonable attacks. -## Related RFCs +## Related Requests for Comment * [RFC-0100: The Base Layer](RFC-0100_BaseLayer.md) @@ -68,15 +68,15 @@ to construct a valid [Mimblewimble transaction]. A valid transaction involves: -* A set of one or more inputs being spent by the Sender, -* A set of zero or more outputs being sent to the Sender, -* A set of recipients, each of whom MUST construct exactly one output, -* A set of partial schnorr signatures that when aggregated, validates the transaction construction and indicates every +* a set of one or more inputs being spent by the Sender; +* a set of zero or more outputs being sent to the Sender; +* a set of recipients, each of whom MUST construct exactly one output; and +* a set of partial Schnorr signatures which, when aggregated, validates the transaction construction and indicates every party's satisfaction with the terms. -### The issue with multiple recipients +### The Issue with Multiple Recipients -Each party involved in a Tari transaction must produce a partial signature signing the same challenge. This challenge is +Each party involved in a Tari transaction must produce a partial signature, signing the same challenge. This challenge is defined as $$ e = H(\Sigma R_i \Vert \Sigma P_i \Vert m) $$ @@ -90,27 +90,28 @@ every party knows how many parties are involved in the transaction, which is not preferable if a secure scheme could be found where each recipient interacts only with the sender and does not need to calculate these sums themselves. -Unfortunately, as we discuss below, it seems that it's not possible using any known scheme that satisfies both this +Unfortunately, as discussed below, it seems that using any known scheme, it's not possible to satisfy this privacy goal while achieving the desired security level. -### The issue with multiple senders +### The Issue with Multiple Senders To increase privacy, the public excess values are [offset] by a constant random value. The choice of this value, as well as fee selection, can only be set once per transaction. The privilege of selecting these values is generally bestowed on -the sender, since she pays the fee. Allowing multiple sending parties (or equivalently, allowing recipients to provide +the sender, since the sender pays the fee. Allowing multiple sending parties (or equivalently, allowing recipients to provide inputs) would require a negotiation round to set the fee and offset before the transaction could be constructed. This is a complication we don't want to deal with, and so all schemes presented here allow exactly one sender. -### Two-party transactions +### Two-party Transactions -Two party transactions are fairly straightforward and are described in detail on TLU. (See [Mimblewimble transaction]). +Two-party transactions are fairly straightforward and are described in detail by Tari Labs University (TLU). (Refer to +[Mimblewimble Transaction].) -It is proposed that Tari implement this single-round 2-party transaction scheme as a special case to support both online -2-party transactions as well as "offline" transactions such as via e-mail, text-message, carrier pigeon etc. +It is proposed that Tari implement this single-round two-party transaction scheme as a special case to support both online +two-party transactions as well as "offline" transactions such as via email, text message and carrier pigeon. -### Multiple recipient transaction scheme +### Multiple-recipient Transaction Scheme
sequenceDiagram @@ -165,7 +166,7 @@ sequenceDiagram | tx_id | Transaction identifier | | amt_i | Amount sent to i-th recipient | | Rs, Ri | Public nonce | -| Xs, Pi | Public excess / key | +| Xs, Pi | Public excess/key | | m | Message metadata | | C_i | Commitment | | RP_i | Range proof | @@ -174,16 +175,16 @@ sequenceDiagram ## Transaction ID The scheme above makes use of a `tx_id` field in every peer-to-peer message. Since all messages are stateless and -asynchronous, peers need some way of figuring out which message refers to which transaction. The transaction id fulfils +asynchronous, peers need some way of figuring out which message refers to which transaction. The transaction ID fulfils this role. -The ID does not appear on the blockchain in any manner; is purely used to disambiguate Tari transaction messages; and +The ID does not appear on the blockchain in any manner; is purely used to disambiguate Tari transaction messages and can be discarded after the transaction is broadcast to the network. -The `tx_id` is unique for every receiver so that any observers of the communication won't be able to group receivers -together (the communication should be over secure channels in general though). +The `tx_id` is unique for every receiver so that any observers of the communication will not be able to group receivers +together (however, the communication should be over secure channels in general). - The format of the transaction ID is a 4-byte little endian integer (u64) and is +The format of the transaction ID is a four-byte, little-endian integer (u64) and is calculated as ```text @@ -194,11 +195,11 @@ where `i` is the i-th recipient in the transaction. The sender can use the `tx_i differentiate recipients. -## Replay attacks +## Replay Attacks -If any party can be convinced to sign a different message with the same nonce, their private keys will be lost. One way +If any party can be convinced to sign a different message with the same nonce, its private keys will be lost. One way of achieving this would be if a virtual machine could be "snapshotted" or otherwise cloned at any point between sharing -the public nonce and signing the message. Both copies of the victim's machine will now continue unaware that there's a +the public nonce and signing the message. Both copies of the victim's machine will now continue, unaware that there's a copy participating in a signature round. What then happens is: $$ @@ -219,7 +220,7 @@ $$ \end{align} $$ -We've demonstrated this with the attacker changing his nonce, but literally any alteration to the challenge will provide +We've demonstrated this with the attacker changing their nonce, but literally any alteration to the challenge will provide a new challenge \\(e_2\\), enabling the attack. What can we do about this? In fact, it's not possible to eliminate this attack at all! The reason sits with the proof @@ -228,31 +229,31 @@ we're trying to avoid [[GOL19]]. If we could eliminate this attack, we'd need to of proving the zero-knowledge property. So we can't stop it, but we can make it as tricky as possible for the attacker to trick the receiver into replaying the -signature -- MuSig does this by requiring parties to share the hash of their nonces beforehand. At it's extreme; in the -two-party single-round scheme for example; the attacker would need to be able to control the victim's machine code +signature. MuSig does this by requiring parties to share the hash of their nonces beforehand. At its extreme: in the +two-party, single-round scheme, for example, the attacker would need to be able to control the victim's machine code execution (like running a debugger), at which point one might think the attacker could read the private key directly from memory anyway. -## Rogue key attacks +## Rogue Key Attacks -Another type of attack that can occur in multi-signature schemes are what's known as -[Rogue Key attacks](https://tlu.tarilabs.com/cryptography/digital_signatures/introduction_schnorr_signatures.html#key-cancellation-attack). +[Rogue Key attacks](https://tlu.tarilabs.com/cryptography/digital_signatures/introduction_schnorr_signatures.html#key-cancellation-attack) +are another type of attack that can occur in multi-signature schemes. -In this case, the attacker has the freedom to choose a key or nonce _after_ the victim has already disclosed his. This -may allow the attacker to forge a valid signature on behalf of the victim. A recent paper, [[DRI19]]], suggests that -_any_ Schnorr-based 2-round multi-signature scheme is vulnerable to a rogue key attack. +In this case, the attacker has the freedom to choose a key or nonce _after_ the victim has already disclosed theirs. This +may allow the attacker to forge a valid signature on behalf of the victim. A recent paper, [[DRI19]], suggests that +_any_ Schnorr-based, two-round multi-signature scheme is vulnerable to a rogue key attack. -How this might apply in an insecure 2-round Tari multi-signature scheme is as follows: A receiver sends his public -nonce, output commitment and range proof, and public spending key to the sender, but then decides to cancel the +How this might apply in an insecure two-round Tari multi-signature scheme is as follows: A receiver sends their public +nonce; output commitment and range proof; and public spending key to the sender, but then decides to cancel the transaction by refusing to provide a signature and sending an "Abort" message to the sender instead. The sender could, -if he wanted, forge the 2-of-2 signature using this rogue key attack and broadcast the transaction anyway. +if they wanted, forge the 2-of-2 signature using this rogue key attack and broadcast the transaction anyway. -_Note:_ This attack is not applicable in the one-round 2-party scheme since the receiver returns his information in an -all-or-nothing manner. However, the _receiver_ could attempt to forge a signature, since he has the Sender's public -nonce, but there's nothing he can really do with this signature; he certainly cannot broadcast a transaction with it -because he doesn't have any of the transaction data at this stage. +**Note:** This attack is not applicable in the one-round, two-party scheme, since the receiver returns their information in an +all-or-nothing manner. However, the _receiver_ could attempt to forge a signature, since they have the Sender's public +nonce, but there's nothing they can really do with this signature; they certainly cannot broadcast a transaction with it +because they don't have any of the transaction data at this stage. -We avoid rogue-key attacks in the Tari multi-recipient scheme by employing 3-rounds. In the first round, parties share a +We avoid rogue-key attacks in the Tari multi-recipient scheme by employing three rounds. In the first round, parties share a hash of their public nonces, which each party can later use to verify that no nonces were changed after the actual public nonces were shared. diff --git a/RFC/src/RFC-0171_MessageSerialisation.md b/RFC/src/RFC-0171_MessageSerialisation.md index 86ed66c3a0..8b18eaa308 100644 --- a/RFC/src/RFC-0171_MessageSerialisation.md +++ b/RFC/src/RFC-0171_MessageSerialisation.md @@ -1,4 +1,4 @@ -# RFC-0711/MessageSerialisation +# RFC-0171/MessageSerialization ## Message Serialization @@ -6,11 +6,11 @@ **Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). -Copyright 2019. The Tari Development Community +Copyright 2019 The Tari Development Community Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -22,49 +22,49 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in -[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in +[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document describes the message serialisation formats for message payloads used in the Tari network. +The aim of this Request for Comment (RFC) is to describe the message serialization formats for message payloads used in the Tari network. -## Related RFCs +## Related Requests for Comment -[RFC-0710: The Tari Communication Network and Network Communication Protocol](RFC-0170_NetworkCommunicationProtocol.md) +[RFC-0710: Tari Communication Network and Network Communication Protocol](RFC-0170_NetworkCommunicationProtocol.md) ## Description One way of interpreting the Tari network is that it is a large peer-to-peer messaging application. The entities chatting -on the network include +on the network include: * Users * Wallets * Base nodes * Validator nodes -The types of messages that these entities send might include +The types of messages that these entities send might include: * Text messages * Transaction messages @@ -78,23 +78,23 @@ For successful communication to occur, the following needs to happen: * The message is translated from its memory storage format into a standard payload format that will be transported over the wire. * The communication module wraps the payload into a message format, which may entail any/all of - * adding a message header to describe the type of payload, - * encrypting the message, - * signing the message, and + * adding a message header to describe the type of payload; + * encrypting the message; + * signing the message; * adding destination/recipient metadata. * The communication module then sends the message over the wire. * The recipient receives the message and unwraps it, possibly performing any/all of the following: - * decryption, - * verifying signatures, - * extracting the payload, - * passing the serialised payload to modules that are interesting in that particular message type -* The message is deserialised into the correct data structure for use by the receiving software + * decryption; + * verifying signatures; + * extracting the payload; + * passing the serialized payload to modules that are interesting in that particular message type. +* The message is deserialized into the correct data structure for use by the receiving software -This document only covers the first and last steps: _viz_: serialising data from in-memory objects to a format that can +This document only covers the first and last steps, i.e. serializing data from in-memory objects to a format that can be transmitted over the wire. The other steps are handled by the Tari communication protocol. -In addition to machine-to-machine communication, we also standardise on human-to-machine communication. Use cases for -this include +In addition to machine-to-machine communication, we also standardize on human-to-machine communication. Use cases for +this include: * Handcrafting instructions or transactions. The ideal format here is a very human-readable format. * Copying transactions or instructions from cold wallets. The ideal format here is a compact but easy-to-copy format. @@ -103,29 +103,29 @@ this include When sending a message from a human to the network, the following happens: -* The message is deserialised into the native structure. -* The deserialisation acts as an automatic validation step. +* The message is deserialized into the native structure. +* The deserialization acts as an automatic validation step. * Additional validation can be performed. -* The usual machine-to-machine process is followed as described above. +* The usual machine-to-machine process is followed, as described above. -### Binary serialisation formats +### Binary Serialization Formats -The ideal properties for binary serialisation formats are: +The ideal properties for binary serialization formats are: -* Widely used across multiple platforms and languages, but with excellent Rust support. -* Compact binary representation -* Serialisation "Just Works"(TM) with little or no additional coding overhead. +* widely used across multiple platforms and languages, but with excellent Rust support; +* compact binary representation; and +* serialization "Just Works"(TM) with little or no additional coding overhead. -Several candidates fulfil these properties to some degree. +Several candidates fulfill these properties to some degree. #### [ASN.1](http://www.itu.int/ITU-T/asn1/index.html) * Pros: - * Very mature (was developed in the 80s) + * Very mature (was developed in the 1980s) * Large number of implementations - * Dovetails into ZMQ nicely + * Dovetails nicely into ZMQ * Cons: - * Limited Rust / Serde support + * Limited Rust/Serde support * Requires schema (additional coding overhead if no automated tools for this exist) @@ -135,52 +135,62 @@ Several candidates fulfil these properties to some degree. * Very compact * Fast * Multiple language support - * Good Rust / Serde support - * Dovetails into ZMQ nicely + * Good Rust/Serde support + * Dovetails nicely into ZMQ * Cons: * No metadata support #### [Protobuf](https://code.google.com/p/protobuf/) -Similar to [Message Pack](#message-pack), but also requires schema's to be written and compiled. Serialisation performance and size -is similar to Message Pack. Can work with ZMQ but is better designed to be used with gRPC. +Similar to Message Pack, but also requires schemas to be written and compiled. Serialization +performance and size +are similar to Message Pack. It Can work with ZMQ, but is better designed to be used with gRPC. #### [Cap'n Proto](http://kentonv.github.io/capnproto/) -Similar to [Protobuf](#protobuf), but claims to be much faster. Rust is supported. +Similar to Protobuf, but claims to be much faster. Rust is supported. -#### Hand-rolled serialisation +#### Hand-rolled Serialization [Hintjens recommends](http://zguide.zeromq.org/py:chapter7#Serialization-Libraries) using hand-rolled serialization for -bulk messaging. While Pieter usually offers sage advice, I'm going to argue against using custom serialisers at this -point in time for the following reasons: -* We're unlikely to improve hugely over MessagePack -* Since serde does 95% of our work for us with MessagePack, there's significant development overhead (and new bugs) +bulk messaging. While Pieter usually offers sage advice, I'm going to argue against using custom serializers at this +stage for the following reasons: + +* We're unlikely to improve hugely over MessagePack. +* Since Serde does 95% of our work for us with MessagePack, there's a significant development overhead (and new bugs) involved with a hand-rolled solution. -* We'd have to write de/serialisers for every language that wants Tari bindings; whereas every major language has a +* We'd have to write de/serializers for every language that wants Tari bindings; whereas every major language has a MessagePack implementation. -### Serialisation in Tari +### Serialization in Tari -Deciding between these protocols is largely a matter of preference, since there isn't that much to choose between them. -Given that ZMQ is used in other places in the Tari network; MessagePack looks to be a good fit while offering a compact -data structure and highly performant de/serialisation. In Rust in particular, there's first-class support for MessagePack -in serde. +Deciding between these protocols is largely a matter of preference, since there isn't that much difference between them. +Given that ZMQ is used in other places in the Tari network, MessagePack looks to be a good fit while offering a compact +data structure and highly performant de/serialization. In Rust, in particular, there's first-class support for MessagePack +in Serde. For human-readable formats, it makes little sense to deviate from JSON. For copy-paste semantics, the extra compression that Base64 offers over raw hex or Base58 makes it attractive. Many Tari data types' binary representation will be the straightforward MessagePack version of each field in the related -Struct. In these cases, as straightforward `#[derive(Deserialize, Serialize)]` is all that is required to make the data -structure able to be sent over the wire. +`struct`. In these cases, a straightforward `#[derive(Deserialize, Serialize)]` is all that is required to enable the data +structure to be sent over the wire. -However, other structures might need fine tuning, or hand-written serialisation procedures. To capture both use cases, +However, other structures might need fine-tuning, or hand-written serialization procedures. To capture both use cases, it is proposed that a `MessageFormat` trait be defined: ```rust,compile_fail -{{#include ../../base_layer/core/src/message.rs:41:49}} +pub trait MessageFormat: Sized { + fn to_binary(&self) -> Result, MessageFormatError>; + fn to_json(&self) -> Result; + fn to_base64(&self) -> Result; + + fn from_binary(msg: &[u8]) -> Result; + fn from_json(msg: &str) -> Result; + fn from_base64(msg: &str) -> Result; +} ``` This trait will have default implementations to cover most use cases (e.g. a simple call through to `serde_json`). Serde -also offers significant ability to tweak how a given struct will be serialised through the use of -[attributes](https://serde.rs/attributes.html). \ No newline at end of file +also offers significant ability to tweak how a given struct will be serialized through the use of +[attributes](https://serde.rs/attributes.html). diff --git a/RFC/src/RFC-0172_PeerToPeerMessagingProtocol.md b/RFC/src/RFC-0172_PeerToPeerMessagingProtocol.md index b56f25172a..cb92ff57f8 100644 --- a/RFC/src/RFC-0172_PeerToPeerMessagingProtocol.md +++ b/RFC/src/RFC-0172_PeerToPeerMessagingProtocol.md @@ -4,13 +4,13 @@ ![status: draft](theme/images/status-draft.svg) -**Maintainer(s)**: [Stanley Bondi](https://github.com/sdbondi), [Cayle Sharrock](https://github.com/CjS77), [Yuko Roodt](https://github.com/neonknight64) +**Maintainer(s)**: [Stanley Bondi](https://github.com/sdbondi), [Cayle Sharrock](https://github.com/CjS77) and [Yuko Roodt](https://github.com/neonknight64) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). -Copyright 2019. The Tari Development Community +Copyright 2019 The Tari Development Community Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -22,99 +22,100 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in -[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in +[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document describes the peer-to-peer messaging protocol for [communication node]s and [communication client]s on the Tari network. +The aim of this Request for Comment (RFC) is to describe the peer-to-peer messaging protocol for [communication node]s +and [communication client]s on the Tari network. -## Related RFCs +## Related Requests for Comment -- [RFC-0170: NetworkCommunicationProtocol](RFC-0170_NetworkCommunicationProtocol.md) -- [RFC-0171: Message Serialisation] +- [RFC-0170: NetworkCommunicationProtocol](rfc-0170_NetworkCommunicationProtocol.md) +- [RFC-0171: MessageSerialization](RFC-0171_MessageSerialisation.md) ## Description ### Assumptions - Either every [communication node] or [communication client] has access to a Tor/I2P proxy, or a native Tor/I2P implementation - exists which allows communication across the Tor network. -- All messages are de/serialized as per [RFC-0171: Message Serialisation] + exists, which allows communication across the Tor network. +- All messages are de/serialized as per [RFC-0171: Message Serialisation]. -### Broad requirements +### Broad Requirements Tari network peer communication must facilitate secure, private and efficient communication -between peers. Broadly, a [communication node] or [communication client] MUST be capable of +between peers. Broadly, a [communication node] or [communication client] MUST be capable of: -- bi-directional communication between multiple connected peers, -- private and secure over the wire communication, -- understanding and constructing Tari messages, -- encrypting and decrypting message payloads, -- gracefully reestablishing dropped connections, -- and optionally, communicating to a SOCKS5 proxy (for connections over Tor and I2P). +- bidirectional communication between multiple connected peers; +- private and secure over-the-wire communication; +- understanding and constructing Tari messages; +- encrypting and decrypting message payloads; +- gracefully reestablishing dropped connections; and (optionally) +- communicating to a SOCKS5 proxy (for connections over Tor and I2P). -Additionally, [communication node]s MUST be capable of performing the following tasks: +Additionally, communication nodes MUST be capable of performing the following tasks: -- Open a control port for establishing secure peer channels. -- Maintain a list of known peers in the form of a routing table. -- Forward directed messages to neighbouring peers. -- Broadcast messages to neighbouring peers. +- opening a control port for establishing secure peer channels; +- maintaining a list of known peers in the form of a routing table; +- forwarding directed messages to neighbouring peers; and +- broadcasting messages to neighbouring peers. ### Overall Architectural Design The Tari communication layer has a modular design to allow for the various communicating nodes and clients to use the same infrastructure code. -The design is influenced by open-source library called [ZeroMQ] and the [ZeroMQ] C bindings are a dependency of -the project. [ZeroMQ]'s over-the-wire protocol is relatively simple and replicating [ZeroMQ] framing in a custom -implementation should not be prohibitively difficult. However, there are many valuable features offered by -[ZeroMQ] which would be a significantly larger undertaking to reproduce. Fortunately, bindings or native ports are -available in numerous languages. +The design is influenced by an open-source library called [ZeroMQ] and the ZeroMQ C bindings are a dependency of +the project. ZeroMQ's over-the-wire protocol is relatively simple, and replicating ZeroMQ framing in a custom +implementation should not be prohibitively difficult. However, ZeroMQ offers many valuable features, which would be a +significantly larger undertaking to reproduce. Fortunately, bindings or native ports are available in numerous languages. -To learn more about [ZeroMQ], read [the guide](http://zguide.zeromq.org/page:all). It's an enjoyable and worthwhile read. +To learn more about ZeroMQ, read [the guide](http://zguide.zeromq.org/page:all). It's an enjoyable and worthwhile read. -A quick overview of what [ZeroMQ] provides: +A quick overview of what ZeroMQ provides: -- A simple socket API. +- A simple socket Application Programming Interface (API). - Some well-defined patterns to connect sockets together. -- Sockets that are tiny asynchronous message queues which: - - abstract away complexity around the underlying socket, - - are transport agnostic, meaning you can choose between TCP, PGM, IPC and inproc transports with little or - no changes to code, - - and, they transparently reconnect when connections are dropped. +- Sockets that are tiny asynchronous message queues, which: + - abstract away complexity around the underlying socket; + - are transport agnostic, meaning you can choose between Transmission Control Protocol (TCP), Pragmatic General + Multicast (PGM), Inter-process Communication (IPC) and in-process (inproc) transports with little or no changes to code; + and + - transparently reconnect when connections are dropped. - The `inproc` transport for message passing between threads without mutex locks. - Built-in protocol for asymmetric encryption over the wire using Curve25519. -- The ability to send and receive multipart messages using a simple framing scheme. [More info](http://zguide.zeromq.org/php:chapter2#toc11). -- Support for SOCKS proxies. +- Ability to send and receive multipart messages using a simple framing scheme. More info [here](http://zguide.zeromq.org/php:chapter2#toc11). +- Support for Secure Socket (SOCKS) proxies. This document will refer to several [ZeroMQ socket]s. These are referred to by prepending `ZMQ_` and the name of the socket in capitals. For example, `ZMQ_ROUTER`. -**_Note about [ZeroMQ] frames and multipart messages_** +**Note about ZeroMQ frames and multipart messages:** -[ZeroMQ] frames are length-specified blocks of binary data and can be strung together to make multipart messages. +ZeroMQ frames are length-specified blocks of binary data and can be strung together to make multipart messages. ```text |5|H|E|L|L|O|*|0|*|3|F|O|O|+| @@ -128,22 +129,23 @@ When this RFC mentions 'multipart messages', this is what it's referring to. #### Establishing a Connection -Every participating [communication node] SHOULD open a control socket (see [ControlService]) to allow peers to negotiate and -establish a peer connection. The [NetAddress] of the control socket is what is stored in peer's routing tables and will +Every participating [communication node] SHOULD open a control socket (refer to [ControlService]) to allow peers to negotiate and +establish a peer connection. The [NetAddress] of the control socket is what is stored in peers' routing tables and will be used to establish new ephemeral [PeerConnection]s. Any peer that wants to connect MUST establish a connection -to the control socket of the destination peer to negotiate a new encrypted [PeerConnection]. +to the control socket of the destination peer to negotiate a new encrypted PeerConnection. -Once a connection is established, messages can be sent and received directly to/from the [Peer]. +Once a connection is established, messages can be sent and received directly to or from the [Peer]. Incoming messages are validated, deserialized and handled as appropriate. #### Encryption -There are two forms of encryption which are used: +Two forms of encryption are used: -- Over the wire encryption: encryption of traffic between nodes using zMQ's [CURVE](http://curvezmq.org/page:read-the-docs) +- Over-the-wire encryption, in which traffic between nodes is encrypted using ZMQ's [CURVE](http://curvezmq.org/page:read-the-docs) implementation. -- Payload encryption: the [MessageEnvelopeBody] is encrypted in such a way that only the destination recipient can decrypt it. +- Payload encryption, in which the [MessageEnvelopeBody] is encrypted in such a way that it can only be decrypted by the +destination recipient. ### Components @@ -178,11 +180,11 @@ PEER -->|has many| NA #### NetAddress -Represents one of the following: +Represents: -- an IP address, -- an Onion address, -- or, an I2P address. +- IP address; +- Onion address; or +- I2P address. ```rust,compile_fail #[derive(Clone, PartialEq, Eq, Debug)] @@ -195,9 +197,9 @@ pub enum NetAddress { } ``` -#### Messaging structure +#### Messaging Structure -This illustrates the structure of a Tari message. +The following illustrates the structure of a Tari message: ```text +----------------------------------------+ @@ -222,41 +224,42 @@ This illustrates the structure of a Tari message. +----------------------------------------+ ``` -#### MessageEnvelope wire format +#### MessageEnvelope Wire format Every Tari message MUST use the MessageEnvelope format. This format consists of four frames of a multipart message. -A MessageEnvelope represents a message which has just come off, or is about to go on to the wire and consists of the following: +A MessageEnvelope represents a message that has either just come off or is about to go on to the wire. and consists of +the following: -| Name | Frame | Length (octets) | Type | Description | +| Name | Frame | Length (Octets) | Type | Description | | -------- | ----- | --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | identity | 0 | 8 | `[u8;8]` | The identifier that a `ZMQ_ROUTER` socket expects so that it knows the intended destination of the message. This can be thought of as a session token. | | version | 1 | 1 | `u8` | The wire protocol version. | -| header | 2 | varies | `Vec` | Serialized bytes of data containing an unencrypted [MessageEnvelopeHeader]. | -| body | 3 | varies | `Vec` | Serialized bytes of data containing a unencrypted or encrypted [MessageEnvelopeBody]. | +| header | 2 | Varies | `Vec` | Serialized bytes of data containing an unencrypted [MessageEnvelopeHeader]. | +| body | 3 | Varies | `Vec` | Serialized bytes of data containing an unencrypted or encrypted [MessageEnvelopeBody]. | -The header and decrypted body MUST be deserializable as per [RFC-0171: Message Serialisation](RFC-0171_MessageSerialisation.md) +The header and decrypted body MUST be deserializable as per [RFC-0171: MessageSerialization](RFC-0171_MessageSerialisation.md). #### MessageEnvelopeHeader -Every [MessageEnvelope] MUST have an unencrypted header containing the following fields: +Every MessageEnvelope MUST have an unencrypted header containing the following fields: | Name | Type | Description | | --------- | ------------------------- | -------------------------------------------------------------------------------------------------- | -| version | `u8` | The message protocol version. | -| source | `PublicKey` | The source public key. | -| dest | `Option` | The destination [node ID] or public key. A destination is optional. | +| version | `u8` | Message protocol version. | +| source | `PublicKey` | Source public key. | +| dest | `Option` | Destination [node ID] or public key. A destination is optional. | | signature | `[u8]` | Signature of the message header and body, signed with the private key of the source. | -| flags | `u8` |
  • bit 0: 1 indicates that the message body is encrypted
  • bits 1-7: reserved
| +| flags | `u8` |
  • bit 0: 1 indicates that the message body is encrypted
  • bits 1-7: reserved
. | A [communication node] and [communication client]: - MUST validate the signature of the message using the source's public key. - MUST reject the message if the signature verification fails. -- if the encryption bit flag is set: - - MUST attempt to decrypt the [MessageEnvelopeBody], or failing that - - MUST forward the message to a subset of peers using the `Closest` [BroadcastStrategy] - - MUST discard the message if the body is not encrypted +- If the encryption bit flag is set: + - MUST attempt to decrypt the [MessageEnvelopeBody]; or failing that + - MUST forward the message to a subset of peers using the `Closest` [BroadcastStrategy]. + - MUST discard the message if the body is not encrypted. #### MessageEnvelopeBody @@ -266,21 +269,21 @@ It consists of a [MessageHeader] and [Message] of a particular predefined [Messa #### MessageType -An enumeration of the messages which are part of the Tari network. [MessageType]s are represented -as an unsigned 8-bit integer and each value must be mapped to a corresponding [Message] struct. +An enumeration of the messages that are part of the Tari network. MessageTypes are represented +as an unsigned eight-bit integer and each value must be mapped to a corresponding Message struct. -All [MessageType]s fall within a particular numerical range according to the message's concern: +All MessageTypes fall within a particular numerical range according to the message's concern: -| Category | Range | # message types | Description | -| ------------ | ------- | --------------- | ---------------------------------------------------------------------- | -| reserved | 0 | 1 | Reserved for control messages such as `Ack` | -| `net` | 1-32 | 32 | Network-related messages such as `join` and `discover` | -| `peer` | 33-64 | 32 | Peer connection messages, such as `establish connection` | -| `blockchain` | 65-96 | 32 | Messages related to the blockchain, such as `add block` | -| `vn` | 97-224 | 128 | Messages related to the validator nodes, such as `execute instruction` | -| `extended` | 225-255 | 30 | Reserved for future use | +| Category | Range | # Message Types | Description | +| ------------ | ------- | --------------- | ------------------------------------------------------------ | +| `reserved` | 0 | 1 | Reserved for control messages such as `Ack`. | +| `net` | 1-32 | 32 | Network-related messages such as `join` and `discover`. | +| `peer` | 33-64 | 32 | Peer connection messages, such as `establish connection`. | +| `blockchain` | 65-96 | 32 | Messages related to the blockchain, such as `add block`. | +| `vn` | 97-224 | 128 | Messages related to the validator nodes, such as `execute instruction`. | +| `extended` | 225-255 | 30 | Reserved for future use. | -In documentation, [MessageType]s can be referred to by the category and name. For example, `peer::EstablishConnection` and +In documentation, MessageTypes can be referred to by the category and name. For example, `peer::EstablishConnection` and `net::Discover`. #### MessageHeader @@ -290,35 +293,34 @@ Every Tari message MUST have a header containing the following fields: | Name | Type | Description | | ------------ | ---- | ------------------------------------------------------------------ | | version | `u8` | The message version. | -| message_type | `u8` | An enumeration of the message type of the body. See [MessageType]. | +| message_type | `u8` | An enumeration of the message type of the body. Refer to [MessageType]. | -As this is part of the [MessageEnvelopeBody], it can be encrypted along with the rest of the message +As this is part of the [MessageEnvelopeBody], it can be encrypted along with the rest of the message, which keeps the type of message private. #### MessageBody -Messages are an intention to perform a task, and so MessageType names should be a verb like `net::Join` or `blockchain::AddBlock`. - -All messages can be categorized as follows, each categorization has rules for how they should be handled: - -- a propagation message - - SHOULD NOT have a destination in the MessageHeader - - MUST be forwarded - - SHOULD use the `Random` BroadcastStrategy - - SHOULD discard a message that they have seen within the [DuplicateMessageWindow] -- a direct message - - MUST have a destination in the MessageHeader - - SHOULD be discarded if it does not have a destination - - SHOULD discard a message that they have seen before - - if a destination peer is known, MUST use the `Direct` BroadcastStrategy - - otherwise, SHOULD use the `Closest` BroadcastStrategy -- an encrypted message - - - all recipients MUST attempt to decrypt the message - - recipients MUST forward a message which cannot be decrypted - - SHOULD discard a message that they have seen before - -The [MessageType] in the header MUST be used to determine the type of the message deserialize. +Messages are an intention to perform a task. MessageType names should thus be a verb such as `net::Join` or `blockchain::AddBlock`. + +All messages can be categorized as follows; each categorization has rules for how they should be handled: + +- A propagation message + - SHOULD NOT have a destination in the MessageHeader; + - MUST be forwarded; + - SHOULD use the `Random` BroadcastStrategy; + - SHOULD discard a message that it has seen within the [DuplicateMessageWindow]. +- A direct message + - MUST have a destination in the MessageHeader; + - SHOULD be discarded if it does not have a destination; + - SHOULD discard a message that it has seen before; + - MUST use the `Direct` BroadcastStrategy if a destination peer is known; + - SHOULD use the `Closest` BroadcastStrategy if a destination peer is not known. +- An encrypted message + - MUST undergo an attempt to be decrypted by all recipients; + - MUST be forwarded by recipients if it cannot be decrypted; + - SHOULD discard a message that it has seen before. + +The [MessageType] in the header MUST be used to determine the type of the message deserialized. If the deserialization fails, the message SHOULD be discarded. #### DuplicateMessageWindow @@ -329,15 +331,15 @@ and short enough to not be a burden on the node. #### InboundConnection -A thin wrapper around a `ZMQ_ROUTER` socket which binds to a [NetAddress] and accepts incoming multipart messages. -This connection blocks until there is data to read, or a timeout is reached. In either case, the `receive` method -can be called again (i.e. in a loop) to continue listening for messages. Client code should run this loop in it's own thread. +A thin wrapper around a `ZMQ_ROUTER` socket, which binds to a [NetAddress] and accepts incoming multipart messages. +This connection blocks until there is data to read, or a timeout is reached. In both cases, the `receive` method +can be called again (i.e. in a loop) to continue listening for messages. Client code should run this loop in its own thread. `send` is only called (if at all) in response to an incoming message. Fields may include -- a [NetAddress], -- a timeout, +- a NetAddress; +- a timeout; - underlying [ZeroMQ socket]. Methods may include: @@ -351,24 +353,24 @@ Methods may include: An InboundConnection: - MUST perform the "server-side" [CurveZMQ](http://curvezmq.org/page:read-the-docs) encryption protocol if encryption is set. - - using [ZeroMQ] this means setting the socketopts `ZMQ_CURVE_SERVER` to 1 and `ZMQ_CURVE_SECRETKEY` to the secret key before binding. + - Using [ZeroMQ]. this means setting the socketopts `ZMQ_CURVE_SERVER` to 1 and `ZMQ_CURVE_SECRETKEY` to the secret key before binding. - MUST listen for and accept TCP connections. - - For an IP [NetAddress], bind on the given host IP and port. - - For an Onion [NetAddress], bind on 127.0.0.1 and the given port. - - For an I2P [NetAddress], as yet undetermined. -- MUST read multipart messages and return them to the caller - - if the timeout is reached return an error to be handled by the calling code + - For an IP NetAddress, bind on the given host IP and port. + - For an Onion NetAddress, bind on 127.0.0.1 and the given port. + - For an I2P NetAddress, as yet undetermined. +- MUST read multipart messages and return them to the caller. + - If the timeout is reached, return an error to be handled by the calling code. #### OutboundConnection -A thin wrapper around a `ZMQ_DEALER` socket which connects to a [NetAddress] and sends outbound multipart messages. -This connection blocks until data can be written, or a timeout is reached. The timeout should never be reached as +A thin wrapper around a `ZMQ_DEALER` socket, which connects to a [NetAddress] and sends outbound multipart messages. +This connection blocks until data can be written, or a timeout is reached. The timeout should never be reached, as [ZeroMQ] internally queues messages to be sent. -Fields may include +Fields may include: -- a [NetAddress], -- underlying [ZeroMQ socket], +- a NetAddress; +- underlying [ZeroMQ socket]. Methods may include: @@ -379,54 +381,55 @@ Methods may include: - `set_socks_proxy(address)` - `set_hwm(v)` -An [OutboundConnection]: +An OutboundConnection: - MUST perform the "client-side" [CurveZMQ](http://curvezmq.org/page:read-the-docs) encryption protocol if encryption is set. - - using [ZeroMQ] this means setting the socketopts `ZMQ_CURVE_SERVERKEY`, `ZMQ_CURVE_SECRETKEY` and `ZMQ_CURVE_PUBLICKEY` -- MUST connect to a TCP endpoint - - For an IP [NetAddress], connect to the given host IP and port - - For an Onion [NetAddress], connect to the onion address using the tcp protocol (e.g. `tcp://xyz...123.onion:1234`) - - For an I2P [NetAddress], as yet undetermined -- MUST write the parts of the given [MessageEnvelope] to the socket as a multipart message consisting of, in order: - - identity - - version - - header - - body -- if specified, MUST set a high water mark (HWM) on the underlying [ZeroMQ] socket -- If the HWM is reached, a call to `send` MUST return an error and any messages received SHOULD be discarded + - Using ZeroMQ, this means setting the socketopts `ZMQ_CURVE_SERVERKEY`, `ZMQ_CURVE_SECRETKEY` and `ZMQ_CURVE_PUBLICKEY`. +- MUST connect to a TCP endpoint. + - For an IP NetAddress, connect to the given host IP and port. + - For an Onion NetAddress, connect to the onion address using the TCP, e.g. `tcp://xyz...123.onion:1234`. + - For an I2P NetAddress, as yet undetermined. +- MUST write the parts of the given MessageEnvelope to the socket as a multipart message consisting of, in order: + - identity; + - version; + - header; + - body. +- If specified, MUST set a High Water Mark (HWM) on the underlying ZeroMQ socket. +- If the HWM is reached, a call to `send` MUST return an error and any messages received SHOULD be discarded. #### Peer -A single peer which can communicate on the Tari network. +A single peer that can communicate on the Tari network. Fields may include: -- `addresses` - a list of [NetAddress]es associated with the Peer, perhaps accompanied with some bookkeeping metadata (such as preferred address) -- `node_type` - The type of node or client (i.e [BaseNode], [ValidatorNode], [Wallet], or [TokenWallet]) -- `last_seen` - a timestamp of the last time a message has been sent/received from this Peer -- `flags` - 8-bit flag - - bit 0: is_banned - - bit 1-7: reserved +- `addresses` - a list of [NetAddress]es associated with the peer, perhaps accompanied by some bookkeeping metadata, such +as preferred address; +- `node_type` - the type of node or client, i.e. [BaseNode], [ValidatorNode], [Wallet] or [TokenWallet]); +- `last_seen` - a timestamp of the last time a message has been sent/received from this peer; +- `flags` - 8-bit flag; + - bit 0: is_banned, + - bit 1-7: reserved. -A Peer may also contain reputation metrics (e.g. rejected_message_count, avg_latency) to be used to decide +A peer may also contain reputation metrics (e.g. rejected_message_count, avg_latency) to be used to decide if a peer should be banned. This mechanism is yet to be decided. #### PeerConnection -Represents direct bi-directional connection to another node or client. As connections are bi-directional, -the [PeerConnection] need only hold a single [InboundConnection] or [OutboundConnection], depending on if the -node requested a peer connect to it or it is connecting to a peer. +Represents direct bidirectional connection to another node or client. As connections are bidirectional, +the PeerConnection need only hold a single [InboundConnection] or [OutboundConnection], depending on if the +node requested a peer connect to it or if it is connecting to a peer. PeerConnection will send messages to the peer in a non-blocking, asynchronous manner as long as the connection is maintained. It has a few important functions: -- Manage the underlying network connections; with automatic reconnection if necessary. -- Forward incoming messages onto the given handler socket. -- Send outgoing messages. +- managing the underlying network connections, with automatic reconnection if necessary; +- forwarding incoming messages onto the given handler socket; and +- sending outgoing messages. -Unlike [InboundConnection] and [OutboundConnection] which are essentially stateless, +Unlike InboundConnection and OutboundConnection, which are essentially stateless, `PeerConnection` maintains a particular `ConnectionState`. - `Idle` - the connection has not been established. @@ -434,15 +437,15 @@ Unlike [InboundConnection] and [OutboundConnection] which are essentially statel - `Connected` - the connection has been established. - `Suspended` - the connection has been suspended. Incoming messages will be discarded, calls to `send()` will error. - `Dead` - the connection is no longer active because the connection was dropped. -- `Shutdown` - the connection is no longer active because it was shutdown. +- `Shutdown` - the connection is no longer active because it was shut down. Fields may include: -- a connection state, -- a control socket, -- a peer connection `NetAddress` -- a direction (either `Inbound` or `Outbound`) -- a public key obtained from the connection negotiation +- a connection state; +- a control socket; +- a peer connection `NetAddress`; +- a direction (either `Inbound` or `Outbound`); +- a public key obtained from the connection negotiation; - (optional) SOCKS proxy. Methods may include: @@ -455,17 +458,17 @@ Methods may include: A `PeerConnection`: -- MUST listen for data on the given [NetAddress] using an [InboundConnection] -- MUST sequentially try to connect to one of the peer's [NetAddress]es until one succeeds or all fail using an [OutboundConnection] -- MUST immediately reject and dispose of a multipart message not consisting of four parts, as detailed in [MessageEnvelope]. -- MUST construct a [MessageEnvelope] from the multiple parts. -- MUST pass the constructed [MessageEnvelope] to the message handler. -- Should a connection drop, the connection state MUST transition to `Connecting` and the connection retried. -- When a shutdown signal is received, MUST send a `net::Disconnect` message and drop the connection. +- MUST listen for data on the given [NetAddress] using an InboundConnection; +- MUST sequentially try to connect to one of the peer's NetAddresses until one succeeds or all fail using an OutboundConnection; +- MUST immediately reject and dispose of a multipart message not consisting of four parts, as detailed in MessageEnvelope; +- MUST construct a MessageEnvelope from the multiple parts; +- MUST pass the constructed MessageEnvelope to the message handler; +- MUST transition to `Connecting` state and retry the connection, should a connection drop; +- MUST send a `net::Disconnect` message and drop the connection when a shutdown signal is received. #### ConnectionManager -The ConnectionManager manages a set of live PeerConnections and provides an abstraction for other components +The ConnectionManager manages a set of live PeerConnections. It provides an abstraction for other components to initiate and use PeerConnections without having to worry about attaching the new PeerConnection to message handlers. It consists of a list of active peer connections and an `inproc` message handler socket. This socket is 'written to' whenever @@ -473,20 +476,20 @@ a message is received from any active [PeerConnection] for other components to a Methods may include: -- `establish_connection(Peer)` - create and return a new PeerConnection -- `disconnect(peer)` - disconnect a particular peer -- `suspend()` - temporarily suspend connections -- `resume()` - temporarily suspend connections -- `shutdown` - cleanly shutdown all PeerConnections +- `establish_connection(Peer)` - create and return a new PeerConnection; +- `disconnect(peer)` - disconnect a particular peer; +- `suspend()` - temporarily suspend connections; +- `resume()` - temporarily suspend connections; +- `shutdown` - cleanly shut down all PeerConnections. The `ConnectionManager`: -- MUST call `suspend` on every [PeerConnection] if it's `suspend` method is called -- MUST call `resume` on every [PeerConnection] if it's `resume` method is called -- MUST call `shutdown` on every [PeerConnection] if it's `shutdown` method is called -- MUST create a new [PeerConnection] with the given Peer and NetAddress, when `establish_connection` is called -- MUST call `shutdown` on the [PeerConnection] and remove the connection for the given Peer, when `disconnect(peer)` is called -- MAY disconnect peers if the connection has not been used for an extended period +- MUST call `suspend` on every PeerConnection if its `suspend` method is called; +- MUST call `resume` on every PeerConnection if its `resume` method is called; +- MUST call `shutdown` on every PeerConnection if its `shutdown` method is called +- MUST create a new PeerConnection with the given Peer and NetAddress, when `establish_connection` is called; +- MUST call `shutdown` on the PeerConnection and remove the connection for the given peer, when `disconnect(peer)` is called; +- MAY disconnect peers if the connection has not been used for an extended period; - SHOULD disconnect the least recently used peer if the connection pool is greater than `max connections` #### ControlService @@ -495,62 +498,60 @@ The purpose of this service is to negotiate a new secure PeerConnection. The control service accepts a single message: -- `peer::EstablishConnection(pk, curve_pk, net_address)` +- `peer::EstablishConnection(pk, curve_pk, net_address)`. A ControlService: -- MUST listen for connections on a predefined CONTROL PORT -- SHOULD deny connections from banned peers +- MUST listen for connections on a predefined CONTROL PORT; +- SHOULD deny connections from banned peers. -To establish a peer connection, the following steps apply: +The steps to establish a peer connection are as follows: Alice wants to connect to Bob 1. Alice creates a `PeerConnection` to which Bob can connect. - - A new CURVE keypair is generated + - A new CURVE key pair is generated. 2. Alice connects to Bob's control server and Bob accepts the connection. -3. Alice sends an `peer::establish_connection` message, with - - the CURVE public key for the socket connection, - - the node's public key corresponding to its [Node ID], +3. Alice sends a `peer::establish_connection` message, with: + - the CURVE public key for the socket connection; + - the node's public key corresponding to its [Node ID]; and - the [NetAddress] of the new PeerConnection. -4. Bob accepts this request, and opens a new `PeerConnnection` socket using Alice's CURVE public key. -5. Bob connects to the given [NetAddress] and sends a `peer::establish_connection` message. +4. Bob accepts this request and opens a new `PeerConnnection` socket using Alice's CURVE public key. +5. Bob connects to the given NetAddress and sends a `peer::establish_connection` message. 6. If Alice accepts the connection, they can begin sending messages. If not, both sides terminate the connection. #### PeerManager -The PeerManager's responsibility is to manage the list of peers that the node has previously interacted with. +The PeerManager is responsible for managing the list of peers with which the node has previously interacted. This list is called a routing table and is made up of [Peer]s. The PeerManager can -- add a peer to the routing table, -- search for a peer given a node id, public key or [NetAddress], -- delete a peer from the list, -- persist the peer list using a storage backend, -- restore the peer list from the storage backend, -- maintain lightweight views of peers; using a filter criterion; e.g. a list of peers that have been banned (i.e. a blacklist), -- prune the routing table based on a filter criterion (e.g. last date seen) +- add a peer to the routing table; +- search for a peer given a node ID, public key or [NetAddress]; +- delete a peer from the list; +- persist the peer list using a storage backend; +- restore the peer list from the storage backend; +- maintain lightweight views of peers, using a filter criterion, e.g. a list of peers that have been banned, i.e. a blacklist; and +- prune the routing table based on a filter criterion, e.g. last date seen. #### MessageDispatcher -A MessageDispatcher associates MessageTypes to handlers. Each handler gets a MessageContext as a parameter. - The MessageContext contains: -- the requesting PeerConnection, -- the MessageHeader -- the deserialized message, +- the requesting PeerConnection; +- the MessageHeader; +- the deserialized message; - the OutboundMessageService. Basically, all the tools the handler needs to interact with the network. A MessageDispatcher is responsible for: -- constructing the MessageContext -- finding the message handler which is associated with the MessageType -- passing the MessageContext to the handler -- if the handler cannot be found, the message is ignored +- constructing the MessageContext; +- finding the message handler that is associated with the MessageType; +- passing the MessageContext to the handler; and +- ignoring the message if the handler cannot be found. An example API may be: @@ -567,39 +568,39 @@ inbound_msg_service.set_handler(dispatcher.handler); #### InboundMessageService InboundMessageService is a service that receives messages over a non-blocking asynchronous socket and -determines what to do with it. There are 3 options: handle, forward, discard. +determines what to do with it. There are three options: handle, forward and discard. -A pool of worker threads (with a configurable size) is started and each listen for messages on their $1:n$ `inproc` message +A pool of worker threads (with a configurable size) is started and each one listens for messages on its $1:n$ `inproc` message socket. A `ZMQ_DEALER` socket is suggested for fair-queueing work amongst workers, who listen for work with a `ZMQ_REP`. The workers read off this socket and process the messages. An InboundMessageService: -- MUST receive messages from all PeerConnections -- MUST write the message to the worker socket +- MUST receive messages from all PeerConnections; and +- MUST write the message to the worker socket. A worker: -- MUST deserialize the MessageHeader - - if unable to deserialize, MUST discard the message -- MUST check the message signature +- MUST deserialize the MessageHeader. + - If unable to deserialize, MUST discard the message. +- MUST check the message signature. - MUST discard the message if the signature is invalid. - - MUST discard the message if the signature has been processed within the [DuplicateMessageWindow] + - MUST discard the message if the signature has been processed within the [DuplicateMessageWindow]. - If the encryption flag is set: - - MUST attempt to decrypt the message - - if successful, process and handle the message - - otherwise, MUST forward the message using the `Random` BroadcastStrategy - - if the message is not encrypted, MUST discard it + - MUST attempt to decrypt the message. + - If successful, process and handle the message. + - Otherwise, MUST forward the message using the `Random` BroadcastStrategy. + - If the message is not encrypted, MUST discard it. - If the destination [node ID] is set: - - if the destination match this node's ID: process and handle the message - - if the destination does not match: MUST forward the message using the `Closest` BroadcastStrategy -- If the destination is not set: - - if the MessageType is a kind of propagation message: - - MUST handle the message - - MUST forward the message using the `Random` BroadcastStrategy, - - if the MessageType is a kind of encrypted message: - - MUST attempt to decrypt and handle the message - - if successful, MUST handle the message + - If the destination matches this node's ID - process and handle the message. + - If the destination does not match this node's ID - MUST forward the message using the `Closest` BroadcastStrategy. +- If the destination is not set + - If the MessageType is a kind of propagation message: + - MUST handle the message; + - MUST forward the message using the `Random` BroadcastStrategy. + - If the MessageType is a kind of encrypted message: + - MUST attempt to decrypt and handle the message; + - if successful, MUST handle the message; - if unsuccessful, MUST forward the message using the `Random` or `Flood` BroadcastStrategy, #### OutboundMessageService @@ -609,32 +610,32 @@ send messages to the rest of the network. In particular, it is responsible for: -- serializing the message body -- constructing the [MessageEnvelope] -- executing the required BroadcastStrategy -- sending messages using the [ConnectionManager] +- serializing the message body; +- constructing the MessageEnvelope; +- executing the required BroadcastStrategy; and +- sending messages using the [ConnectionManager]. -The actual sending of messages can be requested via the public `send_message` method which takes a +The actual sending of messages can be requested via the public `send_message` method, which takes a MessageHeader, MessageBody and BroadcastStrategy as parameters. `send_message` then selects an appropriate peer(s) from the ConnectionManager according to the BroadcastStrategy and sends the message to each of the selected peers. -BroadcastStrategy determines how a set of peer nodes will be selected and can be one of: +BroadcastStrategy determines how a set of peer nodes will be selected and can be: -- `Direct` - send to a particular peer matching the given [node ID] -- `Flood` - send to all known peers who are not [communication clients] -- `Closest` - send to $n$ closest peers who are not [communication clients] -- `Random` - send to a random set of peers of size $n$ who are not [communication clients] +- `Direct` - send to a particular peer matching the given [node ID]; +- `Flood` - send to all known peers who are not [communication clients]; +- `Closest` - send to $n$ closest peers who are not [communication clients]; or +- `Random` - send to a random set of peers of size $n$ who are not [communication clients]. ### Privacy Features The following privacy features are proposed: -- A [communication node] or [communication client] MAY communicate solely over the Tor/I2P networks -- All traffic (with the exception of the control service) MUST be encrypted -- Messages MAY encrypt the body of a MessageEnvelope which only a particular recipient can decrypt. -- The `destination` header field can be omitted, when used in conjunction with body encryption the destination is +- A [communication node] or [communication client] MAY communicate solely over the Tor/I2P networks. +- All traffic (with the exception of the control service) MUST be encrypted. +- Messages MAY encrypt the body of a MessageEnvelope, which only a particular recipient can decrypt. +- The `destination` header field can be omitted when used in conjunction with body encryption; the destination is completely unknown to the rest of the network. ### Store and Forward Strategy @@ -646,49 +647,49 @@ The mechanism for this is proposed as follows: Each [communication node] MUST allocate some disk space for storage of messages for offline recipients. Only some whitelisted MessageTypes are permitted to be stored. A sender sends a message destined for a particular -[node ID] to its closest peers which forward the message to their closest peers and so on. +[node ID] to its closest peers, which forward the message to their closest peers, and so on. -Eventually, the message will reach nodes which either know the destination or are very close to the destination. +Eventually, the message will reach nodes that either know the destination or are very close to the destination. These nodes MUST store the message in some pending message bucket for the destination. The maximum number of -buckets and the size of each bucket SHOULD be a sufficiently large as to be unlikely to overflow, but not so +buckets and the size of each bucket SHOULD be sufficiently large as to be unlikely to overflow, but not so large as to approach disk space problems. Individual messages should be small and responsibilities for -storage are spread over the entire network. +storage spread over the entire network. -A [communication node] +A communication node - MUST store messages for later retransmission, if all of the following conditions are true: - - the MessageType is permitted to be stored - - there are fewer than $n$ closer online peers to the destination -- MUST retransmit pending messages when a closer peer comes online or is added to the routing table + - the MessageType is permitted to be stored; + - there are fewer than $n$ closer online peers to the destination. +- MUST retransmit pending messages when a closer peer comes online or is added to the routing table. - MAY remove a bucket, in any of the following conditions: - - the bucket is empty, - - a configured maximum number of buckets has been reached. Discard the bucket with the earliest creation timestamp. - - the number of closer online peers to the destination is equal to or has exceeded $n$ -- MAY expire individual messages after a sufficiently long time to live (ttl) + - The bucket is empty; + - A configured maximum number of buckets has been reached. Discard the bucket with the earliest creation timestamp. + - The number of closer online peers to the destination is equal to or has exceeded $n$. +- MAY expire individual messages after a sufficiently long Time to Live (TTL) This approach has the following benefits: -- When a destination comes online, they'll receive pending messages without having to query them. +- When a destination comes online, it will receive pending messages without having to query them. - The "closer within a threshold" metric is simple. -- Messages are stored on multiple peers which makes it less likely for messages to disappear as nodes come and go +- Messages are stored on multiple peers, which makes it less likely for messages to disappear as nodes come and go (depending on threshold $n$). ### Queue Overflow Strategy -Inbound/OutboundConnections (and therefore PeerConnection) has a high water mark (HWM) set. +Inbound/OutboundConnections (and therefore PeerConnection) have an HWM set. If the HWM is hit: -- any call to `send()` should return an error. -- Incoming messages should be silently discarded. +- any call to `send()` should return an error; and +- incoming messages should be silently discarded. ### Outstanding Items - A PeerConnection will probably need to implement a heartbeat to detect if a peer has gone offline. -- InboundConnection(Service) may want to send small replies (such as OK, ERR) when the message has been accepted/rejected. +- InboundConnection(Service) may want to send small replies (such as OK, ERR) when the message has been accepted or rejected. - OutboundConnection(Service) may want to receive and handle small replies. -- Encrypted communication for the [ControlService] would be better privacy, but since zMQ requires a CURVE public key before - the connection is bound, a dedicated 'secure connection negotiation socket' would be needed. +- Encrypted communication for the [ControlService] would be better privacy, but since ZMQ requires a CURVE public key before + the connection is bound, a dedicated "secure connection negotiation socket" would be needed. - Details of distributed message storage. - Which [NetAddress] to use if a peer has many. @@ -698,12 +699,12 @@ If the HWM is hit: [communication node]: Glossary.md#communication-node [connectioncontext]: #connectioncontext [controlservice]: #controlservice -[MessageEnvelope]: #MessageEnvelope -[MessageEnvelopebody]: #MessageEnvelopebody -[MessageEnvelopeHeader]: #MessageEnvelopeHeader [duplicatemessagewindow]: #duplicatemessagewindow [inboundconnection]: #inboundconnection [message]: #message +[MessageEnvelopeBody]: #messageenvelopebody +[MessageEnvelopeHeader]: #messageenvelopeheader +[messageheader]: #messageheader [messagetype]: #messagetype [netaddress]: #netaddress [node id]: Glossary.md#node-id diff --git a/RFC/src/RFC-0200_BaseLayerExtensions.md b/RFC/src/RFC-0200_BaseLayerExtensions.md deleted file mode 100644 index 812229dd88..0000000000 --- a/RFC/src/RFC-0200_BaseLayerExtensions.md +++ /dev/null @@ -1,6 +0,0 @@ -# RFC-0200/BaseLayerExtensions - -RFC placeholder. - -This document will list all the Tari-specific extensions to the Mimblewimble protocol to enable the digital assets -network. \ No newline at end of file diff --git a/RFC/src/RFC-0230_HTLC.md b/RFC/src/RFC-0230_HTLC.md index 8a175bf21a..709849e33d 100644 --- a/RFC/src/RFC-0230_HTLC.md +++ b/RFC/src/RFC-0230_HTLC.md @@ -1,14 +1,14 @@ -# RFC-0230/Time related transactions +# RFC-0230/Time-related Transactions -## Time related transactions +## Time-related Transactions ![status: draft](theme/images/status-draft.svg) -**Maintainer(s)**: [SW van heerden](https://github.com/SWvheerden) +**Maintainer(s)**: [S W van Heerden](https://github.com/SWvheerden) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2019 The Tari Development Community @@ -22,94 +22,94 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document describes a few extensions to [MimbleWimble] to allow time related transactions. +The aim of this Request for Comment (RFC) is to describe a few extensions to [Mimblewimble] to allow time-related transactions. -## Related RFCs +## Related Requests for Comment * [RFC-0200: Base Layer Extensions](BaseLayerExtensions.md) ## Description -#### Time Locked contracts -In [Mimblewimble] time-locked contracts can be accomplished by modifying the kernel of each transaction to include a +#### Time-locked Contracts +In [Mimblewimble], time-locked contracts can be accomplished by modifying the kernel of each transaction to include a block height. This limits how early in the blockchain lifetime the specific transaction can be included in a block. -This requires users constructing a transaction to: -* MUST include a lock height in the kernel of their transaction, -* MUST include the lock height in the transaction signature to prevent lock height malleability. +This means that users constructing a transaction: +* MUST include a lock height in the kernel of their transaction; and +* MUST include the lock height in the transaction signature to prevent lock height malleability. -Tari Miners: -* MUST not add any transaction to the mined [block] that has not already exceeded its lock height. +Tari [base node]s MUST NOT add any transaction to the mined [block] that has not already exceeded its lock height. -This also adds the following requirement to a [Base Node]: -* MUST reject any [block] that contains a kernel with a lock height greater than the [current head]. +This also adds the following requirement to a [base node]: +* It MUST reject any [block] that contains a kernel with a lock height greater than the [current head]. -#### Time Locked UTXOs -Time locked UTXOs can be accomplished by adding feature flag to a UTXO and a lock height. This allows a limit as to when - in the blockchain lifetime the specific UTXO can be spent. +#### Time-locked UTXOs +Time-locked Unspent Transaction Outputs (UTXOs) can be accomplished by adding a feature flag to a UTXO and a lock height. +This allows a limit on when in the blockchain lifetime the specific UTXO can be spent. This requires that users constructing a transaction: -- MUST include a the feature flag of their UTXO, -- MUST include a lockheight in their UTXO. +- MUST include a feature flag of their UTXO; and +- MUST include a lock height in their UTXO. -This adds the following requirement to a miner: -- MUST not allow a UTXO to be spent if the [current head] has not already exceeded the UTXO's lock height. +This adds the following requirement for a [base node]: +- A [base node] MUST NOT allow a UTXO to be spent if the [current head] has not already exceeded the UTXO's lock height. -This also adds the following requirement to a [base node]: -- MUST reject any [block] that contains a [UTXO] with a lock height not already past the [current head]. +This also adds the following requirement for a [base node]: +- A base node MUST reject any [block] that contains a [UTXO] with a lock height not already past the [current head]. -#### Hashed Time Locked Contract -Hashed time locked contracts are a way of reserving funds for a certain payment, but it only pays out to the receiver if -certain conditions are met. If these are not met withing a time limit, the funds are payed back to the sender. +#### Hashed Time-locked Contract +Hashed time-locked contracts are a way of reserving funds for a certain payment, but they only pay out to the receiver if +certain conditions are met. If these conditions are not met within a time limit, the funds are paid back to the sender. -Unlike Bitcoin where this can be accomplished with a single transaction, in [MimbleWimble] HTLCs involve a multi-step -process to construct a timelocked contract. +Unlike Bitcoin, where this can be accomplished with a single transaction, in [Mimblewimble], HTLCs involve a multi-step +process to construct a time-locked contract. The steps are as follows: -* The sender MUST pay all the funds into a n-of-n [multisig] [UTXO]. -* All parties involved MUST construct a refund [transaction] paying back all funds to the sender, spending this n-of-n - [multisig] [UTXO]. However, this [transaction] has a [transaction lock height](#hashed-time-locked-contract) set in - the future and cannot be immediately mined. It therefore lives in the [mempool]. This means that if anything goes - wrong from here on out, the sender will get his money back after the time lock expires. -* The sender MUST publish both above [transaction]s at the same time to ensure the receiver cannot hold him hostage. +* The sender MUST pay all the funds into an n-of-n [multisig] [UTXO]. +* All parties involved MUST construct a refund [transaction] to pay back all funds to the sender who has spent this n-of-n + [multisig] [UTXO]. However, this [transaction] has a transaction lock height set in + the future and cannot be mined immediately. It therefore lives in the [mempool]. This means that if anything goes + wrong from here on, the sender will get their money back after the time lock expires. +* The sender MUST publish both above [transaction]s at the same time to ensure the receiver cannot hold the sender hostage. * The parties MUST construct a third [transaction] that pays the receiver the funds. This [transaction] typically makes use of a preimage to allow spending of the [transaction] if the user reveals some knowledge, allowing the user to unlock the [UTXO]. -HTLC's in [Mimblewimble] makes use of double spending the n-of-n [multisig] [UTXO] and the +HTLCs in [Mimblewimble] make use of double-spending of the n-of-n [multisig] [UTXO]. The first valid published [transaction] can then be mined and claim the n-of-n [multisig] [UTXO]. -An example of a [HTLC] in practice can be viewed at Tari University: -[Bitcoin atomic swaps](https://tlu.tarilabs.com/protocols/atomic-swaps/AtomicSwaps.html) -[MimbleWimble atomic swaps](https://tlu.tarilabs.com/protocols/grin-protocol-overview/MainReport.html#atomic-swaps) +An example of an [HTLC] in practice can be viewed at Tari University: + +- [Bitcoin atomic swaps](https://tlu.tarilabs.com/protocols/atomic-swaps/AtomicSwaps.html) +- [Mimblewimble atomic swaps](https://tlu.tarilabs.com/protocols/grin-protocol-overview/MainReport.html#atomic-swaps) [HTLC]: Glossary.md#hashed-time-locked-contract [Mempool]: Glossary.md#mempool diff --git a/RFC/src/RFC-0300_DAN.md b/RFC/src/RFC-0300_DAN.md index 9a4bc72711..9d0597b953 100644 --- a/RFC/src/RFC-0300_DAN.md +++ b/RFC/src/RFC-0300_DAN.md @@ -1,14 +1,14 @@ # RFC-0300/DAN -## The Digital Assets Network +## Digital Assets Network ![status: draft](theme/images/status-draft.svg) -**Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77), [Philip Robinson](https://github.com/philipr-za) +**Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) and [Philip Robinson](https://github.com/philipr-za) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2018 The Tari Development Community @@ -22,76 +22,81 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. - + ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -The goal of this RFC is to describe the key features of the Tari second layer, a.k.a the Digital Assets Network (DAN) +The aim of this Request for Comment (RFC) is to describe the key features of the Tari second layer, also known as the +Digital Assets Network (DAN) -## Related RFCs +## Related Requests for Comment -* [RFC-0100: Base layer](RFC-0100_BaseLayer.md) -* [RFC-0311: Digital assets](RFC-0311_AssetTemplates.md) +* [RFC-0100: Base Layer](RFC-0100_BaseLayer.md) +* [RFC-0311: Digital Assets](RFC-0311_AssetTemplates.md) * [RFC-0340: VN Consensus Overview](RFC-0340_VNConsensusOverview.md) -* [RFC-0302: Validator nodes](RFC-0302_ValidatorNodes.md) +* [RFC-0302: Validator Nodes](RFC-0302_ValidatorNodes.md) ## Description ### Abstract -[Digital asset]s (DAs) are managed by committees of special nodes called [Validator node]s (VNs). VNs manage digital asset state change and ensure -that the rules of the asset contracts are enforced. -* VNs form a peer-to-peer communication network that together defines the Tari [Digital Asset Network] (DAN) +[Digital Asset]s (DAs) are managed by committees of special nodes called [Validator Node]s (VNs): + +* VNs manage digital asset state change and ensure that the rules of the asset contracts are enforced. +* VNs form a peer-to-peer communication network that together defines the Tari DAN. * VNs register themselves on the [base layer] and commit collateral to prevent Sybil attacks. -* Scalability is achieved by sacrificing decentralisation. Not *all* VNs manage *every* asset. Assets are managed by +* Scalability is achieved by sacrificing decentralization. Not *all* VNs manage *every* asset. Assets are managed by subsets of the DAN, called VN [committees]. These committees reach consensus on DA state amongst themselves. * VNs earn fees for their efforts. -* Digital asset contracts are not Turing complete, but are instantiated by [Asset Issuer]s (AIs) using Digital Asset templates that are defined +* DA contracts are not Turing complete, but are instantiated by [Asset Issuer]s (AIs) using [DigitalAssetTemplate]s that are defined in the DAN protocol code. ### Digital Assets -* Digital asset contracts are *not* Turing complete, but are selected from a set of [DigitalAssetTemplate]s that govern - the behaviour of each contract type. e.g. there could be a Single-Use Token template for simple ticketing systems; a - Coupon template for loyalty programmes and so on. +* DA contracts are *not* Turing complete, but are selected from a set of [DigitalAssetTemplate]s that govern + the behaviour of each contract type. For example, there could be a Single-use Token template for simple ticketing systems, a + Coupon template for loyalty programmes, and so on. * The template system is intended to be highly flexible and additional templates can be added to the protocol periodically. * Asset issuers can link a Registered Asset Issuer Domain (RAID) ID in an OpenAlias TXT public Domain Name System (DNS) record to a Fully Qualified Domain Name (FQDN) that they own. This is to help disambiguate similar - contracts and improve the signal-to-noise ratio from scam- or copy-cat contracts. + contracts and improve the signal-to-noise ratio from scam or copycat contracts. -An [Asset Issuer] (AI) will issue a Digital Assets by constructing a contract from one of the supported set of [DigitalAssetTemplate]s. The AI will choose -how large the committee of Validator Nodes will be for this DA and have the option to nominate [Trusted Node]s to be part of the VN committee for the DA. -Any remaining spots on the committee will be filled by permissionless VNs that are selected according to a [CommitteeSelectionStrategy]. This is a strategy -that an AI will use to select from the set of potential candidate VNs that nominated themselves for a position on the committee when the AI broadcast a public call for VNs during the asset creation process. For the VNs to accept the appointment to the committee they will need to put up the specified collateral. +An AI will issue a DA by constructing a contract from one of the supported set of [DigitalAssetTemplate]s. The AI will choose +how large the committee of VNs will be for this DA, and have the option to nominate [Trusted Node]s to be part of the VN +committee for the DA. +Any remaining spots on the committee will be filled by permissionless VNs that are selected according to a +[CommitteeSelectionStrategy]. This is a strategy that an AI will use to select from the set of potential candidate VNs +that nominated themselves for a position on the committee when the AI broadcast a public call for VNs during the asset +creation process. For the VNs to accept the appointment to the committee, they will need to put up the specified collateral. -[asset issuer]: Glossary.md#asset-issuer +[Asset Issuer]: Glossary.md#asset-issuer [base layer]: Glossary.md#base-layer -[digital asset]: Glossary.md#digital-asset [committees]: Glossary.md#committee [CommitteeSelectionStrategy]: Glossary.md#committeeselectionstrategy -[validator node]: Glossary.md#validator-node +[digital asset]: Glossary.md#digital-asset [digital asset network]: Glossary.md#digital-asset-network -[trusted node]: Glossary.md#trusted-node [DigitalAssetTemplate]: Glossary.md#digitalassettemplate +[trusted node]: Glossary.md#trusted-node +[validator node]: Glossary.md#validator-node \ No newline at end of file diff --git a/RFC/src/RFC-0301_NamespaceRegistration.md b/RFC/src/RFC-0301_NamespaceRegistration.md index 22455ed5a9..192382f99d 100644 --- a/RFC/src/RFC-0301_NamespaceRegistration.md +++ b/RFC/src/RFC-0301_NamespaceRegistration.md @@ -1,14 +1,14 @@ # RFC-0301/NamespaceRegistration -## Namespace Registration on the Base Layer +## Namespace Registration on Base Layer ![status: raw](./theme/images/status-draft.svg) **Maintainer(s)**: [Hansie Odendaal](https://github,com/hansieodendaal) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2019 The Tari Development Community @@ -22,38 +22,38 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS ORRAID -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This purpose of this document is to describe and specify the process for creating and linking an [asset issuer] +The aim of this Request for Comment (RFC) is to describe and specify the process for creating and linking an [asset issuer] specified domain name with a [digital asset] on the Digital Assets Network ([DAN]). -## Related RFCs +## Related Requests for Comment - [RFC-0341/AssetRegistration](RFC-0341_AssetRegistration.md) - [RFC-0310/AssetTemplates](RFC-0311_AssetTemplates.md) @@ -68,22 +68,22 @@ specified domain name with a [digital asset] on the Digital Assets Network ([DAN ### Alternative Approaches -In order to easily differentiate different [digital asset]s in the DAN, apart from some unique unpronounceable -character string, a human readable identifier (domain name) is required. It is perceived that shorter names will have -higher value due to branding and marketability, and the question is how this can be managed elegantly. It is also -undesirable if for example the real Disney is forced to use the long versioned "*disney.com-goofy-is-my-asset-yes*" -because some fake Disneys claimed "*goofy*" and "*disney.com-goofy*" and everything in between. +In order to easily differentiate [digital asset]s in the DAN, apart from some unique unpronounceable +character string, a human-readable identifier (domain name) is required. It is perceived that shorter names will have +higher value due to branding and marketability. The question is, how this can be managed elegantly? It is also +undesirable if, for example, the real Disney is forced to use the long versioned "*disney.com-goofy-is-my-asset-yes*" +because some fake Disneys claimed "*goofy*" and "*disney.com-goofy*" and everything in-between. One method to curb name space squatting is to register names on the [base layer] layer with a domain name registration transaction. Let us call such a name a Registered Asset Issuer Name (RAIN). To make registering RAINs difficult enough to prevent spamming the network, a certain amount of [Tari coins] must be committed in a burn (permanently destroy) or -a time locked pay to self type transaction. Lots of management overhead will be associated with such a scheme, even if -domain-less assets are allowed. However, it would be impossible to stop someone -from registering say a "*disney.com*" RAIN if they do not own the real "*disney.com*" Fully Qualified Domain Name (FQDN). +a time-locked pay-to-self type transaction. Lots of management overhead will be associated with such a scheme, even if +domainless assets are allowed. However, it would be impossible to stop someone +from registering, say, a "*disney.com*" RAIN if they did not own the real "*disney.com*" Fully Qualified Domain Name (FQDN). -Another approach would be to make use of the public Domain Name System (DNS) and to link the FQDNs, that are already +Another approach would be to make use of the public Domain Name System (DNS) and to link the FQDNs that are already registered, to the [digital asset]s in the DAN, making use of [OpenAlias](https://openalias.org/) text (TXT) DNS -records on a FQDN. Let us call this a Registered Asset Issuer Domain (RAID) TXT record. If we hash a public key and +records on an FQDN. Let us call this a Registered Asset Issuer Domain (RAID) TXT record. If we hash a public key and FQDN pair, it will provide us with a unique RAID_ID (RAID Identification). The RAID_ID will serve a similar purpose in the DAN as a Top Level Domain (TLD) in a DNS, as all digital assets belonging to a specific [asset issuer] could then be grouped under the FQDN by inference. To make this scheme more elaborate, but potentially unnecessary, all such @@ -93,21 +93,21 @@ If the standard Mimblewimble protocol is followed, a new output feature can be d `(RAID_ID, PubKey)` that is linked to a specific Unspent Transaction Output ([UTXO]). The `RAID_ID` could be based on a [Base58Check](https://en.bitcoin.it/wiki/Base58Check_encoding) variant applied to `Hash256(PubKey || FQDN)`. If the amount of Tari coins associated with the RAID tuple transaction is burned, `(RAID_ID, PubKey)` will forever be present -on the blockchain and can increase blockchain bloat. On the other hand, if those [Tari coins] are spent back to its +on the blockchain and can increase blockchain bloat. On the other hand, if those [Tari coins] are spent back to their owner with a specific time lock, it will be possible to spend and prune that UTXO later on. While that UTXO remains unspent, the RAID tuple will be valid, but when all or part of it is spent, the RAID tuple will disappear from the -blockchain. Such a UTXO will thus be "colored" while unspent as it will have different properties to a normal UTXO. It +blockchain. Such a UTXO will thus be "coloured" while unspent, as it will have different properties to a normal UTXO. It will also be possible to recreate the original RAID tuple by registering it using the original `Hash256(PubKey || FQDN)`. -Thinking about the makeup of the `RAID_ID` it is evident that it can easily be calculated on the fly using the public -key and FQDN, both of which values will always be known. The biggest advantage having the RAID tuple on the [base +Thinking about the make-up of the `RAID_ID`, it is evident that it can easily be calculated on the fly using the public +key and FQDN, the values of which will always be known. The biggest advantage of having the RAID tuple on the [base layer] is that of embedded consensus, where it will be validated (as no duplicates can be allowed) and mined before it can be used. However, this comes at the cost of more complex code, a more elaborate asset registration process and higher asset registration fees. -### This RFC +### This Request for Comment -This document explores the creation and use of RAID TXT records to link asset issuer specified domain names with +This RFC explores the creation and use of RAID TXT records to link asset issuer-specified domain names with digital assets on the [DAN], without RAID_IDs being registered on the [base layer]. @@ -118,22 +118,26 @@ digital assets on the [DAN], without RAID_IDs being registered on the [base laye ### OpenAlias TXT DNS Records -An OpenAlias TXT DNS record [[1]] on a FQDN is a single string and starts with "*oa1:\*" field followed by a -number of key-value pairs. Standard (optional) key-values are: "*recipient_address*"; "*recipient_name*"; -"*tx_description*"; "*tx_amount*"; "*tx_payment_id*"; "*address_signature*" and "*checksum*". Additional key-values +An OpenAlias TXT DNS record [[1]] on an FQDN is a single string and starts with "*oa1:\*" field followed by a +number of key-value pairs. Standard (optional) key values are: "*recipient_address*"; "*recipient_name*"; +"*tx_description*"; "*tx_amount*"; "*tx_payment_id*"; "*address_signature*"; and "*checksum*". Additional key values may also be defined. Only entities with write access to a specific DNS record will be able to create the required TXT DNS record entries. -TXT DNS records are limited to multiple strings of size 255, and as the User Datagram Protocol (UDP) size is 512 bytes, a TXT DNS record that exceeds that limit is less optimal [[2], Sections 3.3 & 3.4]. Some hosting sites also pose limitations to TXT DNS record string lengths and concatenation of multiple strings as per [[2]]. The basic idea of this specification is to make the implementation robust and flexible at the same time. +TXT DNS records are limited to multiple strings of size 255, and as the User Datagram Protocol (UDP) size is 512 bytes, +a TXT DNS record that exceeds that limit is less optimal [[2], Sections 3.3 and 3.4]. Some hosting sites also place +limitations on TXT DNS record string lengths and concatenation of multiple strings as per [[2]]. The basic idea of this +specification is to make the implementation robust and flexible at the same time. **Req** - Integration with public DNS records MUST be used to ensure valid ownership of an FQDN that needs to be linked to a [digital asset] on the DAN. -**Req** - The total size of the OpenAlias TXT DNS record SHOULD not exceed 255 characters. +**Req** - The total size of the OpenAlias TXT DNS record SHOULD NOT exceed 255 characters. -**Req** - The total size of the OpenAlias TXT DNS record MUST not exceed 512 characters. +**Req** - The total size of the OpenAlias TXT DNS record MUST NOT exceed 512 characters. -**Req** - The OpenAlias TXT DNS record implementation MUST make provision to interpret entries that are made up of more than one string as defined in [[2]]. +**Req** - The OpenAlias TXT DNS record implementation MUST make provision to interpret entries that are made up of more +than one string as defined in [[2]]. **Req** - The OpenAlias TXT DNS record SHOULD adhere to the formatting requirements as specified in [[1]]. @@ -143,13 +147,14 @@ linked to a [digital asset] on the DAN. | ------------------------------ | ------------------------------------------------------------ | | oa1:\ | "oa1:tari" | | pk | \<256 bit public key, in hexadecimal format (64 characters), that is converted into a `Base58` encoded string (44 characters)\> | -| raid_id | \<`RAID_ID` (*see [The RAID_ID](#the-raid_id)*) (15 characters)\> | +| raid_id | \<`RAID_ID` (*refer to [RAID_ID](#raid_id)*) (15 characters)\> | | nonce | \<256 bit public nonce, in hexadecimal format (64 characters), that is converted into a `Base58` encoded string (44 characters)\> | -| sig | \<[Asset issuer]'s 256 bit Schnorr signature for the `RAID_ID` (*see [The RAID_ID](#the-raid_id)*), in hexadecimal format (64 characters), that is converted into a `Base58` encoded string (44 characters)\> | +| sig | \<[Asset issuer]'s 256 bit Schnorr signature for the `RAID_ID` (*refer to [RAID_ID](#raid_id)*), in hexadecimal format (64 characters), that is converted into a `Base58` encoded string (44 characters)\> | | desc | \; ASCII String; Up to 48 characters for the condensed version (using only one string) and up to 235 characters (when spanning two strings). | -| crc | \ | +| crc | \ | -    Examples: Two example OpenAlias TXT DNS records are shown; the first a condensed version and the second one that spans two strings: +    **Examples:** Two example OpenAlias TXT DNS records are shown; the first is a condensed version and +the second spans two strings: ``` text RAID_ID: @@ -165,48 +170,48 @@ public nonce = fc2c5fce596338f43f70dc0ce14659fdfea1ba3e588a7c6fa79957fc70aa1b4b signature = 7dc54ec98da2350b0c8ed0561537517ac6f93a37f08a34482824e7df3514ce0d -> base58 = 9TxTorviyTJAaVJ4eY4AQPixwLb6SDL4dieHff6MFUha -OpenAlias TXT DNS record (condensed: 211 characters): +OpenAlias TXT DNS record (condensed: 212 characters): IN TXT = "oa1:tari pk=EcbmnM6PLosBzpyCcBz1TikpNXRKcucpm73ez6xYfLtg;id=RYqMMuSmBZFQkgp; nonce=5ctFNnCfBrP99rT1AFmj1WPyMD8uAdNUTESHhLoV3KBZ;sig=9TxTorviyTJAaVJ4eY4AQPixwLb6SDL4dieHff6MFUha; -desc=Cartoon charaters;crc=B31C7CA0" +desc=Cartoon characters;crc=176BE80C" -OpenAlias TXT DNS record (spanning two strings: string 1 = 179 characters and string 2 = 249 characters): +OpenAlias TXT DNS record (spanning two strings: string 1 = 179 characters and string 2 = 250 characters): IN TXT = "oa1:tari pk=EcbmnM6PLosBzpyCcBz1TikpNXRKcucpm73ez6xYfLtg; id=RYqMMuSmBZFQkgp; nonce=5ctFNnCfBrP99rT1AFmj1WPyMD8uAdNUTESHhLoV3KBZ; sig=9TxTorviyTJAaVJ4eY4AQPixwLb6SDL4dieHff6MFUha; " -"desc=Cartoon charaters: Mickey Mouse\; Minnie Mouse\; Goofy\; Donald Duck\; Pluto\; Daisy Duck\; +"desc=Cartoon characters: Mickey Mouse\; Minnie Mouse\; Goofy\; Donald Duck\; Pluto\; Daisy Duck\; Scrooge McDuck\; Launchpad McQuack\; Huey, Dewey and Louie\; Bambi\; Thumper\; Flower\; Faline\; -Tinker Bell\; Peter Pan and Captain Hook.; crc=96EC6593" +Tinker Bell\; Peter Pan and Captain Hook.; crc=54465902" ``` -### The RAID_ID +### RAID_ID Because the `RAID_ID` does not exist as an entity on the base layer or in the [DAN], it cannot be owned or -transferred, but only be verified as part of the OpenAlias TXT DNS record [[1]] verification. If an asset creator -chooses not to link a `RAID_ID` and FQDN, a default network assigned `RAID_ID` will be used in the digital asset +transferred, but can only be verified as part of the OpenAlias TXT DNS record [[1]] verification. If an asset creator +chooses not to link a `RAID_ID` and FQDN, a default network-assigned `RAID_ID` will be used in the digital asset registration process. -**Req** - A default `RAID_ID` MUST be used where it will not be linked to a FQDN, for example it MAY be derived +**Req** - A default `RAID_ID` MUST be used where it will not be linked to an FQDN, e.g. it MAY be derived from a default input string `"No FQDN"`. -**Req** - A FQDN linked (non-default) `RAID_ID` MUST be derived from the concatenation `PubKey || `. +**Req** - An FQDN-linked (non-default) `RAID_ID` MUST be derived from the concatenation `PubKey || `. **Req** - All concatenations of inputs for any hash algorithm MUST be done without adding any spaces. -**Req** - The hash algorithm MUST be `Blake2b` using a 32 byte digest size unless otherwise specified. +**Req** - The hash algorithm MUST be `Blake2b` using a 32 byte digest size, unless otherwise specified. **Req** - Deriving a `RAID_ID` MUST be calculated as follows: -- Inputs for all hashing algorithms used to calculate the `RAID_ID` MUST be lower case characters. +- Inputs for all hashing algorithms used to calculate the `RAID_ID` MUST be lower-case characters. - Stage 1 - MUST select the input string to use (either `"No FQDN"` or `PubKey || `). - Example: Mimblewimble public key `ca469346d7643336c19155fdf5c6500a5232525ce4eba7e4db757639159e9861` and FQDN - `disney.com` is used here, resulting in `ca469346d7643336c19155fdf5c6500a5232525ce4eba7e4db757639159e9861disney.com`. + `disney.com` are used here, resulting in `ca469346d7643336c19155fdf5c6500a5232525ce4eba7e4db757639159e9861disney.com`. - Stage 2 - MUST perform `Blake2b` hashing on the result of stage 1 using a 10 byte digest size. - - Example: In hexadecimal representation `ff517a1387153cc38009` or binary representation`\xffQz\x13\x87\x15<\xc3\x80\t` + - Example: In hexadecimal representation `ff517a1387153cc38009` or binary representation`\xffQz\x13\x87\x15<\xc3\x80\t`. - Stage 3 - MUST concatenate the `RAID_ID` identifier byte, `0x62`, with the result of stage 2. - - Example: `62ff517a1387153cc38009` ` -- Stage 4 - MUST convert the result of stage 3 from a byte string into `Base58` encoded string. + - Example: `62ff517a1387153cc38009` `. +- Stage 4 - MUST convert the result of stage 3 from a byte string into a `Base58` encoded string. This will result in a 15 character string starting with `R`. - Example: The resulting `RAID_ID` will be `RYqMMuSmBZFQkgp`. @@ -217,30 +222,31 @@ the challenge `e` being `e = Blake2b(PubNonce || PubKey || RAID_ID)`. ### Sequence of Events -The sequence of events leading up to digital asset registration are perceived as follows: +The sequence of events leading up to digital asset registration is perceived as follows: -1. The [asset issuer] will decide if the default `RAID_ID` or a `RAID_ID` that is linked to a FQDN must be used for +1. The [asset issuer] will decide if the default `RAID_ID` or a `RAID_ID` that is linked to an FQDN must be used for asset registration. (_**Note:** A single linked (`RAID_ID`, FQDN) tuple may be associated with multiple digital assets from the same asset issuer._) 2. **Req** - If a default `RAID_ID` is required: - 1. The asset issuer MUST use the default `RAID_ID` (see [The RAID_ID](#the-raid_id)). + 1. The asset issuer MUST use the default `RAID_ID` (refer to [RAID_ID](#raid_id)). 2. The asset issuer MUST NOT sign the `RAID_ID`. 3. **Req** - If a linked (`RAID_ID`, FQDN) tuple is required: 1. The asset issuer MUST create a `RAID_ID`. - 2. The asset issuer MUST sign the `RAID_ID` as specified (see [The RAID_ID](#the-raid_id)). - 3. The asset issuer MUST create a valid TXT DNS record (see [OpenAlias TXT DNS Records](#openalias-txt-dns-records)). + 2. The asset issuer MUST sign the `RAID_ID` as specified (refer to [RAID_ID](#raid_id)). + 3. The asset issuer MUST create a valid TXT DNS record (refer to [OpenAlias TXT DNS Records](#openalias-txt-dns-records)). -4. **Req** - [Validator Node]s (VN) MUST only allow a valid `RAID_ID` to be used in the digital asset registration +4. **Req** - [Validator Node]s (VNs) MUST only allow a valid `RAID_ID` to be used in the digital asset registration process. 5. **Req** - VNs MUST verify the OpenAlias TXT DNS record if a linked (`RAID_ID`, FQDN) tuple is used: - 1. Verify that all fields have been completed as per the specification (see - [OpenAlias TXT DNS Records](#openalias-txt-dns-records)). - 2. Verify that the `RAID_ID` can be calculated from information provided in the TXT DNS record and the FQDN of the public DNS record it is in. + 1. Verify that all fields have been completed as per the specification (refer to + [OpenAlias TXT DNS Records](#openalias-txt-dns-records)). + 2. Verify that the `RAID_ID` can be calculated from information provided in the TXT DNS record and the FQDN of the + public DNS record it is in. 3. Verify that the asset issuer's `RAID_ID` signature is valid. 4. Verify the checksum. @@ -252,11 +258,11 @@ To prevent client lookups from leaking, OpenAlias recommends making use of [DNSC resolution via DNSCrypt-compatible resolvers that support Domain Name System Security Extensions (DNSSEC) and without DNS requests being logged. -**Req** - [Token Wallet]s (TW) and VNs SHOULD implement the following confidentiality and security measures when +**Req** - [Token Wallet]s (TWs) and VNs SHOULD implement the following confidentiality and security measures when dealing with OpenAlias TXT DNS records: - All queries SHOULD make use of the DNSCrypt protocol. -- Resolution SHOULD be forced via DNSCrypt-compatible resolvers that +- Resolution SHOULD be forced via DNSCrypt-compatible resolvers that: - support DNSSEC; - do not log DNS requests. - The DNSSEC trust chain validity: @@ -267,13 +273,13 @@ dealing with OpenAlias TXT DNS records: ## References -[[1]] Crate openalias [online]. Available: https://docs.rs/openalias/0.2.0/openalias/index.html. +[[1]] Crate Openalias [online]. Available: . Date accessed: 2019-03-05. [1]: https://docs.rs/openalias/0.2.0/openalias/index.html "Crate openalias" [[2]] RFC 7208: Sender Policy Framework (SPF) for Authorizing Use of Domains in Email, Version 1 [online]. -Available: https://tools.ietf.org/html/rfc7208. Date accessed: 2019-03-06. +Available: . Date accessed: 2019-03-06. [2]: https://tools.ietf.org/html/rfc7208 "RFC 7208" diff --git a/RFC/src/RFC-0302_ValidatorNodes.md b/RFC/src/RFC-0302_ValidatorNodes.md index 9738b2dc57..4942d175ae 100644 --- a/RFC/src/RFC-0302_ValidatorNodes.md +++ b/RFC/src/RFC-0302_ValidatorNodes.md @@ -4,11 +4,11 @@ ![status: draft](theme/images/status-draft.svg) -**Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77), [Philip Robinson](https://github.com/philipr-za) +**Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) and [Philip Robinson](https://github.com/philipr-za) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2018 The Tari Development Community @@ -22,48 +22,52 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. - + ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -The goal of this RFC is to describe the responisibilities of Validator Nodes (VNs) on the DAN. +The aim of this Request for Comment (RFC) is to describe the responsibilities of Validator Nodes (VNs) on the Digital +Asset Network (DAN). -## Related RFCs +## Related Requests for Comment * [RFC-0322: Validator Node Registration](RFC-0322_VNRegistration.md) -* [RFC-0304: Validator Node committee selection](RFC-0304_VNCommittees.md) +* [RFC-0304: Validator Node Committee Selection](RFC-0304_VNCommittees.md) * [RFC-0340: VN Consensus Overview](RFC-0340_VNConsensusOverview.md) ## Description ### Abstract -[Validator Node]s form the basis of the second layer DAN. All actions on this network take place by interacting with VN's. Some examples of actions +[Validator Node]s form the basis of the second-layer DAN. All actions on this network take place by interacting with VNs. +Some examples of actions that VNs will facilitate are: -* Issuing a [Digital Asset], -* querying the state of [Digital Asset] and its constituent [tokens], -* issuing an instruction to change the state of a [Digital Asset] or [tokens]. -VNs will also perform archival functions for the assets they manage. The lifetime of these archives and the fee structure for this function is +* issuing a [Digital Asset] (DA); +* querying the state of a DA and its constituent [tokens]; and +* issuing an instruction to change the state of a DA or tokens. + +VNs will also perform archival functions for the assets they manage. The lifetime of these archives and the fee structure +for this function are still being discussed. #### Registration @@ -71,46 +75,57 @@ VNs register themselves on the [Base Layer] using a special [transaction] type. Validator node registration is described in [RFC-0322](RFC-0322_VNRegistration.md). -#### Execution of instructions -VNs are expected to manage the state of digital assets on behalf of digital asset issuers. They receive fees as reward +#### Execution of Instructions +VNs are expected to manage the state of DAs on behalf of DA issuers. They receive fees as reward for doing this. -* Digital assets consist of an initial state plus a set of state transition rules. These rules are set by the Tari + +* DAs consist of an initial state plus a set of state transition rules. These rules are set by the Tari protocol, but will usually provide parameters that must be specified by the [Asset Issuer]. -* The set of VNs that participate in managing state of a specific DA is called a [Committee]. A committee is selected during the asset +* The set of VNs that participate in managing state of a specific DA is called a [Committee]. A committee is selected +during the asset issuance process and membership of the committee can be updated at [Checkpoint]s. -* It is the VNs responsibility to ensure that every state change in a digital asset conforms to the contract's rules. -* VNs accept digital asset [Instructions] from clients and peers. [Instructions] allow for creating, updating, expiring and archiving digital assets on the DAN. -* VNs provide additional collateral, called [AssetCollateral], when accepting an offer to manage an asset, which is stored in a multi-signature - UTXO on the base layer. This collateral can be taken from the VN if it is proven that the VN engaged in +* The VN is responsible for ensuring that every state change in a DA conforms to the contract's rules. +* VNs accept DA [Instructions] from clients and peers. Instructions allow for creating, updating, expiring and +archiving DAs on the DAN. +* VNs provide additional collateral, called [AssetCollateral], when accepting an offer to manage an asset, which is +stored in a multi-signature (multi-sig) + Unspent Transaction Output (UTXO) on the base layer. This collateral can be taken from the VN if it is proven that the + VN engaged in malicious behaviour. -* VNs participate in fraud proof validations in the event of consensus disputes (which could result in the malicious VN's +* VNs participate in fraud-proof validations in the event of consensus disputes (which could result in the malicious VN's collateral being slashed). -* Digital asset metadata (e.g. large images) are managed by VNs. The large data itself will not be stored on the VNs but an external location and a hash of the data can be stored. Whether the data is considered part of the state - (and thus checkpointed) or out of state depends on the type of digital asset contract employed. +* DA metadata (e.g. large images) is managed by VNs. The large data itself will not be stored on the VNs, but +in an external location, and a hash of the data can be stored. Whether the data is considered part of the state +(and thus checkpointed) or out of state depends on the type of DA contract employed. #### Fees -Fees will be paid to VNs based on the amount of work they did during a checkpoint period. The fees will be paid from a fee pool which will be collected -and reside in a UTXO that is accessible by the committee. The exact mechanism for the the payment of the fees by the committee and who pays the various +Fees will be paid to VNs based on the amount of work they did during a checkpoint period. The fees will be paid from a +fee pool, which will be collected +and reside in a UTXO that is accessible by the committee. The exact mechanism for the payment of the fees by the +committee and who pays the various types of fees is still under discussion. #### Checkpoints -VNs periodically post checkpoint summaries to the [base layer] for each asset that they are managing. The checkpoints will form an immutable -record of the [Digital Asset] state on the base-layer. There will be two types of checkpoints: -* An Opening checkpoint (OC) will: - * Specify the members of the VN committee. - * Lock up the collateral for the committee members for this checkpoint period. - * Collect the fee pool for this checkpoint period from the Asset Issuer into a Multi-Sig UTXO under the control of the committee. +VNs periodically post checkpoint summaries to the [base layer] for each asset that they are managing. The checkpoints +will form an immutable +record of the DA state on the base layer. There will be two types of checkpoints: +* An Opening Checkpoint (OC) will: + * specify the members of the VN committee; + * lock up the collateral for the committee members for this checkpoint period; and + * collect the fee pool for this checkpoint period from the Asset Issuer into a multi-sig UTXO under the control of the + committee. This can be a top-up of the fees or a whole new fee pool. -* A Closing checkpoint (CC) will: - * Summarize the Digital Asset state on the base layer. - * Release the fee pay outs. - * Release the collateral for the committee members for this checkpoint period. - * Allow for committee members to resign from the committee +* A Closing Checkpoint (CC) will: + * summarize the DA state on the base layer; + * release the fee payouts; + * release the collateral for the committee members for this checkpoint period; and + * allow for committee members to resign from the committee. -After an DA is issued there will immediately be an Opening checkpoint. After a checkpoint period there will then be a Closing checkpoint followed -immediately by an Opening checkpoint for the next period, we will call this set of checkpoints an Intermediate checkpoint, which could be a compressed combination of an opening and closing checkpoint. This will continue -until the end of the asset's lifetime where there will be a final Closing checkpoint that will be followed by the retirement of the asset. +After a DA is issued, there will immediately be an OC. After a checkpoint period there will then be a +CC, followed +immediately by an OC for the next period. We will call this set of checkpoints an Intermediate checkpoint, which could be a compressed combination of an OC and CC. This will continue +until the end of the asset's lifetime, when there will be a final CC that will be followed by the retirement of the asset.
graph LR; @@ -133,19 +148,20 @@ graph LR;
#### Consensus -Committees of VNs will use a [ConsensusStrategy] to manage the process of -* Propogating signed instructions between members of the committee. -* Determining when the threshold has been reached for an instruction to be considered valid. +Committees of VNs will use a [ConsensusStrategy] to manage the process of: +* propagating signed instructions between members of the committee; and +* determining when the threshold has been reached for an instruction to be considered valid. -Part of the [ConsensusStrategy] will be mechanisms for detecting actions by [Bad Actor]s. The nature of the enforcement actions that can be taken -against bad actors are still to be decided. +Part of the Consensus Strategy will be mechanisms for detecting actions by [Bad Actor]s. The nature of the enforcement +actions that can be taken +against bad actors is still to be decided. -### Network communication -The VNs will communicate using a peer-to-peer (P2P) network. To facilitate this this VNs must perform the following functions: -* VNs MUST maintain a list of peers, and which assets each peer is managing. +### Network Communication +The VNs will communicate using a Peer-to-Peer (P2P) network. To facilitate this, the VNs must perform the following functions: +* VNs MUST maintain a list of peers, including which assets each peer is managing. * VNs MUST relay [instructions] to members of the committee that are managing the relevant asset. -* VNs MUST respond to requests for information about digital assets that they manage on the DAN. -* VNs and clients can advertise public keys to facilitate P2P communication encryption +* VNs MUST respond to requests for information about DAs that they manage on the DAN. +* VNs and clients can advertise public keys to facilitate P2P communication encryption. [assetcollateral]: Glossary.md#assetcollateral [asset issuer]: Glossary.md#asset-issuer @@ -154,16 +170,8 @@ The VNs will communicate using a peer-to-peer (P2P) network. To facilitate this [digital asset]: Glossary.md#digital-asset [checkpoint]: Glossary.md#checkpoint [committee]: Glossary.md#committee -[CommitteeSelectionStrategy]: Glossary.md#committeeselectionstrategy [ConsensusStrategy]: Glossary.md#consensusstrategy [validator node]: Glossary.md#validator-node -[digital asset network]: Glossary.md#digital-asset-network [transaction]: Glossary.md#transaction -[tari coin]: Glossary.md#tari-coin [tokens]: Glossary.md#digital-asset-tokens -[trusted node]: Glossary.md#trusted-node [instructions]: Glossary.md#instructions -[RegistrationCollateral]: Glossary.md#registrationcollateral -[RegistrationTerm]: Glossary.md#registrationterm -[DigitalAssetTemplate]: Glossary.md#digitalassettemplate -[node ID]: Glossary.md#node-id diff --git a/RFC/src/RFC-0304_VNCommittees.md b/RFC/src/RFC-0304_VNCommittees.md index 1ad3b43f00..1056e80fc9 100644 --- a/RFC/src/RFC-0304_VNCommittees.md +++ b/RFC/src/RFC-0304_VNCommittees.md @@ -1,16 +1,16 @@ # RFC-0304/VNCommittees -## Validator Node committee selection +## Validator Node Committee Selection ![status: draft](theme/images/status-draft.svg) -**Maintainer(s)**: Philip Robinson +**Maintainer(s)**: [Philip Robinson](https://github.com/philipr-za) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). -Copyright 2019. The Tari Development Community +Copyright 2019 The Tari Development Community Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -22,44 +22,48 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in -[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in +[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document will describe the process an [Asset Issuer] (AI) will go through in order to select the committee of [Validator Node]s +The aim of this Request for Comment (RFC) is to describe the process an [Asset Issuer] (AI) will go through in order to +select the committee of [Validator Node]s (VNs) that will serve a given [Digital Asset] (DA). -## Related RFCs -* [RFC-0311: Digital Asset templates](RFC-0311_AssetTemplates.md) +## Related Requests for Comment +* [RFC-0311: Digital Asset Templates](RFC-0311_AssetTemplates.md) * [RFC-0302: Validator Nodes](RFC-0302_ValidatorNodes.md) * [RFC-0341: Asset Registration](RFC-0341_AssetRegistration.md) ## Description ### Abstract -[Digital Asset]s (DAs) are managed by [committee]s of nodes called [Validator Node]s (VNs), as described in [RFC-0300](RFC-0300_DAN.md) and [RFC-0302](RFC-0302_ValidatorNodes.md). During the asset creation process, described in [RFC-0341](RFC-0341_AssetRegistration.md), the [Asset Issuer] (AI) MUST select a committee of VNs to manage their asset. This process consists of the following steps: +[Digital Asset]s (DAs) are managed by [committee]s of nodes called [Validator Node]s (VNs), as described in +[RFC-0300](RFC-0300_DAN.md) and [RFC-0302](RFC-0302_ValidatorNodes.md). During the asset creation process, described in +[RFC-0341](RFC-0341_AssetRegistration.md), the [Asset Issuer] (AI) MUST select a committee of VNs to manage their asset. +This process consists of the following steps: 1. Candidate VNs MUST be nominated to be considered for selection by the AI. 2. The AI MUST employ a [CommitteeSelectionStrategy] to select VNs from the set of nominated candidates. @@ -67,19 +71,39 @@ This document will describe the process an [Asset Issuer] (AI) will go through i 4. Selected VNs MAY accept the offer to become part of the committee by posting the required [AssetCollateral]. ### Nomination -The first step in assembling a committee is to nominate candidate VNs. As described in [RFC-0311](RFC-0311_AssetTemplates.md) an asset can be created with two possible `committee_modes`: `CREATOR_NOMINATION` or `PUBLIC_NOMINATION`. - -In `CREATOR_NOMINATION` mode the AI nominates candidate committee members directly. The AI will have a list of permissioned [Trusted Node]s that they want to act as the committee. The AI will contact the candidate VNs directly to inform them of their nomination. - -In `PUBLIC_NOMINATION` mode the AI does not have a list of [Trusted Node]s and wants to source unknown VNs from the network. In this case the AI broadcasts a public call for nomination to the Tari network using the peer-to-peer messaging protocol described in [RFC-0172](RFC-0172_PeerToPeerMessagingProtocol.md). This call for nomination contains all the details of the asset and VNs that want to participate will then nominate themselves by contacting the AI. +The first step in assembling a committee is to nominate candidate VNs. As described in +[RFC-0311](RFC-0311_AssetTemplates.md), an asset can be created with two possible `committee_modes` - `CREATOR_NOMINATION` +or `PUBLIC_NOMINATION`: + +- In `CREATOR_NOMINATION` mode, the AI nominates candidate committee members directly. The AI will have a list of permissioned + [Trusted Node]s that they want to act as the committee. The AI will contact the candidate VNs directly to inform them of + their nomination. +- In `PUBLIC_NOMINATION` mode, the AI does not have a list of [Trusted Node]s and wants to source unknown VNs from the + network. In this case, the AI broadcasts a public call for nomination to the Tari network using the peer-to-peer messaging + protocol described in [RFC-0172](RFC-0172_PeerToPeerMessagingProtocol.md). This call for nomination contains all the + details of the asset. VNs that want to participate will then nominate themselves by contacting the AI. ### Selection -Once the AI has received a list of nominated VNs it must make a selection, assuming enough VNs were nominated to populate the committee. The AI will employ some [CommitteeSelectionStrategy] in order to select the committee from the candidate VNs that have been nominated. This strategy might aim for a perfectly random selection or perhaps it will consider some metrics about the candidate VNs such as the length of their VN registrations which might indicate that they are reliable and have not been blacklisted for poor or malicious performance. - -A consideration when selecting a committee in `PUBLIC_NOMINATION` mode will be the size of the pool of nominated VNs. The size of this pool relative to the size of the committee to be selected will be linked to a risk profile. If the pool has very few candidates in it then it will be much easier for an attacker to have nominated their own nodes in order to obtain a majority membership of the committee i.e. if the AI is selecting a committee of 10 members using a uniformly random selection strategy and only 12 public nominations are received an attacker only requires control of 6 VNs to achieve a majority position in the committee. In contrast, if 100 nominations are received and the AI performs a uniformly random selection an attacked would need to control more than 50 of the nominated nodes in order to achieve a majority position in the committee. - -### Offer acceptance -Once the selection has been made by the AI the selected VNs will be informed and an offer of membership will be made to them. If the VNs are still inclined to join the committee they will accept the offer by posting the [AssetCollateral] required by the asset to the [base layer] during the initial [Checkpoint] transaction built to commence the operation of the asset. +Once the AI has received a list of nominated VNs, it must make a selection, assuming enough VNs were nominated to populate +the committee. The AI will employ some [CommitteeSelectionStrategy] in order to select the committee from the candidate +VNs that have been nominated. This strategy might aim for a perfectly random selection, or perhaps it will consider some +metrics about the candidate VNs, such as the length of their VN registrations. These metrics might indicate that they are reliable +and have not been blacklisted for poor or malicious performance. + +A consideration when selecting a committee in `PUBLIC_NOMINATION` mode will be the size of the pool of nominated VNs. +The size of this pool relative to the size of the committee to be selected will be linked to a risk profile. If the pool +contains very few candidates, then it will be much easier for an attacker to have nominated their own nodes in order to +obtain a majority membership of the committee. For example, if the AI is selecting a committee of 10 members using a uniformly +random selection strategy and only 12 public nominations are received, an attacker only requires control of six VNs to +achieve a majority position in the committee. In contrast, if 100 nominations are received and the AI performs a +uniformly random selection, an attacker would need to control more than 50 of the nominated nodes in order to achieve a +majority position in the committee. + +### Offer Acceptance +Once the selection has been made by the AI, the selected VNs will be informed and they will be made an offer of membership. +If the VNs are still inclined to join the committee, they will accept the offer by posting the [AssetCollateral] +required by the asset to the [base layer] during the initial [Checkpoint] transaction built to commence the operation of +the asset. [assetcollateral]: Glossary.md#assetcollateral [asset issuer]: Glossary.md#asset-issuer diff --git a/RFC/src/RFC-0310_AssetManagement.md b/RFC/src/RFC-0310_AssetManagement.md deleted file mode 100644 index 096265bdb4..0000000000 --- a/RFC/src/RFC-0310_AssetManagement.md +++ /dev/null @@ -1,5 +0,0 @@ -# RFC-0310/AssetMAnagement - -RFC placeholder. - -This section will describe the details of how Assets will be created and managed on the DAN by Asset Issuers. \ No newline at end of file diff --git a/RFC/src/RFC-0311_AssetTemplates.md b/RFC/src/RFC-0311_AssetTemplates.md index adb81ca649..d5130f7f4e 100644 --- a/RFC/src/RFC-0311_AssetTemplates.md +++ b/RFC/src/RFC-0311_AssetTemplates.md @@ -1,16 +1,16 @@ # RFC-0311/AssetTemplates -## Digital Asset templates +## Digital Asset Templates -![status: draft](https://github.com/tari-project/tari/raw/master/RFC/src/theme/images/status-draft.svg) +![status: draft](theme/images/status-draft.svg) **Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). -Copyright 2019. The Tari Development Community +Copyright 2019 The Tari Development Community Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -22,148 +22,149 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -Describe the Tari Digital Asset templating system for smart contract definition. +The aim of this Request for Comment (RFC) is to describe the Tari Digital Asset templating system for smart contract +definition. The term “smart contracts” in this document is used to refer to a set of rules enforced by computers. These smart -contracts are not Turing complete, such as those executed by the Ethereum VM. +contracts are not Turing complete, such as those executed by the Ethereum Virtual Machine (VM). -## Related RFCs +## Related Requests for Comment * [RFC-0300: The Digital Assets Network](RFC-0300_DAN.md) -* [RFC-0301: Namespace registration](RFC-0301_NamespaceRegistration.md) +* [RFC-0301: Namespace Registration](RFC-0301_NamespaceRegistration.md) * [RFC-0340: Validator Node Consensus](RFC-0340_VNConsensusOverview.md) -* [RFC-0374: Asset Creation and management](RFC-0374_TCAssetManagement.md) +* [RFC-0374: Asset Creation and Management](RFC-0374_TCAssetManagement.md) ## Description ### Motivation The reasons for issuing assets on Tari under a templating system, rather than a scripting language (whether Turing -complete or not) are manifold: +complete or not), are manifold: * A scripting language, irrespective of how simple it is, limits the target market for asset issuers to developers, or people who pay developers. * The market doesn’t want general smart contracts. This is evidenced by the fact that the vast majority of Ethereum transactions go through ERC-20 or ERC-721 contracts, which are literally contract templates. -* The attack surface for smart contracts is reduced considerably; to the node software itself. -* Bugs can be fixed for all contracts simultaneously by using a template versioning system. Existing assets can opt-in +* The attack surface for smart contracts is reduced considerably, to the node software itself. +* Bugs can be fixed for all contracts simultaneously by using a template versioning system. Existing assets can opt in to fixes by migrating assets to a new version of the contract. -* Contracts will have better QA since more eyes are looking at fewer contract code sets. -* Transmission, storage and processing of contracts will be more efficient as one only has to deal with the parameters +* Contracts will have better Quality Assurance (QA), since more eyes are looking at fewer contract code sets. +* Transmission, storage and processing of contracts will be more efficient, as one only has to deal with the parameters, and not the logic of the contract. Furthermore, the cost for users is usually lower, since there's no need to add - friction / extra costs to contract execution (e.g. ethereum gas) to work around the halting problem. + friction or extra costs to contract execution (e.g. Ethereum gas) to work around the halting problem. ### Implementation -Assets are created on the Tari network by issuing a `create_asset` instruction from a wallet or client and broadcasting +Assets are created on the Tari network by issuing a `create_asset` instruction from a wallet or client, and broadcasting it to the Tari Digital Assets Network (DAN). The instruction is in JSON format and MUST contain the following fields: -| Name | Type | Description | -|:-----------------------------------|:--------------|:---------------------------------------------------------------------------------------------------------------------------------------| -| **Asset description** | | | -| issuer | PubKey | The public key of the creator of the asset. See [issuer](#issuer) | -| name | `string[64]` | The name or identifier for the asset. See [Name and Description](#name-and-description) | -| description | `string[216]` | A short description of the asset - with name, fits in a tweet. See [Name and Description](#name-and-description) | -| raid_id | `string[15]` | The [Registered Asset Issuer Domain (RAID_ID)](#raid-id) for the asset. | -| fqdn | `string[*]` | The FQDN corresponding to the `raid_id`. Up to 255 characters in length; or "No_FQDN" to use the default. | -| public_nonce | PubKey | Public nonce part of the creator signature | -| template_id | `u64` | The template descriptor. See [Template ID](#template-id) | -| asset_expiry | `u64` | A timestamp or block height after which the asset will automatically expire. Zero for arbitrarily long-lived assets | -| **Validation Committee selection** | | | -| committee_mode | `u8` | The validation committee nomination mode, either `CREATOR_NOMINATION` (0) or `PUBLIC_NOMINATION` (1) | -| committee_parameters | Object | See [Committee Parameters](#committee-parameters). | -| asset_creation_fee | `u64` | The fee the issuer is paying, in microTari, for the asset creation process | -| commitment | `u256` | A time-locked commitment for the asset creation fee | -| initial_state_hash | `u256` | The hash of the canonical serialisation of the initial template state (of the template-specific data) | -| initial_state_length | `u64` | Size in bytes of initial state | -| **template-specific data** | Object | Template-specific metadata can be defined in this section | -| **Signatures** | | | -| metadata_hash | `u256` | A hash of the previous three sections' data, in canonical format (`m`) | -| creator_sig | `u256` | A digital signature of the message `H(R ‖ P ‖ RAID_ID ‖ m)` using the asset creator’s private key corresponding to the `issuer` PKH | -| commitment_sig | `u256` | A signature proving the issuer is able to spend the commitment to the asset fee | - -#### Committee parameters - -If `committee_mode` is `CREATOR_NOMINATION` the `committee_parameters` object is +| Name | Type | Description | +| ---------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Asset Description** | | | +| issuer | PubKey | The public key of the creator of the asset. Refer to [issuer](#issuer). | +| name | `string[64]` | The name or identifier for the asset. Refer to [Name and Description](#name-and-description). | +| description | `string[216]` | A short description of the asset - with name, fits in a tweet. Refer to [Name and Description](#name-and-description). | +| raid_id | `string[15]` | The [Registered Asset Issuer Domain (RAID_ID)](#raid-id) for the asset. | +| fqdn | `string[*]` | The Fully Qualified Domain Name (FQDN) corresponding to the `raid_id`. Up to 255 characters in length; or "No_FQDN" to use the default. | +| public_nonce | PubKey | Public nonce part of the creator signature. | +| template_id | `u64` | The template descriptor. Refer to [Template ID](#template-id). | +| asset_expiry | `u64` | A timestamp or block height after which the asset will automatically expire. Zero for arbitrarily long-lived assets. | +| **Validation Committee Selection** | | | +| committee_mode | `u8` | The validation committee nomination mode, either `CREATOR_NOMINATION` (0) or `PUBLIC_NOMINATION` (1). | +| committee_parameters | Object | Refer to [Committee Parameters](#committee-parameters). | +| asset_creation_fee | `u64` | The fee the issuer is paying, in microTari, for the asset creation process. | +| commitment | `u256` | A time-locked commitment for the asset creation fee. | +| initial_state_hash | `u256` | The hash of the canonical serialization of the initial template state (of the template-specific data). | +| initial_state_length | `u64` | Size in bytes of initial state. | +| **Template-specific Data** | Object | Template-specific metadata can be defined in this section. | +| **Signatures** | | | +| metadata_hash | `u256` | A hash of the previous three sections' data, in canonical format (`m`). | +| creator_sig | `u256` | A digital signature of the message `H(R ‖ P ‖ RAID_ID ‖ m)`, using the asset creator’s private key corresponding to the `issuer` Public Key Hash (PKH). | +| commitment_sig | `u256` | A signature proving the issuer is able to spend the commitment to the asset fee. | + + +#### Committee Parameters + +If `committee_mode` is `CREATOR_NOMINATION`, the `committee_parameters` object is: | Name | Type | Description | -|:-----------------|:-------------|:------------| -| trusted_node_set | Array of PKH | See below | +| :--------------- | :----------- | :---------- | +| trusted_node_set | Array of PKH | See below. | Only the nodes in the trusted node set will be allowed to execute instructions for this asset. -If `committee_mode` is `PUBLIC_NOMINATION` the `committee_parameters` object is +If `committee_mode` is `PUBLIC_NOMINATION`, the `committee_parameters` object is: | Name | Type | Description | |:------------------------|:------|:----------------------------------------------------------------------------------------------------------------------| -| node_threshold | `u32` | The required number of validator nodes that must register to execute instructions for this asset | -| minimum_collateral | `u64` | The minimum amount of Tari a validator node must put up in collateral in order to execute instructions for this asset | -| node_selection_strategy | `u32` | The selection strategy to employ allowing nodes to register to manage this asset | +| node_threshold | `u32` | The required number of Validator Nodes (VNs) that must register to execute instructions for this asset. | +| minimum_collateral | `u64` | The minimum amount of Tari a VN must put up in collateral in order to execute instructions for this asset. | +| node_selection_strategy | `u32` | The selection strategy to employ allowing nodes to register to manage this asset. | #### Issuer -Anyone can create new assets on the Tari network from their Tari Collections client. The client will provide the PKH and +Anyone can create new assets on the Tari network from their Tari Collections client. The client will provide the Public Key Hash (PKH) and sign the instruction. The client needn’t use the same private key each time. #### Name and Description -These fields are purely for informational purposes and do not need to be unique, and do not act as an asset ID. +These fields are purely for information purposes. They do not need to be unique and do not act as an asset ID. #### RAID ID -The RAID_ID is a 15 character string that associates the asset issuer with a registered internet domain name on DNS. +The RAID_ID is a 15-character string that associates the asset issuer with a registered Internet domain name on the Domain Name System (DNS). -If it is likely that a digital asset issuer will be issuing many assets on the Tari Network (hundreds, or thousands), +If it is likely that a digital asset issuer will be issuing many assets on the Tari Network (hundreds or thousands), the issuer should strongly consider using a registered domain (e.g. `acme.com`). This is -done via OpenAlias on the domain owner's DNS record, as described in [RFC-0301]. A - RAID prevents spoofing of assets from copycats or other malicious actors. It also makes asset discovery -simpler. +done via OpenAlias on the domain owner's DNS record, as described in [RFC-0301]. A RAID prevents spoofing of assets from +copycats or other malicious actors. It also simplifies asset discovery. Assets from issuers that do not have a RAID are all grouped under the default RAID. RAID owners must provide a valid signature proving that they own the given domain when creating assets. -#### FQDN +#### Fully Qualified Domain Name -The fully qualified domain name that corresponds to the `raid_id` or the string `"NO FQDN"` to use the default RAID ID. -Validator nodes will calculate and check that the RAID ID is valid when -[validating the instruction signature](signature-validation). +The Fully Qualified Domain Name (FQDN) that corresponds to the `raid_id` or the string `"NO FQDN"` to use the default RAID ID. +Validator Nodes (VNs) will calculate and check that the RAID ID is valid when +[validating the instruction signature](#signature-validation). -#### Public nonce +#### Public Nonce A single-use public nonce to be used in the asset signature. -#### Asset identification +#### Asset Identification Assets are identified by a 64-character string that uniquely identifies an asset on the network: @@ -177,70 +178,70 @@ Assets are identified by a 64-character string that uniquely identifies an asset | 32 | Hex representation of the `metadata_hash` field | This allows assets to be deterministically identified from their initial state. Two different creation instructions -leading to the same hash refer to the same single asset, by definition. Validator Nodes maintain an index of assets and +leading to the same hash refer to the same single asset, by definition. VNs maintain an index of assets and their committees, and so can determine whether a given asset already exists; and MUST reject any `create_asset` instruction for an existing asset. #### Template ID -Tari uses templates to define the behaviour for its smart contracts. The template id refers to the type of digital asset -begin created. +Tari uses templates to define the behaviour for its smart contracts. The template ID refers to the type of digital asset +being created. -**Note:** Integer values are given in _little-endian_ format; i.e. the least significant bit is _first_. +**Note:** Integer values are given in _little-endian_ format, i.e. the least significant bit is _first_. The template number is a 64-bit unsigned integer and has the following format, with 0 representing -the least significant bit. +the least significant bit: -| Bit range | Description | -|:----------|:----------------------------------| +| Bit Range | Description | +| :-------- | :-------------------------------- | | 0 - 31 | Template type (0 - 4,294,967,295) | | 32 - 47 | Template version (0 - 65,535) | | 48 | Beta Mode flag | | 49 | Confidentiality flag | -| 50 - 63 | Reserved (Must be 0) | +| 50 - 63 | Reserved (must be 0) | -The lowest 32 bits refer to the canonical smart contract type; the qualitative types of contracts the network supports. +The lowest 32 bits refer to the canonical smart contract type, i.e. the qualitative types of contracts the network supports. Many assets can be issued from a single template. Template types below 65,536 (216) are public, community-developed templates. -All validator nodes MUST implement and be able to interpret instructions related to these templates. +All VNs MUST implement and be able to interpret instructions related to these templates. -Template types 65,536 and above are opt-in or proprietary templates. There is no guarantee that any given validator node +Template types 65,536 and above are opt-in or proprietary templates. There is no guarantee that any given VN will be able to manage assets on these templates. Part of the committee selection and confirmation process for new -assets will be an attestation by validator nodes that they are willing and able to manage the asset under the designated +assets will be an attestation by VNs that they are willing and able to manage the asset under the designated template rules. A global registry of opt-in template types will be necessary to prevent collisions (public templates existence will be -evident from the Validator Node source code), possibly implemented as a special transaction type on the base layer; the -base layer being perfectly suited for hosting such a registry. The details of this will be covered in a separate +evident from the Validator Node source code), possibly implemented as a special transaction type on the base layer, +which is perfectly suited for hosting such a registry. The details of this will be covered in a separate proposal. -Examples of template types may be +Examples of template types may be: -| Template type | Asset | -|:--------------|:-------------------------| +| Template Type | Asset | +| :------------ | :----------------------- | | 1 | Simple single-use tokens | | 2 | Simple coupons | | 20 | ERC-20-compatible | | ... | ... | -| 120 | Collectible Cards | +| 120 | Collectible cards | | 144 | In-game items | | 721 | ERC-721-compatible | | ... | ... | | 65,537 | Acme In game items | | 723,342 | CryptoKitties v8 | -The template id may also set one or more feature flags to indicate that the contract is -* experimental, or in testing phase, (bit 48). -* confidential. The definition of confidential assets and their implementation is not finalised at the time of writing. +The template ID may also set one or more feature flags to indicate that the contract is: +* Experimental, or in testing phase (bit 48). +* Confidential. The definition of confidential assets and their implementation had not been finalized at the time of writing. -Wallets / client apps SHOULD have settings to allow or otherwise completely ignore asset types on the network that have +Wallets/client apps SHOULD have settings to allow, or otherwise completely ignore, asset types on the network that have certain feature flags enabled. For instance, most consumer wallets should never interact with templates that have the -“Beta mode” bit set. Only developer’s wallets should ever even see that such assets exist. +“Beta mode” bit set. Only developers' wallets should ever even see that such assets exist. -#### Asset expiry +#### Asset Expiry -Asset issuers can set a future expiry date or block height after which the asset will expire and nodes will be free to +Asset issuers can set a future expiry date or block height, after which the asset will expire and nodes will be free to expunge any/all state relating to the asset from memory after a fixed grace period. The grace period is to allow interested parties (e.g. the issuer) to take a snapshot of the final state of the contract if they wish (e.g. proving that you had a ticket for that epic World Cup final game, even after the asset no longer exists on the DAN). @@ -251,41 +252,44 @@ The expiry_date is a Unix epoch, representing the number of seconds since 1 Janu greater than 1,500,000,000; or a block height if it is less than that value (with 1 min blocks this scheme is valid until the year 4870). -Expiry times should not be considered exact, since nodes don’t share the same clocks and block heights as time proxies +Expiry times should not be considered exact, since nodes don’t share the same clocks and block heights, and time proxies become more inaccurate the further out you go (since height in the future is dependent on hash rate). -### Signature validation +### Signature Validation Validator nodes will verify the `creator_sig` for every `create_asset` instruction before propagating the instruction to -the network. This involves the following process: +the network. The process is as follows: 1. The VN MUST calculate the metadata hash by hashing the canonical representation of all the data in the first three sections of the `create_asset` instruction. -2. The VN MUST compare this calculated value to the value given in the `metadata_hash` field. If they do not match, drop +2. The VN MUST compare this calculated value to the value given in the `metadata_hash` field. If they do not match, the +VN MUST drop the instruction and STOP. 3. The VN MUST calculate the RAID ID from the `fqdn` and `issuer` fields as specified in [RFC-0301]. -4. The VN MUST compare the calculated RAID ID with the value given in the `raid_id` field. If they do not match, drop +4. The VN MUST compare the calculated RAID ID with the value given in the `raid_id` field. If they do not match, the VN +MUST drop the instruction and STOP. 5. If the `fqdn` is `"No FQDN", then skip to step 9. -6. The VN MUST Look up the OpenAlias TXT record at the domain given in `fqdn`. If the record is does not exist, then +6. The VN MUST Look up the OpenAlias TXT record at the domain given in `fqdn`. If the record does not exist, then the +VN MUST drop the instruction and STOP. 7. The VN MUST check that each of the public key and RAID ID in the TXT record match the values in the `create_asset` - instruction. If any values do not match, then drop the instruction and STOP. + instruction. If any values do not match, the VN MUST then drop the instruction and STOP. 8. The VN MUST validate the registration signature in the TXT record, using the TXT record's nonce, the issuer's public - key and the RAID ID. If the signature does not verify, drop the instruction and STOP. + key and the RAID ID. If the signature does not verify, the VN MUST drop the instruction and STOP. 9. The VN MUST validate the signature in the `creator_sig` field against the challenge built up from the issuer's public key, the nonce given in `public_nonce` field, the `raid_id` field and the `metadata_hash` field. If step 9 passes, then the VN has proven that the `create_asset` contains a valid RAID ID, and that if a non-default -FQDN was provided, that the owner of that domain provided the `create_asset` instruction. In this case, the VN SHOULD +FQDN was provided, the owner of that domain provided the `create_asset` instruction. In this case, the VN SHOULD propagate the instruction to the network. [RFC-0301]: RFC-0301_NamespaceRegistration.md diff --git a/RFC/src/RFC-0313_AssetReadAPI.md b/RFC/src/RFC-0313_AssetReadAPI.md deleted file mode 100644 index 539f15b77a..0000000000 --- a/RFC/src/RFC-0313_AssetReadAPI.md +++ /dev/null @@ -1,6 +0,0 @@ -# RFC-0313/AssetReadAPI - -RFC placeholder. - -This document will describe the read-only API for gaining basic information about public digital assets; and how -information about hidden/private assets is provided. \ No newline at end of file diff --git a/RFC/src/RFC-0314_AssetDiscovery.md b/RFC/src/RFC-0314_AssetDiscovery.md deleted file mode 100644 index 50b322c12b..0000000000 --- a/RFC/src/RFC-0314_AssetDiscovery.md +++ /dev/null @@ -1,6 +0,0 @@ -# RFC-0314/AssetDiscovery - -RFC placeholder. - -This document will describe how clients can discover which assets are being managed on the network and the basic ways of -interacting with them. \ No newline at end of file diff --git a/RFC/src/RFC-0315_AssetTransfer.md b/RFC/src/RFC-0315_AssetTransfer.md deleted file mode 100644 index 36113d50c8..0000000000 --- a/RFC/src/RFC-0315_AssetTransfer.md +++ /dev/null @@ -1,5 +0,0 @@ -# RFC-0315/AssetTransfer - -RFC placeholder. - -This document will describe the process for third-party asset transfer; i.e. Collections <-> Collections transfer \ No newline at end of file diff --git a/RFC/src/RFC-0322_VNRegistration.md b/RFC/src/RFC-0322_VNRegistration.md index f267a80573..562cfeb9c0 100644 --- a/RFC/src/RFC-0322_VNRegistration.md +++ b/RFC/src/RFC-0322_VNRegistration.md @@ -6,11 +6,11 @@ **Maintainer(s)**: [Cayle Sharrock](https://github.com/CjS77) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). -Copyright 2019. The Tari Development Community +Copyright 2019 The Tari Development Community Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -22,70 +22,71 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in -[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in +[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document describes Validator Node registration. Registration accomplishes two goals: +The aim of this Request for Comment (RFC) is to describe Validator Node (VN) registration. Registration accomplishes two goals: -1. Provide a register of Validator Nodes with an authority (the Tari base layer). -2. Offer Sybil resistance against gaining probabilistic majority of any given publicly nominated VN committee. +1. Provides a register of Validator Nodes with an authority (the Tari base layer). +2. Offers Sybil resistance against gaining probabilistic majority of any given publicly nominated VN committee. -## Related RFCs +## Related Requests for Comment * [RFC-0302: Validator Nodes](RFC-0302_ValidatorNodes.md) -* [RFC-0304: Validator Node committee selection](RFC-0304_VNCommittees.md) +* [RFC-0304: Validator Node Committee Selection](RFC-0304_VNCommittees.md) * [RFC-0170: The Tari Communication Network and Network Communication Protocol](RFC-0170_NetworkCommunicationProtocol.md) * [RFC-0341: Asset Registration](RFC-0341_AssetRegistration.md) * [RFC-0200: Base Layer Extensions](BaseLayerExtensions.md) ## Description -Validator Nodes register themselves on the [Base layer] using a special [transaction] type. The registration +VNs register themselves on the [Base layer] using a special [transaction] type. The registration [transaction] type requires the spending of a certain minimum amount of [Tari coin], the ([Registration Deposit]), -that has a time-lock on the output for a minimum amount of time ([Registration Term]) as well as some metadata, such as -the VNs public key. +which has a time lock on the output for a minimum amount of time ([Registration Term]), as well as some metadata, such as +the VN's public key. The Node ID is calculated after registration to prevent mining of VN public keys that can be used to manipulate routing -on the DHT. +on the Distributed Hash Table (DHT). -Once a VNs [Registration Term] has expired so will this specific VN registration. The UTXO timelock will have elapsed so -the [Registration Deposit] can be reclaimed and a new VN registration need to be performed. This automatic -registration expiry will ensure that the VN registry stays up to date with active VN registrations and inactive +Once a VN's [Registration Term] has expired, so will this specific VN registration. The Unspent Transaction Output (UTXO) +time lock will have elapsed so +the [Registration Deposit] can be reclaimed and a new VN registration needs to be performed. This automatic +registration expiry will ensure that the VN registry stays up to date with active VN registrations, and inactive registrations will naturally be removed. Requiring nodes to register themselves serves two purposes: -* Makes VN Sybil attacks expensive, -* Provides an authoritative "central-but-not-centralised" registry of validator nodes from the base layer. +* makes VN Sybil attacks expensive; and +* provides an authoritative "central-but-not-centralized" registry of VNs from the base layer. ### Node ID -The Validator node ID can be calculated deterministically after the VN registration transaction is mined. This ensures +The VN ID can be calculated deterministically after the VN registration transaction is mined. This ensures that VNs are randomly distributed over the DHT network. -VN Node IDs MUST be calculated as follows: +VN IDs MUST be calculated as follows: ```text NodeId = Hash( pk || h || kh ) @@ -95,17 +96,18 @@ Where | Field | Description | |:------|:------------------------------------------------------------------------------| -| pk | The Validator Node's DHT public key | +| pk | The VN's DHT public key | | h | The block height of the block in which the registration transaction was mined | | kh | The hash of the registration transaction's kernel | -Base Nodes SHOULD maintain a cached list of Validator Nodes and MUST return the Node ID in response to a +Base Nodes SHOULD maintain a cached list of VNs and MUST return the Node ID in response to a `get_validator_node_id` request. -### Validator node registration +### Validator Node Registration -A validator node MUST register on the base layer before it can join any DAN [committee]s. Registration happens by virtue -of a Validator Node Registration [transaction]. +A VN MUST register on the base layer before it can join any Distributed Area Network (DAN) [committee]s. Registration +happens by virtue +of a VN registration [transaction]. VN registrations are valid for the [Registration Term]. @@ -115,48 +117,48 @@ A VN registration transaction is a special transaction. * The transaction MUST have EXACTLY ONE UTXO with the `VN_Deposit` flag set. * This UTXO MUST also: - * Set a time lock for AT LEAST the [Registration Term] (or equivalent block periods) - * Provide the _value_ of the UTXO in the signature metadata - * Provide the _public key_ for the spending key for the output in the signature metadata -* The value of this output MUST be equal or greater than the [Registration Deposit]. + * set a time lock for AT LEAST the [Registration Term] (or equivalent block periods); + * provide the _value_ of the UTXO in the signature metadata; and + * provide the _public key_ for the spending key for the output in the signature metadata. +* The value of this output MUST be equal to or greater than the [Registration Deposit]. * The UTXO MUST store: - * The value of the VN deposit UTXO as a u64. - * The value of the _public key_ for the spending key for the output as 32 bytes in little endian order. + * the value of the VN deposit UTXO as a u64; and + * the value of the _public key_ for the spending key for the output as 32 bytes in little-endian order. * The `KernelFeatures` bit flag MUST have the `VN_Registration` flag set. -* The kernel MUST also store the VN's DHT public key as 32 bytes in little endian order. +* The kernel MUST also store the VN's DHT public key as 32 bytes in little-endian order. -### Validator node registration renewal +### Validator Node Registration Renewal If a VN owner does not _renew_ the registration before the [Registration Term] has expired, the registration will lapse and the VN will no longer be allowed to participate in any [committee]s. The number of consecutive renewals MAY increase the VN's reputation score. -A VN may only renew a registration in the TWO WEEK period prior to the current term expiring. +A VN may only renew a registration in the TWO-WEEK period prior to the current term expiring. A VN renewal transaction is a special transaction: * The transaction MUST have EXACTLY ONE UTXO with the `VN_Deposit` flag set. -* The transaction MUST spend the previous VN Deposit UTXO for this VN. +* The transaction MUST spend the previous VN deposit UTXO for this VN. * This UTXO MUST also: - * Set a time lock for AT LEAST 6 months (or equivalent block periods) - * Provide the _value_ of the transaction in the signature metadata - * Provide the _public key_ for the spending key for the output in the signature metadata -* This UTXO MUST also store + * set a time lock for AT LEAST six months (or equivalent block periods); + * provide the _value_ of the transaction in the signature metadata; and + * provide the _public key_ for the spending key for the output in the signature metadata. +* This UTXO MUST also store: * The value of the VN deposit UTXO as a u64. - * The value of the _public key_ for the spending key for the output as 32 bytes in little endian order. + * The value of the _public key_ for the spending key for the output as 32 bytes in little-endian order; * The VN's Node ID. This can be validated by following the Renewal transaction kernel chain. * The kernel hash of this transaction's kernel. * A counter indicating that this is the n-th consecutive renewal. This counter will be confirmed by nodes and miners. - The first renewal will have counter value of one. + The first renewal will have a counter value of one. * The previous VN deposit UTXO MUST NOT be spendable in a standard transaction (i.e. its time lock has not expired). * The previous VN deposit UTXO MUST expire within the next TWO WEEKS. * The transaction MAY provide additional inputs to cover transaction fees and increases in the [Registration Deposit]. * The transaction kernel MUST have the `VN_Renewal` bit flag set. -* The transaction kernel MUST also store - * The hash of the previous renewal transaction kernel, or the registration kernel if this is the first renewal. +* The transaction kernel MUST also store the hash of the previous renewal transaction kernel, or the registration kernel, +if this is the first renewal. -One will notice that a validator node's Node ID does not change as a result of a renewal transaction. Rather, every +One will notice that a VN's Node ID does not change as a result of a renewal transaction. Rather, every renewal adds to a chain linking back to the original registration transaction. It may be desirable to establish a long chain of renewals, in order to offer evidence of longevity and improve a VN's reputation. diff --git a/RFC/src/RFC-0340_VNConsensusOverview.md b/RFC/src/RFC-0340_VNConsensusOverview.md index f5cb37c8c6..8ddd08f4aa 100644 --- a/RFC/src/RFC-0340_VNConsensusOverview.md +++ b/RFC/src/RFC-0340_VNConsensusOverview.md @@ -1,6 +1,249 @@ # RFC-0340/VNConsensusOverview -RFC placeholder. +## Validator node consensus algorithm -This document will describe the high-level overview of how VN committees reach consensus about the current state of an -asset after each instruction is executed. \ No newline at end of file +![status: draft](theme/images/status-draft.svg) + +**Maintainer(s)**: Cayle Sharrock + +# License + +[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). + +Copyright 2019. The Tari Development Community + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: + +1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following + disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## Language + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as +shown here. + +## Disclaimer + +The purpose of this document and its content is for information purposes only and may be subject to change or update +without notice. + +This document may include preliminary concepts that may or may not be in the process of being developed by the Tari +community. The release of this document is intended solely for review and discussion by the community regarding the +technological merits of the potential system outlined herein. + +## Goals + +This document describes at a high level how smart contract state is managed on the Tari Digital Assets Network. + +## Related RFCs + +* [RFC-0300: The Tari Digital Assets Network](RFC-0300_DAN.md) +* [RFC-0302: Validator Nodes](RFC-0302_ValidatorNodes.md) +* [RFC-0304: Validator Node committee selection](RFC-0304_VNCommittees.md) +* [RFC-0341: Asset Registration](RFC-0341_AssetRegistration.md) + +## Description + +### Overview + +The primary problem under consideration here is for multiple machines running the same program (in the form of a Tari +smart contract) to maintain agreement on what the state of the program is, often under adverse conditions, including +unreliable network communication, malicious third parties, or even malicious peers running the smart contract. + +In computer science terms, the problem is referred to as +[State Machine Replication](https://en.wikipedia.org/wiki/State_machine_replication), or SMR. If we want our honest +machines (referred to as _replicas_ in SMR parlance) to reach agreement in the face of arbitrary failures, then we talk +about our system being +[Byzantine Fault Tolerant](https://tlu.tarilabs.com/consensus-mechanisms/BFT-consensusmechanisms/sources/PITCHME.link.html). + +Tari Asset [committees] are chosen by the asset issuer according to [RFC-0304](RFC-0304_VNCommittees.md). The committees +form a fixed set of replicas, at the very least from checkpoint to checkpoint, and will typically be limited in size, +usually less than ten, and almost always under 100. _Note_: These numbers are highly speculative based on an intuitive +guess about the main use cases for Tari DAs, where we have +* many 1-3-sized committees where the asset issuer and the VN committee are the same entity, +* semi-decentralised assets of ±4-10 where speed trumps censorship-resistance, +* a small number of 50-100 VNs where censorship-resistance trumps speed. + +Because nodes cannot join and leave the committees at will, robust yet slow and expensive consensus approaches such as +Nakamoto consensus can be dropped in favour of something more performant. + +There is a good survey of consensus mechanisms on +[Tari Labs University](https://tlu.tarilabs.com/consensus-mechanisms/consensus-mechanisms.html). + +From the point of view of a DAN committee, the ideal consensus algorithm is one that +1. Allows a high number of transactions per second, and doesn't have unnecessary pauses (i.e. a partially synchronous or + asynchronous model). +2. Is Byzantine Fault tolerant. +3. Is relatively efficient from a network communication point of view (number of messages passed per state agreement). +4. Is relatively simple to implement (to reduce the bug and vulnerability surface in implementations). + +A summary of some of the most well-known BFT algorithms is presented in +[this table](https://tlu.tarilabs.com/consensus-mechanisms/BFT-consensus-mechanisms-applications/MainReport.html#summary-of-findings). + +A close reading of the algorithms presented suggest that [LinBFT](https://arxiv.org/pdf/1807.01829.pdf), which is based +on [HotStuff] BFT provide the best trade-offs for the goals that a DAN committee is trying to achieve: +1. The algorithm is optimistic, i.e. as soon as quorum is reached on a particular state, the committee can move onto the + next one. There is no need to wait for the "timeout" period as we do in e.g. Tendermint. This allows instructions to + be executed almost as quickly as they are received. +2. The algorithm is efficient in communication, requiring O(n) messages per state agreement in most practical cases. + This is compared to e.g. PBFT which requires O(n4) messages. +3. The algorithm is modular and relatively simple to implement. + +Potential drawbacks to using HotStuff include: +1. Each round required the election of a _leader_. Having a leader dramatically simplifies the consensus algorithm; it + allows a linear number of messages to be sent between the leader and the other replicas in order to agree on the + current state; and it allows a strict ordering to be established on instructions without having to resort to e.g. + proof of work. However, if the choice of leader is deterministic, attackers can identify and potentially DDOS the + leader for a given round, causing the algorithm to time out. There are ways to mitigate this attack for a _specific + round_, as suggested in the LinBFT paper, such as using Verifiable Random Functions, but DDOSing a single replica + means that, on average, the algorithm will time out every 1/n rounds. +2. The attack described above only pauses progress in Hotstuff for the timeout period. In similar protocols, e.g. + Tendermint it can be shown to [delay progress indefinitely](https://arxiv.org/pdf/1803.05069). + +Given these trade-offs, there is strong evidence to suggest that [HotStuff] BFT, when implemented on the Tari DAN will +provide BFT security guarantees with liveness performance in the sub-second scale and throughput on the order of +thousands of instructions per second, if the benchmarks presented in the [HotStuff] paper are representative. + +### Implementation + +The [HotStuff] BFT algorithm provides a detailed description of the consensus algorithm. Only a summary of it is +presented here. To reduce confusion, we adopt the HotStuff nomenclature to describe state changes, rounds and function +names where appropriate. + +Every proposed state change, as a result of replicas receiving instructions from clients is called a _view_. There is a +[function that every node can call](#leader-selection) that will tell it which replica will be the _leader_ for a given +view. Every view goes through three phases (`Prepare`, `PreCommit`, `Commit`) before final consensus is reached. Once a +view reaches the `Commit` phase, it is finalised and will never be reversed. + +As part of their normal operation, every replica broadcasts [instructions] it receives for its contract to its peers. +These instructions are stored in a replica's instruction mempool. + +When the [leader selection](#leader-selection) function designates a replica as leader for the next view, it will try +and execute _all_ the instructions it currently has in its mempool to update the state for the next view. Following this +it compiles a tuple of <_valid-instructions_, _rejected-instructions_, _new-state_>. This tuple represents the `CMD` +structure described in [HotStuff]. + +In parallel with this, the leader expects a set of `NewView` messages from the other replicas, indicating that the other +replicas know that this replica is the leader for the next view. + +Once a super-majority of these messages have been received, the leader composes a proposal for the next state by adding +a new node to the state history graph (I'm calling it a state history graph to avoid naming confusion, but it's really a +blockchain). It composes a message containing the new proposal, and broadcasts it to the other replicas. + +Replicas, on receipt of the proposal, decide whether the proposal is valid, both from a protocol point of view (i.e. did +the leader provide a well-formed proposal) as well as whether they agree on the new state (e.g. by executing the +instructions as given and comparing the resulting state with that of the proposal). If there is agreement, they vote on +the proposal by signing it, and sending their partial signature back to the leader. + +When the leader has received a super-majority of votes, it sends a message back to the replicas with the (aggregated) +set of signatures. + +Replicas can validate this signature and provide another partial signature indicating that they've received the first +aggregated signature for the proposal. + +At this point, all replicas know that enough other replicas have received the proposal and are in agreement that it is +valid. + +In Tendermint, replicas would now wait for the timeout period to make sure that the proposal wasn't going to be +superseded before finalising the proposal. But there is an attack described in the [HotStuff] paper that could stall +progress at this point. + +The HotStuff protocol prevents this by having a final round of confirmations with the leader. This prevents the stalling +attack and _also_ lets replicas finalise the proposal _immediately_ on receipt of the final confirmation from the +leader. This lets HotStuff proceed at "network" speed, rather than with a heartbeat dictated by the BFT synchronicity +parameter. + +Although there are 4 round trips of communication between replicas and the leader, the number of messages sent are O(n). +It's also possible to stagger and layer these rounds on top of each other, so that there are always four voting rounds +happening simultaneously, rather than waiting for one to complete in its entirety before moving onto the next one. +Further details are given in the [HotStuff] paper. + +#### Forks and byzantine failures + +The summary of the HotStuff protocol given above describes the "Happy Path", when there are no breakdowns in +communication, or when the leader is an honest node. In cases where the leader is unavailable, the protocol will time +out, the current view will be abandoned, and all replicas will move onto the next view. + +If a leader is not honest, replicas will reject its proposal, and move onto the next view. + +If there is a temporary network partition, the chain may fork (up to a depth of three), but the protocol guarantees +safety via the voting mechanism, and the chain will reconcile once the partition resolves. + +#### Leader selection + +[HotStuff] leaves the leader selection algorithm to the application. Usually, a round-robin approach is suggested for +its simplicity. However, this requires the replicas to _reliably_ self-order themselves before starting with SMR, which +is a non-trivial exercise in byzantine conditions. + +For Tari DAN committees, the following algorithm is proposed: +1. Every replica knows the [Node ID] of every other replica in the committee. +2. For a given _view number_, the Node ID with the closest XOR distance to the hash of the _view number_ will be the + leader for that view, where the hash function provides a uniformly random value of the same length as the Node ID. + + +#### Quorum Certificate + +A Quorum certificate, or QC is proof that a super-majority of replicas have agreed on a given state. In particular, a QC +consists of +* The type of QC (depending on the phase in which the HotStuff pipeline the QC was signed), +* The _view number_ for the QC +* A reference to the node in the state tree being ratified, +* A signature from a super-majority of replicas. + +### Tari-specific considerations + +As soon as a state is finalised, replicas can inform clients as to the result of instructions they have submitted (in +the affirmative or negative). Given that HotStuff proceeds optimistically, and finalisation happens after 4 rounds of +communication, it's anticipated that clients can receive a final response from the validator committee in under 500 ms +for reasonably-sized committees (this value is speculation at present and will be updated once exploratory experiments +have been carried out). + +The Tari communication platform was designed to handle peer-to-peer messaging of the type described in [HotStuff], and +therefore the protocol implementation should be relatively straightforward. + +The "state" agreed upon by the VN committee will not only include the smart-contract state, but instruction fee +allocations and periodic checkpoints onto the base layer. + +Checkpoints onto the base layer achieve several goals: +* Offers a proof-of-work backstop against "evil committees". Without proof of work, there's nothing stopping an evil + committee (one that controls a super-majority of replicas) from rewriting history. Proof-of-work is the only reliable + and practical method that currently exists to make it expensive to change the history of a chain of records. Tari + gives us a "best of both worlds" scenario wherein an evil committee would have to rewrite the base layer history + (which _does_ use proof-of-work) before they could rewrite the digital asset history (which does not). +* They allow the asset issuer to authorise changes in the VN committee replica set. +* It allows asset owners to have an immutable proof of asset ownership long after the VN committee has dissolved after + the useful end-of-life of a smart contract. +* Provides a means for an asset issuer to resurrect a smart contract long after the original contract has terminated. + +When Validator Nodes run smart contracts, they should be run in a separate thread so that if a smart contract crashes, +it does not bring the consensus algorithm down with it. + +Furthermore, VNs should be able to quickly revert state to at least four views back in order to handle temporary forks. +Nodes should also be able to initialise/resume a smart contract (e.g. from a crash) given a state, view number, and view +history. + +This implies that VNs, in addition to passing around HotStuff BFT messages, will expose additional APIs in order to +* allow lagging replicas to catch up in the execution state. +* Provide information to (authorised) clients regarding the most recent finalised state of the smart contract via a + read-only API. +* Accept smart-contract instructions from clients and forward these onto the other replicas in the VN committee. + +[committees]: Glossary.md#committee +[Node ID]: Glossary.md#node-id +[instructions]: Glossary.md#instructions +[HotStuff]: https://arxiv.org/pdf/1803.05069 "Hotstuff BFT" \ No newline at end of file diff --git a/RFC/src/RFC-0341_AssetRegistration.md b/RFC/src/RFC-0341_AssetRegistration.md index 4a3c5d6476..2d8f0d1af5 100644 --- a/RFC/src/RFC-0341_AssetRegistration.md +++ b/RFC/src/RFC-0341_AssetRegistration.md @@ -1,15 +1,15 @@ -# RFC-0341: Asset registration -## Asset registration process +# RFC-0341: Asset Registration +## Asset Registration Process ![status: draft](theme/images/status-draft.svg) -**Maintainer(s)**: Philip Robinson +**Maintainer(s)**: [Philip Robinson](https://github.com/philipr-za) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). -Copyright 2019. The Tari Development Community +Copyright 2019 The Tari Development Community Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -21,79 +21,103 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in -[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in +[BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -This document will describe the process that an [Asset Issuer] will need to engage in to register a [digital asset] and commence its operation on the [digital asset network]. +The aim of this Request for Comment (RFC) is to describe the process in which an [Asset Issuer] (AI) will need to engage to +register a [Digital Asset] (DA) and commence its operation on the [Digital Asset Network] (DAN). -## Related RFCs -* [RFC-0311: Digital Asset templates](RFC-0311_AssetTemplates.md) +## Related Requests for Comment +* [RFC-0311: Digital Asset Templates](RFC-0311_AssetTemplates.md) * [RFC-0302: Validator Nodes](RFC-0302_ValidatorNodes.md) -* [RFC-0304: Validator Node committee selection](RFC-0304_VNCommittees.md) +* [RFC-0304: Validator Node Committee Selection](RFC-0304_VNCommittees.md) * [RFC-0220: Asset Checkpoints](RFC-0220_AssetCheckpoints.md) ## Description ### Abstract -This document will describe the process an [Asset Issuer] (AI) will go through in order to: -- Register a [digital asset] (DA) on the [base layer], -- assemble a [committee] of [Validator Node]s (VNs) and -- commence operation of the (DA) on the [Digital Asset Network] (DAN). +This document will describe the process through which an AI will go in order to: +- register a DA on the [base layer]; +- assemble a [committee] of [Validator Node]s (VNs); and +- commence operation of the DA on the DAN. -### Asset creation instruction -The first step in registering and commencing the operation of an asset is that the AI MUST issue an asset creation transaction to the [base layer]. +### Asset Creation Instruction +The first step in registering and commencing the operation of an asset is that the AI MUST issue an asset creation +transaction to the [base layer]. -This transaction will be time-locked for the length of the desired nomination period. This ensures that this transaction cannot be spent until the nomination period has elapsed so that it is present during the entire nomination process. The value of the transaction will be the `asset_creation_fee` described in [RFC-0311](RFC-0311_AssetTemplates.md). The AI will spend the transaction back to themselves but locking this fee up at this stage achieves 2 goals. Firstly, it makes it expensive to spam the network with asset creation transactions that a malicious AI does not intend to complete. Secondly, it proves to the VNs that participate in the nomination process that the AI does indeed have the funds required to commence operation of the asset once the committee has been selected. If the asset registration process fails, for example if there are not enough available VNs for the committee, then the AI can refund the fee to themselves after the time-lock expires. +This transaction will be time-locked for the length of the desired nomination period. This ensures that this transaction +cannot be spent until the nomination period has elapsed so that it is present during the entire nomination process. The +value of the transaction will be the `asset_creation_fee` described in [RFC-0311](RFC-0311_AssetTemplates.md). The AI +will spend the transaction back to themselves, but locking this fee up at this stage achieves two goals: -The transaction will contain the following extra metadata to facilitate the registration process: +- Firstly, it makes + it expensive to spam the network with asset creation transactions that a malicious AI does not intend to complete. + +- Secondly, it proves to the VNs that participate in the nomination process that the AI does indeed have the funds + required to commence operation of the asset once the committee has been selected. -1. The value of the transaction in clear text and the public spending key of the commitment so that it can be verified by third parties. A third party can verify the value of the commitment by doing the following: +If the asset registration process +fails, e.g. if there are not enough available VNs for the committee, then the AI can refund the fee to themselves +after the time lock expires. - 1. The output commitment is $ C = k.G + v.H $ - 2. $ v $ and $ k.G $ are provided in the metadata - 3. A verifier can calculate $ C - k.G = v.H $ and verify this value by multiplying the clear text $ v $ by $ H $ themselves. +The transaction will contain the following extra metadata to facilitate the registration process: -2. A commitment (hash) to the asset parameters as defined by a [DigitalAssetTemplate] described in [RFC-0311](RFC-0311_AssetTemplates.md). This template will define all the parameters of the asset the AI intends to register including information the VNs need to know like what the required [AssetCollateral] is to be part of the committee. +1. The value of the transaction in clear text and the public spending key of the commitment so that it can be verified + by third parties. A third party can verify the value of the commitment by using the information in (1) and (2) below, to calculate (3): + 1. The output commitment is $ C = k \cdot G + v \cdot H $. + 2. $ v​ $ and $ k \cdot G ​$ are provided in the metadata. + 3. A verifier can calculate $ C - k \cdot G = v \cdot H $ and verify this value by multiplying the clear text $ v $ by $ H $ themselves. -Once this transaction has been confirmed to the required depth on the blockchain the nomination phase can begin. +2. A commitment (hash) to the asset parameters as defined by a [DigitalAssetTemplate] described in + [RFC-0311](RFC-0311_AssetTemplates.md). This template will define all the parameters of the asset that the AI intends to + register, including information the VNs need to know, such as what [AssetCollateral] is required to be part of the committee. -### Nomination phase -The next step in registering an asset is for the AI to select a committee of VNs to manage the asset. The process to do this is described in [RFC-0304](RFC-0304_VNCommittees.md). This process lasts as long as the time-lock on the asset creation transaction described above. The VNs have until that time-lock elapses to nominate themselves (in the case of an asset being registered using the `committee_mode::PUBLIC_NOMINATION` parameter in the [DigitalAssetTemplate]). +Once this transaction has been confirmed to the required depth on the blockchain, the nomination phase can begin. -### Asset commencement -Once the Nomination phase is complete and the AI has selected a committee as described in [RFC-0304](RFC-0304_VNCommittees.md) the chosen committee and AI are ready to commit their `asset_creation_fee` and [AssetCollateral]s to commence the operation of the asset. This is done by the AI and the committee members collaborating to build the initial [Checkpoint] of the asset. When this [Checkpoint] transaction is published to the [base layer] the [digital asset] will be live on the DAN. The [Checkpoint] transaction is described in [RFC_0220](RFC-0220_AssetCheckpoints.md). +### Nomination Phase +The next step in registering an asset is for the AI to select a committee of VNs to manage the asset. The process to do +this is described in [RFC-0304](RFC-0304_VNCommittees.md). This process lasts as long as the time lock on the asset +creation transaction described above. The VNs have until that time lock elapses to nominate themselves (in the case of +an asset being registered using the `committee_mode::PUBLIC_NOMINATION` parameter in the [DigitalAssetTemplate]). + +### Asset Commencement +Once the nomination phase is complete and the AI has selected a committee as described in [RFC-0304](RFC-0304_VNCommittees.md), +the chosen committee and AI are ready to commit their `asset_creation_fee` and [AssetCollateral]s to commence the +operation of the asset. This is done by the AI and the committee members collaborating to build the initial [Checkpoint] +of the asset. When this [Checkpoint] transaction is published to the [base layer], the [digital asset] will be live on +the DAN. The [Checkpoint] transaction is described in [RFC_0220](RFC-0220_AssetCheckpoints.md). [assetcollateral]: Glossary.md#assetcollateral [asset issuer]: Glossary.md#asset-issuer [base layer]: Glossary.md#base-layer [checkpoint]: Glossary.md#checkpoint -[digital asset]: Glossary.md#digital-asset -[DigitalAssetTemplate]: Glossary.md#digitalassettemplate [committee]: Glossary.md#committee [CommitteeSelectionStrategy]: Glossary.md#committeeselectionstrategy -[validator node]: Glossary.md#validator-node +[digital asset]: Glossary.md#digital-asset +[DigitalAssetTemplate]: Glossary.md#digitalassettemplate [digital asset network]: Glossary.md#digital-asset-network [trusted node]: Glossary.md#trusted-node +[validator node]: Glossary.md#validator-node diff --git a/RFC/src/RFC-0350_DisputeResolution.md b/RFC/src/RFC-0350_DisputeResolution.md deleted file mode 100644 index be6ebad5de..0000000000 --- a/RFC/src/RFC-0350_DisputeResolution.md +++ /dev/null @@ -1,6 +0,0 @@ -# RFC-0350/DisputeResolution - -RFC placeholder. - -This document will describe the dispute resolution process, which can occur if a party feels that a VN committee has -broken network rules while executing an instructions. \ No newline at end of file diff --git a/RFC/src/RFC-0370_TariCollections.md b/RFC/src/RFC-0370_TariCollections.md deleted file mode 100644 index 36782a1041..0000000000 --- a/RFC/src/RFC-0370_TariCollections.md +++ /dev/null @@ -1,13 +0,0 @@ -# RFC-0370/TariCollections - -RFC placeholder. - -This document will describe the Tari Collections app. Tari Collections manages all Tari digital assets on behalf of the -user and includes - -* A Tari coin wallet -* A Tari asset token wallet -* The instruction API -* Peer-to-peer communication capabilities -* Asset creation and management features -* Coin <-> Asset bridging capabilities diff --git a/RFC/src/RFC-0371_TokenWallet.md b/RFC/src/RFC-0371_TokenWallet.md deleted file mode 100644 index 9777353736..0000000000 --- a/RFC/src/RFC-0371_TokenWallet.md +++ /dev/null @@ -1,5 +0,0 @@ -# RFC-0371/TokenWallet - -RFC placeholder. - -This document will describe how asset key management and storage is applied by Tari Collections. \ No newline at end of file diff --git a/RFC/src/RFC-0372_TCP2PComm.md b/RFC/src/RFC-0372_TCP2PComm.md deleted file mode 100644 index d59af91a63..0000000000 --- a/RFC/src/RFC-0372_TCP2PComm.md +++ /dev/null @@ -1,5 +0,0 @@ -# RFC-0372/TCP2PComm - -RFC placeholder. - -This document will describe how AMs commincate with each other, VNs, and the Tari base layer. \ No newline at end of file diff --git a/RFC/src/RFC-0373_TCInstructionAPI.md b/RFC/src/RFC-0373_TCInstructionAPI.md deleted file mode 100644 index 0c70534215..0000000000 --- a/RFC/src/RFC-0373_TCInstructionAPI.md +++ /dev/null @@ -1,6 +0,0 @@ -# RFC-0373/TCInstructionAPI - -RFC placeholder. - -This document will describe the Tari instruction API; the API Tari Collections uses to construct and deliver instructions to the VN -network. \ No newline at end of file diff --git a/RFC/src/RFC-0374_TCAssetManagement.md b/RFC/src/RFC-0374_TCAssetManagement.md deleted file mode 100644 index b9fd0952d6..0000000000 --- a/RFC/src/RFC-0374_TCAssetManagement.md +++ /dev/null @@ -1,6 +0,0 @@ -# RFC-0374/TCAssetManagement - -RFC placeholder. - -This document will describe the process of creating and submitting an `AssetCreation` instruction to the DAN, and -tracking the state from an asset issuer's perspective \ No newline at end of file diff --git a/RFC/src/RFC-0375_TCWalletBridge.md b/RFC/src/RFC-0375_TCWalletBridge.md deleted file mode 100644 index 2a790d8c55..0000000000 --- a/RFC/src/RFC-0375_TCWalletBridge.md +++ /dev/null @@ -1,6 +0,0 @@ -# RFC-0375/TCWalletBridge - -RFC placeholder. - -Since all instructions carry a small fee which is paid to the VN committee managing the asset, this document will -describe how Tari Collections synchronises Tari coin payments when submitting instructions. \ No newline at end of file diff --git a/RFC/src/RFC-0530_P2PPayments.md b/RFC/src/RFC-0530_P2PPayments.md deleted file mode 100644 index ca90f3c8bd..0000000000 --- a/RFC/src/RFC-0530_P2PPayments.md +++ /dev/null @@ -1,5 +0,0 @@ -# RFC-0530_P2PPayments - -RFC placeholder. - -This document will describe how peer-to-peer (micro)payments can be achieved using the Tari payment channel mechanism. \ No newline at end of file diff --git a/RFC/src/RFC-0550_VNFees.md b/RFC/src/RFC-0550_VNFees.md deleted file mode 100644 index 52369d7f51..0000000000 --- a/RFC/src/RFC-0550_VNFees.md +++ /dev/null @@ -1,6 +0,0 @@ -# RFC-0550/VNFees - -RFC placeholder. - -This document will describe the high level economics and incentives for setting instruction fees when submitting -instructions on the DAN. \ No newline at end of file diff --git a/RFC/src/RFC-1000_TariUseCases.md b/RFC/src/RFC-1000_TariUseCases.md index e135bcb8f6..287569edb4 100644 --- a/RFC/src/RFC-1000_TariUseCases.md +++ b/RFC/src/RFC-1000_TariUseCases.md @@ -1,14 +1,14 @@ # RFC-1000/TariUseCases -## A Digital Asset Framework +## Digital Asset Framework ![status: draft](./theme/images/status-draft.svg) **Maintainer(s)**: [Leland Lee](https://github.com/lelandlee) -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright 2019 The Tari Development Community @@ -22,38 +22,40 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari -community. The release of this document is intended solely for review and discussion by the community regarding the +community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein. ## Goals -There will be many types of digital assets that can be issued on Tari. This document is intended to help potential asset + +There will be many types of digital assets that can be issued on Tari. The aim of this Request for Comment (RFC) is to +help potential asset issuers identify use cases for Tari that lead to the design of new types of digital assets that may or may not exist in their ecosystems today. -## Related RFCs -* [RFC-0001: An overview of the Tari network](RFC-0001_overview.md) -* [RFC-0300: The Digital Assets Network](RFC-0300_DAN.md) +## Related Requests for Comment +* [RFC-0001: Overview of Tari Network](RFC-0001_overview.md) +* [RFC-0300: Digital Assets Network](RFC-0300_DAN.md) ## Description @@ -61,13 +63,14 @@ Tari [digital asset]s may exist on the Tari [Digital Assets Network] (DAN), are owned, with the associated digital data being classified as intangible, personal property. ### Types -Digital assets may have the following high level classification scheme: + +Digital assets may have the following high-level classification scheme: * Symbolic * Insignia * Mascots - * Event-driven or historical (eg. a rivalry or a highly temporal event) -* Artifacts or Objects + * Event-driven or historical, e.g. a rivalry or a highly temporal event +* Artefacts or objects * Legendary * Rare * High demand @@ -79,7 +82,7 @@ Digital assets may have the following high level classification scheme: * Points * Currency * Full or fractional representation - * Bearer instruments / access token + * Bearer instruments/access token * Chat stickers * Persona * Personality trait(s) @@ -90,8 +93,9 @@ Digital assets may have the following high level classification scheme: * Relationship(s) * Avatar -### Behaviors -Digital asset tokens may influence the following behavioral types: +### Behaviours + +Digital asset tokens may influence the following behavioural types: * Advance purchase * Experience enhancement @@ -102,96 +106,99 @@ Digital asset tokens may influence the following behavioral types: * Donating to a charity * Collecting * Trading -* Building / combining +* Building/combining ### Attributes + Digital assets have many different properties, which may be one or more of the following: * Digital assets can be interactive: - * Easter eggs - * Media - * Dynamic (eg. Imagine if assets were similar to sounds, visualisations or other kinds of "demos" or "gifs") - * Game mechanics - * Evolutionary + * Easter eggs; + * media + * dynamic, e.g. imagine if assets were similar to sounds, visualizations or other kinds of "demos" or "gifs") + * Game mechanics; + * Evolutionary. * Digital assets can be combined to create super assets. -* Digital assets can be attribute(s) of another digital asset (e.g. wheels of a vehicle or a VIP ticket has two drink cards). -* Digital assets can have contingencies (e.g. ownership of a digital asset is contingent on ownership of a different digital asset, using this digital asset is contingent on holding it for a particular duration, etc) -* Digital assets can have utility (e.g. be useful). -* Digital assets can be used across platforms (e.g. a digital asset for a game could be used as avatars in a social network). +* Digital assets can be attribute(s) of another digital asset, e.g. wheels of a vehicle or a VIP ticket has two drink +cards. +* Digital assets can have contingencies, e.g. ownership of a digital asset is contingent on ownership of a different +digital asset. Using this digital asset is contingent on holding it for a particular duration, etc. +* Digital assets can have utility, e.g. be useful. +* Digital assets can be used across platforms, e.g. a digital asset for a game could be used as avatars in a social +network. * Digital assets can have history. * Digital assets can have user-generated tags and/or metadata. ### Interactions Digital asset owners may have the following interactions with the DAN and/or other people: -* Digital asset owners can attest that they have ownership over there assets at time `t`. -* Digital assets owners may attest ownership to an individual, to a group of friends or to the entire world. +* Digital asset owners can attest that they have ownership over their assets at time `t`. +* Digital asset owners may attest ownership to an individual, to a group of friends or to the entire world. ### Rules + Rules are the governance of how digital assets may be used or transferred, as defined by the asset issuer: -* Royalty Fees - Digital asset issuers can set a royalty that charges a fee every time the digital asset is transferred between parties. The fee as defined by the issuer can be fixed or dynamic or follow a complex formula and value is granted to the issuer(s) and/or other entities. -* Contingency - Digital asset ownership/interaction may be contingent/dependent on another asset. -* Timing Controls - Digital assets can only be transferred or used at particular times. -* Sharing - Digital assets can be shared to others or even co-owned. -* Privacy - Ownership of a digital asset can be changed from private to public. -* Upgradability/Versioning - Digital assets can be upgraded and/or versioned. -* Redeemability - Digital assets can be used once or multiple times. +* Royalty fees. Digital asset issuers can set a royalty that charges a fee every time the digital asset is transferred +between parties. The fee as defined by the issuer can be fixed or dynamic, or follow a complex formula, and value is +granted to the issuer(s) and/or other entities. +* Contingency. Digital asset ownership/interaction may be contingent/dependent on another asset. +* Timing controls. Digital assets can only be transferred or used at particular times. +* Sharing. Digital assets can be shared with others or even co-owned. +* Privacy. Ownership of a digital asset can be changed from private to public. +* Upgradability/versioning. Digital assets can be upgraded and/or versioned. +* Redeemability. Digital assets can be used once or multiple times. ### Examples + Some examples of how different types of digital assets with different attributes, rules and interactions may be -manifested are shown below. +manifested are provided here: #### Crystal Skull of Akador -* Is rare - * Is 1 of 5 -* Is legendary - * https://indianajones.fandom.com/wiki/Crystal_Skull_of_Akator - * Press reports that this artifact could be worth $X -* Drives collectibility -* Drives advance purchase - * If you are one of the first 100,000 people to buy tickets to Indiana Jones World you have a chance of winning this 1 of 5 artifact. -* Has superpowers and utility - * If you have this item while visiting Indiana Jones World, you get to skip the line three times. -* Is a contingency for another asset - * If you collect this item, two Sankara stones, and the Cross of Coronado, you can buy the ark of the covenant: - * Ark of the covenant is rare. It is 1 of 1. - * Ark of the covenant is legendary. - * Ark of the covenant gives you lifetime access to Indiana Jones World. - * Ark of the covenant has rules; 20% of the resale price goes to Indiana Jones World. +* Is rare, it is 1 of 5. +* Is legendary: + * https://indianajones.fandom.com/wiki/Crystal_Skull_of_Akator; + * press reports that this artefact could be worth $X. +* Drives collectability. +* Drives advance purchase, e.g. if you are one of the first 100,000 people to buy tickets to Indiana Jones World, you +have a chance of winning this + 1 of 5 artefact. +* Has superpowers and utility, e.g. if you have this item while visiting Indiana Jones World, you get to skip the line +three times. +* Is a contingency for another asset, e.g. if you collect this item, two Sankara stones and the Cross of Coronado, you + can buy the ark of the covenant: + - Ark of the covenant is rare. It is 1 of 1. + - Ark of the covenant is legendary. + - Ark of the covenant gives you lifetime access to Indiana Jones World. + - Ark of the covenant has rules; 20% of the resale price goes to Indiana Jones World. #### AB de Villiers' bat -* Isn’t Rare - * Is 1 of 100,000 -* Is Legendary - * https://www.youtube.com/watch?v=HK6B2da3DPA -* Drives collectibility - * Is part of a series of bats from famous batsmen. -* Can be combined with other assets - * Be one of the first 10 people to combine six bats to turn this asset into a One Day International (ODI) century bat: - * ODI century bats are rare, they are 1 of 10. - * ODI century bats are legendary. -* Has no superpowers -* Has no utility -* Has no rules +- Is not rare, it is 1 of 100,000. +- Is legendary - https://www.youtube.com/watch?v=HK6B2da3DPA. +- Drives collectability, it is part of a series of bats from famous batsmen. +- Can be combined with other assets, e.g. be one of the first 10 people to combine six bats to turn this asset into a +One Day International (ODI) century bat: + - ODI century bats are rare, they are 1 of 10; + - ODI century bats are legendary. +- Has no superpowers. +- Has no utility. +- Has no rules. #### OVO Owl x Supreme -* Is Rare - * Is 1 of 200 -* Is Legendary - * https://www.supremenewyork.com - * https://us.octobersveryown.com -* Has a game mechanic - * Every time it’s transferred, it may become a golden ticket that grants you access to any Drake show. - * If its become a golden ticket and is transferred, it loses its golden ticket superpower. -* Has utility - * Unlocks exclusive media content feat. Drake hosted by OVO SOUND. - * May become a golden ticket that grants you access to any Drake show. -* Has rules - * Every time its transferred Supreme and OVO SOUND receive 25% of the transaction value. +* Is rare, it is 1 of 200. +* Is legendary: + * https://www.supremenewyork.com; + * https://us.octobersveryown.com. +* Has a game mechanic: + * Every time it’s transferred, it may become a golden ticket that grants you access to any Drake show. + * If it becomes a golden ticket and is transferred, it loses its golden ticket superpower. +* Has utility: + * Unlocks exclusive media content feat. Drake hosted by OVO SOUND. + * May become a golden ticket that grants you access to any Drake show. +* Has rules - every time its transferred Supreme and OVO SOUND receive 25% of the transaction value. [digital asset]: Glossary.md#digital-asset [Digital Assets Network]:Glossary.md#digital-asset-network diff --git a/RFC/src/RFC_template.md b/RFC/src/RFC_template.md index 7cead3db3e..c3acb77333 100644 --- a/RFC/src/RFC_template.md +++ b/RFC/src/RFC_template.md @@ -6,9 +6,9 @@ **Maintainer(s)**: -# License +# Licence -[ The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). +[ The 3-Clause BSD Licence](https://opensource.org/licenses/BSD-3-Clause). Copyright @@ -22,24 +22,24 @@ following conditions are met: 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Language -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", -"NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14) (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here. ## Disclaimer -The purpose of this document and its content is for information purposes only and may be subject to change or update +This document and its content are intended for information purposes only and may be subject to change or update without notice. This document may include preliminary concepts that may or may not be in the process of being developed by the Tari @@ -48,7 +48,7 @@ technological merits of the potential system outlined herein. ## Goals -## Related RFCs +## Related Requests for Comment ## Description diff --git a/RFC/src/SUMMARY.md b/RFC/src/SUMMARY.md index 052deeb29a..0e4cd34177 100644 --- a/RFC/src/SUMMARY.md +++ b/RFC/src/SUMMARY.md @@ -6,6 +6,7 @@ - [RFC-0010: Tari code structure and organisation](RFC-0010_CodeStructure.md) - [RFC-0100: The Tari Base Layer](RFC-0100_BaseLayer.md) - [RFC-0110: Base nodes](RFC-0110_BaseNodes.md) + - [RFC-0111: Base node architecture](RFC-0111_BaseNodeArchitecture.md) - [RFC-0130: Mining](RFC-0130_Mining.md) - [RFC-0140: Sync and Seeding](RFC-0140_Syncing_and_seeding.md) - [RFC-0150: Wallets](RFC-0150_Wallets.md) @@ -26,12 +27,10 @@ - [Asset Management](AssetManagement.md) - [RFC-0311: Digital Asset templates](RFC-0311_AssetTemplates.md) - [RFC-0313: The asset read-only API](RFC-0313_AssetReadAPI.md) - - [RFC-0315: Asset transfer mechanism](RFC-0315_AssetTransfer.md) - [RFC-0316: Asset retirement](RFC-0316_AssetRetirement.md) - [RFC-0340: Validator Node Consensus](RFC-0340_VNConsensusOverview.md) - [RFC-0342: Validator node instructions](RFC-0342_VNInstructions.md) - [RFC-0343: The VN consensus algorithm](RFC-0343_VNConsensusAlgorithm.md) - - [RFC-0350: DAN Dispute resolution mechanism](RFC-0350_DisputeResolution.md) - [RFC-0500: Tari payment channels](RFC-0500_PaymentChannels.md) - [RFC-0400: Tari Application Suite](RFC-0400_TariApplications.md) - [RFC-1000: Tari Use Cases](RFC-1000_TariUseCases.md) diff --git a/RFC/src/assets/BaseLayerArch.drawio b/RFC/src/assets/BaseLayerArch.drawio new file mode 100644 index 0000000000..ed0431b4e4 --- /dev/null +++ b/RFC/src/assets/BaseLayerArch.drawio @@ -0,0 +1 @@ +7Vtbc9o6EP41zKQPYcDEQB4DCc1pmzZN0ttTR7EF1iBbVBa3/vqzkuW7wCTglpOTzjSx15JW+2kv0mrT6Az91VuOZt4NczFtWC131ehcNiyrbVld+CUp64RiRZQJJ66mpYR78htrYktT58TFYa6hYIwKMssTHRYE2BE5GuKcLfPNxozmuc7QBJcI9w6iZeo34gpPU9vd8/TDNSYTT7PuW73og4/ixlqS0EMuW2ZInatGZ8gZE9GTvxpiKtGLcYn6jTZ8TSbGcSB26TD23z1cn0/Hn29WX8NvDnEeftyetvvRMAtE51riy+sHINxjviAO1lMX6xgPzuaBi+WQrUZnsPSIwPcz5MivS1ABoHnCp/DWhscF5oIAlheUTAKgCSYblCeuZZHN8SpD0oK8xczHgq+hSfz1XIOq1cqO1WWZrpHV0zQvszyJXiGtF5Nk7BQ6eNDoPQHJWK8zSJbAAxWYyceACVyN3iNyphOF96e5oCTAmu4iPv0EvYiQ0reaLRuISENM8RhEHYSRIbXtw+Btde083r0y3u22Ae/2WW14t18y3nZBv237b+N9Vg03jAJuWSIdA+9QNnerkT8AYmd5wBKvmwXMMgDWrwsvewe8AvdChigFFApD4kjoBOKiTM6gBSDx9XftgdXLD6WXdvx6ucp+vFzHbysiVLdmv2fr90xPeEs7ypdsv1vMCeCCuaZtXLGQzbmDq/UIpJxgsaWdDqPYzcXn8vpnFtg2rG9M45giQRb5qG5adM3hlhGQbJN6JfEnHiESW3fKBuHCOEn80QP17fw4ESylcZQGJkI/Xym7JaUcIDBYq/URdm71Rv0xoXTIKONqxM5I/TuQ7ffzoJ6Vbb/bMelGty7j75VwHjLfD/fD9QBIFfdNBqSMXvKstrBybnCTXSpD6iM8TOTDR7yEFg8cBSFyBGFB3AIYJo2SXrxEiQnfPGzu2rmA8eX/QHESKzlpWKzWIybBRGLOGezzkYCVgs8erNrEU09YdRJLxqc7TOBi8+zVFL7CpsIFLg25BF3ky/WPfgJlxPgScbfMRo8aCeGqGTJ5/sD+DA5Kpmm9lF1Sx67W5j+7SYq5VavzgDnTmvX4kSoex6fKQgogm+kPwWM42030KrYbjeTJI2UsCXZkUpAjtKNOPXaURIm/aEim011hCe/XgQNN7vCvOQ5FbcY0w7Dr1VYUAsvIjpZE1GE3AxSkLBu9oZJvRtcpx4G06nDT1yvOb8LJUSprTU6/cwRe35T6KSy8WjbHQyQoepTtCvvgcYzc0xCNpaJd3P7TkElMqR2g9XwdKWM0nvKoGxRwTosUSnIK0f01l4nIAayCONUrKBVSLWLyNR7ky8P3T7I7zpqdHPGAPAZR+Bpdg/zSHEZTzAOVXvZIKJhct7pYR84/F0W3sAJiGd5wJi05T3PJYo9JmUa+RoFLsfQGHJ8yPpFPcHQEoBClWYBM0wGymlGJamq8xZX86UNMv5f3AIZcpGU6xLRry/VAIK50ADf5/fiTrf5plp3VY2E8OZlNJqIkIfUZfe/BF0k1PFF+35IxygEFEdHjDAdu5LMUsdlsvtlx3DGhoNfhjq1BBzkCJ7G9vdFyj1jTkzR8lapbLbsuVTflgQ/p3hKryB344wNF9JKkqTbbktm5Hd4Df8TYDTOh90m+9sBR2bTHyDqPwwfKxKnVy2bIghAH4VwFurkKeHWxukPBBN9yxsZGPTu4YL5PhI9VingEyl61rTF6rP20+gYLjxV1uDbb2RauFjpp8DMTdU62xwcjHnXOvxyAi9z+csywW9W7o75pd1S80jhcyDBdhdUSMvSR4TVYvAaL12CxJ7dYLPUjym61Fhm7+t8HKZUYeA1Pe4anneOT1aotPpVvxUuRZXJ3O0zybyrDMkbbY0q6Eh+Yg6g68QaCK/+nbCpgLm6WW3+WaT3tklWaF2STLMeM+3FIG3Pmw6/UoaLAjVNVxhxjYTIqYxzOH8GdNHK3nie4OWmmDGjSWJdXqlujJdCweCPl4I30mulkayefBJi/2aymR3TqLpYTJLUDVfUE9e2gTLfkLyWhb3XycNtHkNAv12+UPMJbmftu3UY3NR9ITRdQA8ZECMeh2SzKmimLm2FOmEucbI53jzunLzN9U0u1o9LXTzQj1Eu8SSrWYtkGO69P8XrjQPxsnb3nzuh3e4SmvcvWh1NTHrkA995Fg2md4I9GtkywqmgwWzLYbmwrGNy4NpXFgToSVxYHxiH7OKsDe8+tDixUZ/WKena46kCj9pX/FqC87X7aBQTMNton/4xQr9gu5zu6ZDwmzpyK9c7dnrQ53yvxlFKazebWloUzQMGc5fY+b6ng7tkUx6WSAZMuMlc9qUkF51isuvSJ69JNDjm/pTqALy3eVJz1drypaHcOEcTvrF+0P363WofXHv79eCUW7083V5DUlHXS9ShHlG/aeircPfNUayLMWAK2YXpZ2g75qz8lYdZLthKXJx3IMJobj70g2DZGfs61/KdTASUHYXAjO6cCknNXZar6GX/EBa/pn9pFATP9i8XO1b8= \ No newline at end of file diff --git a/RFC/src/theme/images/base_layer_arch.png b/RFC/src/theme/images/base_layer_arch.png new file mode 100644 index 0000000000..1e13902b1d Binary files /dev/null and b/RFC/src/theme/images/base_layer_arch.png differ diff --git a/applications/console_text_messenger/Cargo.toml b/applications/console_text_messenger/Cargo.toml new file mode 100644 index 0000000000..fde5fe6802 --- /dev/null +++ b/applications/console_text_messenger/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "console_text_messenger" +version = "0.1.0" +authors = ["Philip Robinson "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tari_wallet = {path = "../../base_layer/wallet", version="^0.0"} +tari_common = {path = "../../common", version= "^0.0"} +tari_utilities = { path = "../../infrastructure/tari_util", version = "^0.0"} +tari_comms = { path = "../../comms", version = "^0.0"} +tari_p2p = {path = "../../base_layer/p2p", version = "^0.0"} +tari_crypto = { path = "../../infrastructure/crypto", version = "^0.0"} +clap = "2.33.0" +serde = "1.0.90" +serde_derive = "1.0.90" +chrono = { version = "0.4.6", features = ["serde"]} +config = { version = "0.9.3" } +simple_logger = "1.2.0" +log = { version = "0.4.0", features = ["std"] } +crossbeam-channel = "0.3.8" +log4rs = {version ="0.8.3",features = ["console_appender", "file_appender", "file", "yaml_format"]} +ctrlc = "3.1.3" +pnet = "0.22.0" diff --git a/applications/console_text_messenger/src/main.rs b/applications/console_text_messenger/src/main.rs new file mode 100644 index 0000000000..93b8c8f928 --- /dev/null +++ b/applications/console_text_messenger/src/main.rs @@ -0,0 +1,400 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#[macro_use] +extern crate clap; + +use clap::{App, Arg}; +use crossbeam_channel as channel; +use log::*; +use log4rs::{ + append::file::FileAppender, + config::{Appender, Config, Root}, + encode::pattern::PatternEncoder, +}; +use pnet::datalink::{self, NetworkInterface}; +use serde::{Deserialize, Serialize}; +use std::{fs, io, sync::Arc, thread, time::Duration}; +use tari_comms::{ + connection::NetAddress, + control_service::ControlServiceConfig, + peer_manager::Peer, + types::{CommsPublicKey, CommsSecretKey}, +}; +use tari_crypto::keys::PublicKey; +use tari_p2p::{initialization::CommsConfig, sync_services::ServiceError}; +use tari_utilities::{hex::Hex, message_format::MessageFormat}; +use tari_wallet::{ + text_message_service::{Contact, ReceivedTextMessage}, + wallet::WalletConfig, + Wallet, +}; + +const LOG_TARGET: &str = "applications::cli_text_messenger"; + +#[derive(Debug, Default, Deserialize)] +struct Settings { + control_port: Option, + grpc_port: Option, + secret_key: Option, + data_path: Option, + screen_name: Option, +} +#[derive(Debug, Serialize, Deserialize)] +struct ConfigPeer { + screen_name: String, + pub_key: String, + address: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Peers { + peers: Vec, +} + +/// # A Barebones console based text messege application to help with debugging the comms stack and wallet library +/// This app uses the same command switches as the grpc_wallet server and when using the -N command to load config +/// file/Peer list pairs will load them from the grpc_wallet/sample_config folder. +/// ## Usage +/// You can provide your own config files and parameters via switches (-help will explain those) or you can use the -N +/// switch followed by an integer to load an integer label config/peer list pair +/// `e.g. cargo run --bin console_text_messenger -- -N1` will load wallet_config_node1.toml and node1_peers.json +pub fn main() { + let matches = App::new("Tari Console Text Message Application") + .version("0.1") + .arg( + Arg::with_name("node-num") + .long("node_num") + .short("N") + .help( + "An integer indicating which Node number config to load from the Tari repo root (Node config is a \ + pair of files consisting of config + peers for that node)", + ) + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("config") + .value_name("FILE") + .long("config") + .short("c") + .help("The relative path of a wallet config.toml file") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("grpc-port") + .long("grpc") + .short("g") + .help("The port the gRPC server will listen on") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("control-port") + .long("control-port") + .short("p") + .help("The port the p2p stack will listen on") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("secret-key") + .long("secret") + .short("s") + .help("This nodes communication secret key") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("data-path") + .long("data-path") + .short("d") + .help("Path where this node's database files will be stored") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("peers") + .value_name("FILE") + .long("peers") + .takes_value(true) + .required(false), + ) + .get_matches(); + + let mut settings = Settings::default(); + let mut contacts = Peers { peers: Vec::new() }; + let mut database_path = "./data/text_message_service.sqlite3".to_string(); + // The node-num switch overrides the config and peers switch for quick testing from the tari repo root + if matches.is_present("node-num") { + let node_num = value_t!(matches, "node-num", u32).unwrap(); + let peer_path = format!("./applications/grpc_wallet/sample_config/node{}_peers.json", node_num); + let config_path = format!( + "./applications/grpc_wallet/sample_config/wallet_config_node{}.toml", + node_num + ); + let mut settings_file = config::Config::default(); + settings_file + .merge(config::File::with_name(config_path.as_str())) + .expect("Could not open specified config file"); + settings = settings_file.try_into().unwrap(); + let contents = fs::read_to_string(peer_path).expect("Could not open specified Peers json file"); + contacts = Peers::from_json(contents.as_str()).expect("Could not parse JSON from specified Peers json file"); + database_path = format!("./data/text_message_service_node{}.sqlite3", node_num).to_string(); + } else { + if matches.is_present("config") { + let mut settings_file = config::Config::default(); + settings_file + .merge(config::File::with_name(matches.value_of("config").unwrap())) + .expect("Could not open specified config file"); + settings = settings_file.try_into().unwrap(); + } + if let Some(f) = matches.value_of("peers") { + let contents = fs::read_to_string(f).expect("Could not open specified Peers json file"); + contacts = + Peers::from_json(contents.as_str()).expect("Could not parse JSON from specified Peers json file"); + } + } + if let Some(_c) = matches.values_of("control-port") { + if let Ok(v) = value_t!(matches, "control-port", u32) { + settings.control_port = Some(v) + } + } + if let Some(_c) = matches.values_of("grpc-port") { + if let Ok(v) = value_t!(matches, "grpc-port", u32) { + settings.grpc_port = Some(v); + } + } + if let Some(c) = matches.value_of("secret-key") { + settings.secret_key = Some(c.to_string()) + } + if let Some(p) = matches.value_of("data-path") { + settings.data_path = Some(p.to_string()) + } + + if settings.secret_key.is_none() || + settings.control_port.is_none() || + settings.grpc_port.is_none() || + settings.data_path.is_none() || + settings.screen_name.is_none() + { + error!( + target: LOG_TARGET, + "Control port, gRPC port, Data path, Screen name or Secret Key has not been provided via command line or \ + config file" + ); + std::process::exit(1); + } + + // Setup the local comms stack + let listener_address: NetAddress = format!("0.0.0.0:{}", settings.control_port.unwrap()).parse().unwrap(); + let secret_key = CommsSecretKey::from_hex(settings.secret_key.unwrap().as_str()).unwrap(); + let public_key = CommsPublicKey::from_secret_key(&secret_key); + + // get and filter interfaces + let interfaces: Vec = datalink::interfaces() + .into_iter() + .filter(|interface| { + !interface.is_loopback() && interface.is_up() && interface.ips.iter().any(|addr| addr.is_ipv4()) + }) + .collect(); + + // select first interface + if interfaces.first().is_none() { + error!( + target: LOG_TARGET, + "No available network interface with an Ipv4 Address." + ); + std::process::exit(1); + } + + // get network interface and retrieve ipv4 address + let interface = interfaces.first().unwrap().clone(); + let local_ip = interface + .ips + .iter() + .find(|addr| addr.is_ipv4()) + .unwrap() + .ip() + .to_string(); + + let local_net_address = match format!("{}:{}", local_ip, settings.control_port.unwrap()).parse() { + Ok(na) => na, + Err(_) => { + error!(target: LOG_TARGET, "Could not resolve local IP address"); + std::process::exit(1); + }, + }; + + info!(target: LOG_TARGET, "Local Net Address: {:?}", local_net_address); + + let config = WalletConfig { + comms: CommsConfig { + control_service: ControlServiceConfig { + listener_address: listener_address.clone(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(5000), + }, + socks_proxy_address: None, + host: "0.0.0.0".parse().unwrap(), + public_key: public_key.clone(), + secret_key: secret_key.clone(), + public_address: local_net_address, + datastore_path: settings.data_path.unwrap(), + peer_database_name: public_key.to_hex(), + }, + public_key: public_key.clone(), + database_path, + }; + + let wallet = Wallet::new(config).unwrap(); + + // Add any provided peers to Peer Manager and Text Message Service Contacts + if !contacts.peers.is_empty() { + for p in contacts.peers.iter() { + let pk = CommsPublicKey::from_hex(p.pub_key.as_str()).expect("Error parsing pub key from Hex"); + if let Ok(na) = p.address.clone().parse::() { + let peer = Peer::from_public_key_and_address(pk.clone(), na.clone()).unwrap(); + wallet.comms_services.peer_manager().add_peer(peer).unwrap(); + // If the contacts already exist we don't mind + if let Err(e) = wallet.text_message_service.add_contact(Contact { + screen_name: p.screen_name.clone(), + pub_key: pk.clone(), + address: na.clone(), + }) { + println!("Error adding config file contacts: {:?}", e); + } + } + } + } + + // Setup the logging to a file (screen_name.log), the file will appear in the root where the binary is run from + let logfile = FileAppender::builder() + .encoder(Box::new(PatternEncoder::new( + "{d(%Y-%m-%d %H:%M:%S.%f)} [{M}#{L}] [{t}] {l:5} {m} (({T}:{I})){n}", + ))) + .build(format!("{}.log", settings.screen_name.clone().unwrap())) + .unwrap(); + + let config = Config::builder() + .appender(Appender::builder().build("logfile", Box::new(logfile))) + .build(Root::builder().appender("logfile").build(LevelFilter::Debug)) + .unwrap(); + + let _handle = log4rs::init_config(config).unwrap(); + + let contacts = wallet + .text_message_service + .get_contacts() + .expect("Could not read contacts"); + + // Print out some help messages + println!( + "┌─────────────────────────────────────┐\n│Tari Console Barebones Text \ + Messenger│\n└─────────────────────────────────────┘" + ); + // TODO Read this from the SQL database rather than the config file + println!("This node's screen name: {}", settings.screen_name.unwrap().clone()); + for (i, c) in contacts.iter().enumerate() { + println!("Contact {}: {}", i, c.screen_name.clone()); + } + println!("Active Contact is 0: {}", contacts[0].screen_name.clone()); + println!("To change active contact to send to enter an integer and input_int%(# of contacts) will be made active"); + + // Start a text input thread which sends inputted lines to the main thread via a channel + let (tx, rx) = channel::unbounded(); + let (tx_sigint, rx_sigint) = channel::unbounded(); + thread::spawn(move || { + let mut input = String::new(); + loop { + if let Ok(_l) = io::stdin().read_line(&mut input) { + tx.send(input.clone()).unwrap(); + input = "".to_string(); + } + } + }); + + // setup a handler for ctrl-c + ctrlc::set_handler(move || { + println!("Received SIGINT"); + tx_sigint.send("shutdown").unwrap(); + }) + .expect("Error setting Ctrl-C handler"); + + // keeps track of which received messages have been printed to the console + let mut msg_index = 0; + // keeps track of which contact is currently active for sending + let mut active_contact: usize = 0; + + // Main Loop + loop { + let mut rx_messages: Vec = wallet + .text_message_service + .get_text_messages() + .expect("Error retrieving text messages from TMS") + .received_messages; + + rx_messages.sort(); + + for i in msg_index..rx_messages.len() { + let contact = contacts + .iter() + .find(|c| c.pub_key == rx_messages[i].source_pub_key) + .expect("Message from unknown peer"); + println!( + "{:?} - {:?}: {:?}", + rx_messages[i].timestamp, contact.screen_name, rx_messages[i].message + ); + msg_index = i + 1; + } + + if let Ok(mut input) = rx.recv_timeout(Duration::from_millis(100)) { + input.truncate(input.len() - 1); + + if let Ok(i) = input.clone().parse::() { + active_contact = i % contacts.len(); + println!("Active Contact updated to: {}", contacts[active_contact].screen_name); + } + + wallet + .text_message_service + .send_text_message(contacts[active_contact % contacts.len()].pub_key.clone(), input) + .unwrap() + } + + // check sigint to trigger shutdown + if rx_sigint.recv_timeout(Duration::from_millis(10)).is_ok() { + wallet.service_executor.shutdown().unwrap(); + wallet + .service_executor + .join_timeout(Duration::from_millis(3000)) + .unwrap(); + let comms = Arc::try_unwrap(wallet.comms_services) + .map_err(|_| ServiceError::CommsServiceOwnershipError) + .unwrap(); + + comms.shutdown().unwrap(); + println!("Exiting"); + break; + } + } +} diff --git a/applications/grpc_wallet/Cargo.toml b/applications/grpc_wallet/Cargo.toml new file mode 100644 index 0000000000..3d50dbf57c --- /dev/null +++ b/applications/grpc_wallet/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "tari_grpc_wallet" +version = "0.1.0" +authors = ["Philip Robinson "] +edition = "2018" + +[dependencies] +tari_wallet = {path = "../../base_layer/wallet", version="^0.0"} +tari_common = {path = "../../common", version= "^0.0"} +tari_utilities = { path = "../../infrastructure/tari_util", version = "^0.0"} +tari_comms = { path = "../../comms", version = "^0.0"} +tari_p2p = {path = "../../base_layer/p2p", version = "^0.0"} +tari_crypto = { path = "../../infrastructure/crypto"} +chrono = { version = "0.4.6", features = ["serde"]} +config = { version = "0.9.3" } +crossbeam-channel = "0.3.8" +bytes = "0.4" +derive-error = "0.0.4" +futures = "0.1" +http = "0.1" +log = { version = "0.4.0", features = ["std"] } +prost = "0.5" +tokio = "0.1" +tower-request-modifier = { git = "https://github.com/tower-rs/tower-http" } +tower-hyper = "0.1" +hyper = "0.12" +tower-grpc = { git = "https://github.com/tower-rs/tower-grpc.git", features = ["tower-hyper"] } +tower-grpc-build = { git = "https://github.com/tower-rs/tower-grpc.git", features = ["tower-hyper"]} +simple_logger = "1.2.0" +clap = "2.33.0" +serde = "1.0.90" +serde_derive = "1.0.90" +pnet = "0.22.0" + + +[dev-dependencies] +tari_crypto = { path = "../../infrastructure/crypto"} +tower-util = "0.1" +tempdir = "0.3.7" +rand = "0.5" + +[build-dependencies] +tower-grpc-build = { git = "https://github.com/tower-rs/tower-grpc.git", features = ["tower-hyper"] } + diff --git a/base_layer/blockchain/src/error.rs b/applications/grpc_wallet/build.rs similarity index 83% rename from base_layer/blockchain/src/error.rs rename to applications/grpc_wallet/build.rs index 8901da5de8..4054b842da 100644 --- a/base_layer/blockchain/src/error.rs +++ b/applications/grpc_wallet/build.rs @@ -1,4 +1,4 @@ -// Copyright 2018 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -20,11 +20,11 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// this file is used for all blockchain error types -use derive_error::Error; - -/// The ChainError is used to present all generic chain error of the actual blockchain -#[derive(Debug, Error)] -pub enum ChainError { - Brokenchain, // place holder for real error +fn main() { + // Build wallet + tower_grpc_build::Config::new() + .enable_server(true) + .enable_client(true) + .build(&["proto/wallet_rpc.proto"], &["proto/"]) + .unwrap_or_else(|e| panic!("protobuf compilation failed: {}", e)); } diff --git a/applications/grpc_wallet/proto/wallet_rpc.proto b/applications/grpc_wallet/proto/wallet_rpc.proto new file mode 100644 index 0000000000..8f9f621678 --- /dev/null +++ b/applications/grpc_wallet/proto/wallet_rpc.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +package wallet_rpc; + +// Wallet gRPC service +service WalletRpc { + // Send a Tari Text Message + rpc SendTextMessage(TextMessageToSend) returns (RpcResponse) {} + // Request all messages + // TODO implement pagination + rpc GetTextMessages(VoidParams) returns (TextMessagesResponse) {} + // Return messages that are from/to the Contact's pub_key + rpc GetTextMessagesByContact(Contact) returns (TextMessagesResponse) {} + // Get/Set for Screen Name + rpc SetScreenName(ScreenName) returns (RpcResponse) {} + rpc GetScreenName(VoidParams) returns (ScreenName) {} + // CRUD for Contacts + rpc AddContact(Contact) returns (RpcResponse) {} + rpc RemoveContact(Contact) returns (RpcResponse) {} + rpc GetContacts(VoidParams) returns (Contacts) {} + //This method will update the screen_name of the contact with Pub-key contained in the argument Contact + rpc UpdateContact(Contact) returns (RpcResponse) {} + rpc GetPublicKey(VoidParams) returns (PublicKey) {} +} + +// A generic RPC call response message to convey the result of the call +message RpcResponse { + bool success = 1; + string message = 2; +} + +// A Tari Text Message to be sent +message TextMessageToSend { + string dest_pub_key = 1; + string message = 2; +} + +// A Received Tari Text Message +message ReceivedTextMessage { + string id = 1; + string source_pub_key = 2; + string dest_pub_key = 3; + string message = 4; + string timestamp = 5; +} + +// A Sent Tari Text Message +message SentTextMessage { + string id = 1; + string source_pub_key = 2; + string dest_pub_key = 3; + string message = 4; + string timestamp = 5; + bool acknowledged = 6; +} + +// An Empty message for RPC calls with no parameters +message VoidParams{} + +// A collection of all messages +message TextMessagesResponse { + repeated ReceivedTextMessage received_messages = 1; + repeated SentTextMessage sent_messages = 2; +} + +// Current users screen name +message ScreenName { + string screen_name = 1; +} + +// A contact +message Contact { + string screen_name = 1; + string pub_key = 2; + string address = 3; //IP address with port i.e. "127.0.0.1:11123" +} + +// A list of contacts +message Contacts { + repeated Contact contacts = 1; +} +// Returns your node's communication public key +message PublicKey { + string pub_key = 1; +} \ No newline at end of file diff --git a/applications/grpc_wallet/sample_config/node1_peers.json b/applications/grpc_wallet/sample_config/node1_peers.json new file mode 100644 index 0000000000..0b7813310d --- /dev/null +++ b/applications/grpc_wallet/sample_config/node1_peers.json @@ -0,0 +1,14 @@ +{ + "peers": [ + { + "screen_name": "Philip", + "pub_key": "daa67142abc315b1149b54b8f086eb16596b15ee0e70e37f0aa15294fdcbd65b", + "address": "127.0.0.1:20000" + }, + { + "screen_name": "Cayle", + "pub_key": "a091006a3c606b6dacc4d165688ef137becdcaeb00acbba61dbb15f6c2df1900", + "address": "127.0.0.1:30000" + } + ] +} diff --git a/applications/grpc_wallet/sample_config/node2_peers.json b/applications/grpc_wallet/sample_config/node2_peers.json new file mode 100644 index 0000000000..98195602eb --- /dev/null +++ b/applications/grpc_wallet/sample_config/node2_peers.json @@ -0,0 +1,14 @@ +{ + "peers": [ + { + "screen_name": "Jason", + "pub_key": "e0482c31139733b954cc3e59dcfbb4a65dbec1d53c97d47b18c91f6abfd04209", + "address": "127.0.0.1:10000" + }, + { + "screen_name": "Cayle", + "pub_key": "a091006a3c606b6dacc4d165688ef137becdcaeb00acbba61dbb15f6c2df1900", + "address": "127.0.0.1:30000" + } + ] +} diff --git a/applications/grpc_wallet/sample_config/node3_peers.json b/applications/grpc_wallet/sample_config/node3_peers.json new file mode 100644 index 0000000000..e26e6ba1f3 --- /dev/null +++ b/applications/grpc_wallet/sample_config/node3_peers.json @@ -0,0 +1,14 @@ +{ + "peers": [ + { + "screen_name": "Jason", + "pub_key": "e0482c31139733b954cc3e59dcfbb4a65dbec1d53c97d47b18c91f6abfd04209", + "address": "127.0.0.1:10000" + }, + { + "screen_name": "Philip", + "pub_key": "daa67142abc315b1149b54b8f086eb16596b15ee0e70e37f0aa15294fdcbd65b", + "address": "127.0.0.1:20000" + } + ] +} diff --git a/applications/grpc_wallet/sample_config/wallet_config_node1.toml b/applications/grpc_wallet/sample_config/wallet_config_node1.toml new file mode 100644 index 0000000000..42b29f48e3 --- /dev/null +++ b/applications/grpc_wallet/sample_config/wallet_config_node1.toml @@ -0,0 +1,17 @@ +######################################################################################################################## +# # +# Wallet Configuration Options # +# # +######################################################################################################################## + +# TODO Finalize this config file + +control_port = 10000 +grpc_port = 10001 +secret_key = "bafd4efafac9310b85c7afdc68d0c874597102bc2e1c824caa19d8b263afff05" +data_path = "./data" +screen_name = "Jason" + + + + diff --git a/applications/grpc_wallet/sample_config/wallet_config_node2.toml b/applications/grpc_wallet/sample_config/wallet_config_node2.toml new file mode 100644 index 0000000000..54cf4c3cb6 --- /dev/null +++ b/applications/grpc_wallet/sample_config/wallet_config_node2.toml @@ -0,0 +1,13 @@ +######################################################################################################################## +# # +# Wallet Configuration Options # +# # +######################################################################################################################## + +# TODO Finalize this config file + +control_port = 20000 +grpc_port = 10001 +secret_key = "7745546269487c282076ab3f54a3867c51cf71892f7f37d92323aa6f24dc8b02" +data_path = "./data" +screen_name = "Philip" \ No newline at end of file diff --git a/applications/grpc_wallet/sample_config/wallet_config_node3.toml b/applications/grpc_wallet/sample_config/wallet_config_node3.toml new file mode 100644 index 0000000000..78a862952f --- /dev/null +++ b/applications/grpc_wallet/sample_config/wallet_config_node3.toml @@ -0,0 +1,13 @@ +######################################################################################################################## +# # +# Wallet Configuration Options # +# # +######################################################################################################################## + +# TODO Finalize this config file + +control_port = 30000 +grpc_port = 10001 +secret_key = "a6bbc534c6d390e9e8ab3626d01677a1195dd59e0594c13f786c29b9abb77102" +data_path = "./data" +screen_name = "Cayle" \ No newline at end of file diff --git a/applications/grpc_wallet/src/grpc_interface.rs b/applications/grpc_wallet/src/grpc_interface.rs new file mode 100644 index 0000000000..d8f3afa6f2 --- /dev/null +++ b/applications/grpc_wallet/src/grpc_interface.rs @@ -0,0 +1,395 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::grpc_interface::wallet_rpc::{ + server, + Contact as ContactRpc, + Contacts as ContactsRpc, + PublicKey as PublicKeyRpc, + ReceivedTextMessage as ReceivedTextMessageRpc, + RpcResponse, + ScreenName as ScreenNameRpc, + SentTextMessage as SentTextMessageRpc, + TextMessageToSend as TextMessageToSendRpc, + TextMessagesResponse as TextMessagesResponseRpc, + VoidParams, +}; + +use futures::future; +use log::*; +use std::{convert::From, sync::Arc}; +use tari_comms::{connection::NetAddress, peer_manager::Peer, types::CommsPublicKey}; +use tari_utilities::hex::Hex; +use tari_wallet::{ + text_message_service::{Contact, ReceivedTextMessage, SentTextMessage, UpdateContact}, + Wallet, +}; +use tower_grpc::{Request, Response}; + +const LOG_TARGET: &str = "applications::grpc_wallet"; + +pub mod wallet_rpc { + include!(concat!(env!("OUT_DIR"), "/wallet_rpc.rs")); +} + +impl From for ReceivedTextMessageRpc { + fn from(m: ReceivedTextMessage) -> Self { + ReceivedTextMessageRpc { + id: m.id.to_hex(), + source_pub_key: m.source_pub_key.to_hex(), + dest_pub_key: m.dest_pub_key.to_hex(), + message: m.message, + timestamp: m.timestamp.to_string(), + } + } +} + +impl From for SentTextMessageRpc { + fn from(m: SentTextMessage) -> Self { + SentTextMessageRpc { + id: m.id.to_hex(), + source_pub_key: m.source_pub_key.to_hex(), + dest_pub_key: m.dest_pub_key.to_hex(), + message: m.message, + timestamp: m.timestamp.to_string(), + acknowledged: m.acknowledged, + } + } +} + +#[derive(Clone)] +pub struct WalletRPC { + pub wallet: Arc, +} + +/// Implementation of the the gRPC service methods. +impl server::WalletRpc for WalletRPC { + type AddContactFuture = future::FutureResult, tower_grpc::Status>; + type GetContactsFuture = future::FutureResult, tower_grpc::Status>; + type GetPublicKeyFuture = future::FutureResult, tower_grpc::Status>; + type GetScreenNameFuture = future::FutureResult, tower_grpc::Status>; + type GetTextMessagesByContactFuture = future::FutureResult, tower_grpc::Status>; + type GetTextMessagesFuture = future::FutureResult, tower_grpc::Status>; + type RemoveContactFuture = future::FutureResult, tower_grpc::Status>; + type SendTextMessageFuture = future::FutureResult, tower_grpc::Status>; + type SetScreenNameFuture = future::FutureResult, tower_grpc::Status>; + type UpdateContactFuture = future::FutureResult, tower_grpc::Status>; + + fn send_text_message(&mut self, request: Request) -> Self::SendTextMessageFuture { + trace!( + target: LOG_TARGET, + "SendTextMessage gRPC Request received: {:?}", + request, + ); + + let msg = request.into_inner(); + + let response = match CommsPublicKey::from_hex(msg.dest_pub_key.as_str()) { + Ok(pk) => match self.wallet.text_message_service.send_text_message(pk, msg.message) { + Ok(()) => Response::new(RpcResponse { + success: true, + message: "Text Message Sent".to_string(), + }), + Err(e) => Response::new(RpcResponse { + success: false, + message: format!("Error sending text message: {:?}", e).to_string(), + }), + }, + + Err(e) => Response::new(RpcResponse { + success: false, + message: format!("Error sending text message: {:?}", e).to_string(), + }), + }; + + future::ok(response) + } + + fn get_text_messages(&mut self, request: Request) -> Self::GetTextMessagesFuture { + trace!( + target: LOG_TARGET, + "GetTextMessages gRPC Request received: {:?}", + request + ); + + let response_body = match self.wallet.text_message_service.get_text_messages() { + Ok(mut msgs) => TextMessagesResponseRpc { + sent_messages: msgs.sent_messages.drain(..).map(|m| m.into()).collect(), + received_messages: msgs.received_messages.drain(..).map(|m| m.into()).collect(), + }, + _ => TextMessagesResponseRpc { + sent_messages: Vec::new(), + received_messages: Vec::new(), + }, + }; + let response = Response::new(response_body); + + future::ok(response) + } + + fn get_text_messages_by_contact(&mut self, request: Request) -> Self::GetTextMessagesFuture { + trace!( + target: LOG_TARGET, + "GetTextMessages gRPC Request received: {:?}", + request + ); + + let msg = request.into_inner(); + + let pub_key = match CommsPublicKey::from_hex(msg.pub_key.as_str()) { + Ok(pk) => pk, + _ => { + return future::ok(Response::new(TextMessagesResponseRpc { + sent_messages: Vec::new(), + received_messages: Vec::new(), + })) + }, + }; + + let response_body = match self.wallet.text_message_service.get_text_messages_by_pub_key(pub_key) { + Ok(mut msgs) => TextMessagesResponseRpc { + sent_messages: msgs.sent_messages.drain(..).map(|m| m.into()).collect(), + received_messages: msgs.received_messages.drain(..).map(|m| m.into()).collect(), + }, + _ => TextMessagesResponseRpc { + sent_messages: Vec::new(), + received_messages: Vec::new(), + }, + }; + let response = Response::new(response_body); + + future::ok(response) + } + + fn set_screen_name(&mut self, request: Request) -> Self::SetScreenNameFuture { + trace!(target: LOG_TARGET, "SetScreenName gRPC Request received: {:?}", request,); + + let msg = request.into_inner(); + + let response = match self.wallet.text_message_service.set_screen_name(msg.screen_name) { + Ok(()) => Response::new(RpcResponse { + success: true, + message: "Screen Name Set".to_string(), + }), + Err(e) => Response::new(RpcResponse { + success: false, + message: format!("Error setting screen name: {:?}", e).to_string(), + }), + }; + + future::ok(response) + } + + fn get_screen_name(&mut self, request: Request) -> Self::GetScreenNameFuture { + trace!(target: LOG_TARGET, "GetScreenName gRPC Request received: {:?}", request,); + + let _msg = request.into_inner(); + + let screen_name = self + .wallet + .text_message_service + .get_screen_name() + .unwrap_or_else(|_| Some("".to_string())) // Unwrap result + .unwrap_or_else(|| "".to_string()); // Unwrap Option + + future::ok(Response::new(ScreenNameRpc { screen_name })) + } + + fn get_public_key(&mut self, request: Request) -> Self::GetPublicKeyFuture { + trace!(target: LOG_TARGET, "GetPublicKey gRPC Request received: {:?}", request,); + + let _msg = request.into_inner(); + + let public_key = self.wallet.public_key.clone().to_hex(); + + future::ok(Response::new(PublicKeyRpc { pub_key: public_key })) + } + + fn add_contact(&mut self, request: Request) -> Self::AddContactFuture { + trace!(target: LOG_TARGET, "AddContact gRPC Request received: {:?}", request,); + + let msg = request.into_inner(); + + let screen_name = msg.screen_name.clone(); + let pub_key = match CommsPublicKey::from_hex(msg.pub_key.as_str()) { + Ok(pk) => pk, + _ => { + return future::ok(Response::new(RpcResponse { + success: false, + message: "Failed to add contact, cannot serialize public key".to_string(), + })) + }, + }; + + let net_address = match msg.address.clone().parse::() { + Ok(n) => n, + Err(e) => { + return future::ok(Response::new(RpcResponse { + success: false, + message: format!("Failed to add contact, cannot parse net address: {:?}", e).to_string(), + })) + }, + }; + + let peer = match Peer::from_public_key_and_address(pub_key.clone(), net_address.clone()) { + Ok(p) => p, + Err(e) => { + return future::ok(Response::new(RpcResponse { + success: false, + message: format!("Failed to add contact, cannot create peer: {:?}", e).to_string(), + })) + }, + }; + + match self.wallet.comms_services.peer_manager().add_peer(peer) { + Err(e) => { + return future::ok(Response::new(RpcResponse { + success: false, + message: format!("Failed to add contact, cannot add peer to Peer Manager: {:?}", e).to_string(), + })) + }, + _ => (), + }; + + match self.wallet.text_message_service.add_contact(Contact { + screen_name, + pub_key, + address: net_address, + }) { + Ok(()) => (), + Err(e) => { + return future::ok(Response::new(RpcResponse { + success: false, + message: format!("Error adding contact: {:?}", e).to_string(), + })) + }, + }; + + future::ok(Response::new(RpcResponse { + success: true, + message: "Successfully added contact".to_string(), + })) + } + + fn remove_contact(&mut self, request: Request) -> Self::RemoveContactFuture { + trace!(target: LOG_TARGET, "RemoveContact gRPC Request received: {:?}", request,); + + let msg = request.into_inner(); + + let net_address = match msg.address.clone().parse::() { + Ok(n) => n, + Err(e) => { + return future::ok(Response::new(RpcResponse { + success: false, + message: format!("Failed to remove contact, cannot parse net address: {:?}", e).to_string(), + })) + }, + }; + + let screen_name = msg.screen_name.clone(); + + if let Ok(pk) = CommsPublicKey::from_hex(msg.pub_key.as_str()) { + let response = match self.wallet.text_message_service.remove_contact(Contact { + screen_name, + pub_key: pk, + address: net_address, + }) { + Ok(()) => Response::new(RpcResponse { + success: true, + message: "Successfully removed contact".to_string(), + }), + Err(e) => Response::new(RpcResponse { + success: false, + message: format!("Error removing contact: {:?}", e).to_string(), + }), + }; + + return future::ok(response); + } else { + return future::ok(Response::new(RpcResponse { + success: false, + message: "Failed to remove contact, cannot serialize public key".to_string(), + })); + } + } + + fn update_contact(&mut self, request: Request) -> Self::RemoveContactFuture { + trace!(target: LOG_TARGET, "UpdateContact gRPC Request received: {:?}", request,); + + let msg = request.into_inner(); + let net_address = match msg.address.clone().parse::() { + Ok(n) => n, + Err(e) => { + return future::ok(Response::new(RpcResponse { + success: false, + message: format!("Failed to update contact, cannot parse net address: {:?}", e).to_string(), + })) + }, + }; + let screen_name = msg.screen_name.clone(); + if let Ok(pk) = CommsPublicKey::from_hex(msg.pub_key.as_str()) { + let response = match self.wallet.text_message_service.update_contact(pk, UpdateContact { + screen_name: Some(screen_name), + address: Some(net_address), + }) { + Ok(()) => Response::new(RpcResponse { + success: true, + message: "Successfully updated contact".to_string(), + }), + Err(e) => Response::new(RpcResponse { + success: false, + message: format!("Error updating contact: {:?}", e).to_string(), + }), + }; + + return future::ok(response); + } else { + return future::ok(Response::new(RpcResponse { + success: false, + message: "Failed to update contact, cannot serialize public key".to_string(), + })); + } + } + + fn get_contacts(&mut self, request: Request) -> Self::GetContactsFuture { + trace!(target: LOG_TARGET, "GetContacts gRPC Request received: {:?}", request,); + + let mut contacts_resp: Vec = Vec::new(); + + if let Ok(contacts) = self.wallet.text_message_service.get_contacts() { + for c in contacts.iter() { + let sn = c.screen_name.clone(); + let address = format!("{}", c.address.clone()); + + contacts_resp.push(ContactRpc { + screen_name: sn, + pub_key: c.pub_key.to_hex(), + address, + }); + } + } + + future::ok(Response::new(ContactsRpc { + contacts: contacts_resp, + })) + } +} diff --git a/base_layer/core/src/pow.rs b/applications/grpc_wallet/src/lib.rs similarity index 93% rename from base_layer/core/src/pow.rs rename to applications/grpc_wallet/src/lib.rs index 99c60c6162..48b472e08d 100644 --- a/base_layer/core/src/pow.rs +++ b/applications/grpc_wallet/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2018 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -20,5 +20,5 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#[derive(Clone, Debug, PartialEq)] -pub struct ProofOfWork {} +pub mod grpc_interface; +pub mod wallet_server; diff --git a/applications/grpc_wallet/src/main.rs b/applications/grpc_wallet/src/main.rs new file mode 100644 index 0000000000..6024917a5d --- /dev/null +++ b/applications/grpc_wallet/src/main.rs @@ -0,0 +1,275 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#[macro_use] +extern crate clap; + +use pnet::datalink::{self, NetworkInterface}; + +use clap::{App, Arg}; +use log::*; +use serde::{Deserialize, Serialize}; +use std::{fs, sync::Arc, time::Duration}; +use tari_comms::{ + connection::NetAddress, + control_service::ControlServiceConfig, + peer_manager::Peer, + types::{CommsPublicKey, CommsSecretKey}, +}; +use tari_crypto::keys::PublicKey; +use tari_grpc_wallet::wallet_server::WalletServer; +use tari_p2p::initialization::CommsConfig; +use tari_utilities::{hex::Hex, message_format::MessageFormat}; +use tari_wallet::{text_message_service::Contact, wallet::WalletConfig, Wallet}; +const LOG_TARGET: &str = "applications::grpc_wallet"; + +#[derive(Debug, Default, Deserialize)] +struct Settings { + control_port: Option, + grpc_port: Option, + secret_key: Option, + data_path: Option, +} +#[derive(Debug, Serialize, Deserialize)] +struct ConfigPeer { + screen_name: String, + pub_key: String, + address: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Peers { + peers: Vec, +} + +/// Entry point into the gRPC server binary +pub fn main() { + let _ = simple_logger::init_with_level(log::Level::Info); + + let matches = App::new("Tari Wallet gRPC server") + .version("0.1") + .arg( + Arg::with_name("node-num") + .long("node_num") + .short("N") + .help( + "An integer indicating which Node number config to load from the Tari repo root (Node config is a \ + pair of files consisting of config + peers for that node)", + ) + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("config") + .value_name("FILE") + .long("config") + .short("c") + .help("The relative path of a wallet config.toml file") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("grpc-port") + .long("grpc") + .short("g") + .help("The port the gRPC server will listen on") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("control-port") + .long("control-port") + .short("p") + .help("The port the p2p stack will listen on") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("secret-key") + .long("secret") + .short("s") + .help("This nodes communication secret key") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("data-path") + .long("data-path") + .short("d") + .help("Path where this node's database files will be stored") + .takes_value(true) + .required(false), + ) + .arg( + Arg::with_name("peers") + .value_name("FILE") + .long("peers") + .takes_value(true) + .required(false), + ) + .get_matches(); + + let mut settings = Settings::default(); + let mut contacts = Peers { peers: Vec::new() }; + let mut database_path = "./data/text_message_service.sqlite3".to_string(); + // The node-num switch overrides the config and peers switch for quick testing from the tari repo root + if matches.is_present("node-num") { + let node_num = value_t!(matches, "node-num", u32).unwrap(); + let peer_path = format!("./applications/grpc_wallet/sample_config/node{}_peers.json", node_num); + let config_path = format!( + "./applications/grpc_wallet/sample_config/wallet_config_node{}.toml", + node_num + ); + let mut settings_file = config::Config::default(); + settings_file + .merge(config::File::with_name(config_path.as_str())) + .expect("Could not open specified config file"); + settings = settings_file.try_into().unwrap(); + let contents = fs::read_to_string(peer_path).expect("Could not open specified Peers json file"); + contacts = Peers::from_json(contents.as_str()).expect("Could not parse JSON from specified Peers json file"); + database_path = format!("./data/text_message_service_node{}.sqlite3", node_num).to_string(); + } else { + if matches.is_present("config") { + let mut settings_file = config::Config::default(); + settings_file + .merge(config::File::with_name(matches.value_of("config").unwrap())) + .expect("Could not open specified config file"); + settings = settings_file.try_into().unwrap(); + } + if let Some(f) = matches.value_of("peers") { + let contents = fs::read_to_string(f).expect("Could not open specified Peers json file"); + contacts = + Peers::from_json(contents.as_str()).expect("Could not parse JSON from specified Peers json file"); + } + } + if let Some(_c) = matches.values_of("control-port") { + if let Ok(v) = value_t!(matches, "control-port", u32) { + settings.control_port = Some(v) + } + } + if let Some(_c) = matches.values_of("grpc-port") { + if let Ok(v) = value_t!(matches, "grpc-port", u32) { + settings.grpc_port = Some(v); + } + } + if let Some(c) = matches.value_of("secret-key") { + settings.secret_key = Some(c.to_string()) + } + if let Some(p) = matches.value_of("data-path") { + settings.data_path = Some(p.to_string()) + } + + if settings.secret_key.is_none() || + settings.control_port.is_none() || + settings.grpc_port.is_none() || + settings.data_path.is_none() + { + error!( + target: LOG_TARGET, + "Control port, gRPC port, Data path or Secret Key has not been provided via command line or config file" + ); + std::process::exit(1); + } + + // Setup the local comms stack + let listener_address: NetAddress = format!("0.0.0.0:{}", settings.control_port.unwrap()).parse().unwrap(); + let secret_key = CommsSecretKey::from_hex(settings.secret_key.unwrap().as_str()).unwrap(); + let public_key = CommsPublicKey::from_secret_key(&secret_key); + + // get and filter interfaces + let interfaces: Vec = datalink::interfaces() + .into_iter() + .filter(|interface| { + !interface.is_loopback() && interface.is_up() && interface.ips.iter().any(|addr| addr.is_ipv4()) + }) + .collect(); + + // select first interface + if interfaces.first().is_none() { + error!( + target: LOG_TARGET, + "No available network interface with an Ipv4 Address." + ); + std::process::exit(1); + } + + // get network interface and retrieve ipv4 address + let interface = interfaces.first().unwrap().clone(); + let local_ip = interface + .ips + .iter() + .find(|addr| addr.is_ipv4()) + .unwrap() + .ip() + .to_string(); + + let local_net_address = match format!("{}:{}", local_ip, settings.control_port.unwrap()).parse() { + Ok(na) => na, + Err(_) => { + error!(target: LOG_TARGET, "Could not resolve local IP address"); + std::process::exit(1); + }, + }; + + info!(target: LOG_TARGET, "Local Net Address: {:?}", local_net_address); + + let config = WalletConfig { + comms: CommsConfig { + control_service: ControlServiceConfig { + listener_address: listener_address.clone(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(5000), + }, + socks_proxy_address: None, + host: "0.0.0.0".parse().unwrap(), + public_key: public_key.clone(), + secret_key: secret_key.clone(), + public_address: local_net_address, + datastore_path: settings.data_path.unwrap(), + peer_database_name: public_key.to_hex(), + }, + public_key: public_key.clone(), + database_path, + }; + + let wallet = Wallet::new(config).unwrap(); + + // Add any provided peers to Peer Manager and Text Message Service Contacts + if !contacts.peers.is_empty() { + for p in contacts.peers.iter() { + let pk = CommsPublicKey::from_hex(p.pub_key.as_str()).expect("Error parsing pub key from Hex"); + if let Ok(na) = p.address.clone().parse::() { + let peer = Peer::from_public_key_and_address(pk.clone(), na.clone()).unwrap(); + wallet.comms_services.peer_manager().add_peer(peer).unwrap(); + if let Err(e) = wallet.text_message_service.add_contact(Contact { + screen_name: p.screen_name.clone(), + pub_key: pk.clone(), + address: na.clone(), + }) { + info!("Error adding config file contacts: {:?}", e); + } + } + } + } + + let wallet_server = WalletServer::new(settings.grpc_port.unwrap(), Arc::new(wallet)); + let _res = wallet_server.start(); +} diff --git a/applications/grpc_wallet/src/wallet_server.rs b/applications/grpc_wallet/src/wallet_server.rs new file mode 100644 index 0000000000..9266d8a0ec --- /dev/null +++ b/applications/grpc_wallet/src/wallet_server.rs @@ -0,0 +1,82 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::grpc_interface::{wallet_rpc::server, WalletRPC}; +use derive_error::Error; +use futures::{future::Future, stream::Stream}; +use hyper::server::conn::Http; +use log::*; +use std::{net::AddrParseError, sync::Arc}; +use tari_utilities::message_format::MessageFormatError; +use tari_wallet::Wallet; +use tokio::net::TcpListener; +use tower_hyper::Server; + +const LOG_TARGET: &str = "applications::grpc_wallet"; + +#[derive(Debug, Error)] +pub enum WalletServerError { + AddrParseError(AddrParseError), + IoError(std::io::Error), + MessageFormatError(MessageFormatError), +} + +/// Instance of the Wallet RPC Server with a reference to the Wallet API and the config +pub struct WalletServer { + // TODO some form of authentication + port: u32, + wallet: Arc, +} + +impl WalletServer { + pub fn new(port: u32, wallet: Arc) -> WalletServer { + WalletServer { port, wallet } + } + + pub fn start(self) -> Result<(), WalletServerError> { + let new_service = server::WalletRpcServer::new(WalletRPC { + wallet: self.wallet.clone(), + }); + + let mut server = Server::new(new_service); + + let http = Http::new().http2_only(true).clone(); + let addr = format!("127.0.0.1:{}", self.port); + let bind = TcpListener::bind(&addr.clone().as_str().parse()?)?; + let serve = bind + .incoming() + .for_each(move |sock| { + if let Err(e) = sock.set_nodelay(true) { + return Err(e); + } + + let serve = server.serve_with(sock, http.clone()); + tokio::spawn(serve.map_err(|e| error!("Error starting Hyper service: {:?}", e))); + + Ok(()) + }) + .map_err(|e| error!("Error accepting request: {:?}", e)); + info!(target: LOG_TARGET, "Starting Wallet gRPC Server at {}", addr); + tokio::run(serve); + Ok(()) + } +} diff --git a/applications/grpc_wallet/tests/mod.rs b/applications/grpc_wallet/tests/mod.rs new file mode 100644 index 0000000000..14d5c72a00 --- /dev/null +++ b/applications/grpc_wallet/tests/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod wallet_grpc_server; diff --git a/applications/grpc_wallet/tests/wallet_grpc_server/mod.rs b/applications/grpc_wallet/tests/wallet_grpc_server/mod.rs new file mode 100644 index 0000000000..5d9444883d --- /dev/null +++ b/applications/grpc_wallet/tests/wallet_grpc_server/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +pub mod wallet_grpc_server; diff --git a/applications/grpc_wallet/tests/wallet_grpc_server/wallet_grpc_server.rs b/applications/grpc_wallet/tests/wallet_grpc_server/wallet_grpc_server.rs new file mode 100644 index 0000000000..8e4f49d268 --- /dev/null +++ b/applications/grpc_wallet/tests/wallet_grpc_server/wallet_grpc_server.rs @@ -0,0 +1,631 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crossbeam_channel::bounded; +use futures::future::Future; +use hyper::client::connect::{Destination, HttpConnector}; +use log::{Level, *}; +use rand::{distributions::Alphanumeric, rngs::OsRng, Rng}; +use std::{iter, path::PathBuf, sync::Arc, thread, time::Duration}; +use tari_comms::{ + connection::{net_address::NetAddressWithStats, NetAddress, NetAddressesWithStats}, + control_service::ControlServiceConfig, + peer_manager::{peer::PeerFlags, NodeId, Peer}, + types::{CommsPublicKey, CommsSecretKey}, +}; +use tari_crypto::keys::{PublicKey, SecretKey}; +use tari_grpc_wallet::{ + grpc_interface::wallet_rpc::{ + client::WalletRpc, + Contact as ContactRpc, + RpcResponse, + ScreenName as ScreenNameRpc, + TextMessageToSend as TextMessageToSendRpc, + VoidParams, + }, + wallet_server::WalletServer, +}; +use tari_p2p::initialization::CommsConfig; +use tari_utilities::hex::Hex; +use tari_wallet::{text_message_service::Contact, wallet::WalletConfig, Wallet}; +use tempdir::TempDir; +use tower_grpc::Request; +use tower_hyper::{client, util}; +use tower_util::MakeService; + +const LOG_TARGET: &str = "applications::grpc_wallet"; +const WALLET_GRPC_PORT: u32 = 26778; + +pub fn init() { + let _ = simple_logger::init_with_level(Level::Debug); +} + +fn get_path(name: Option<&str>) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name.unwrap_or("")); + path.to_str().unwrap().to_string() +} + +fn clean_up_sql_database(name: &str) { + if std::fs::metadata(get_path(Some(name))).is_ok() { + std::fs::remove_file(get_path(Some(name))).unwrap(); + } +} + +fn init_sql_database(name: &str) { + clean_up_sql_database(name); + let path = get_path(None); + let _ = std::fs::create_dir(&path).unwrap_or_default(); +} + +fn send_text_message_request(msg: TextMessageToSendRpc, desired_response: RpcResponse) { + let (tx, rx) = bounded(1); + + let uri: http::Uri = format!("http://127.0.0.1:{}", WALLET_GRPC_PORT).parse().unwrap(); + + let dst = Destination::try_from_uri(uri.clone()).unwrap(); + let connector = util::Connector::new(HttpConnector::new(1)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings); + + let send_text_message = make_client + .make_service(dst.clone()) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri.clone()) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + WalletRpc::new(conn).ready() + }) + .and_then(|mut client| client.send_text_message(Request::new(msg))) + .and_then(move |response| { + info!(target: LOG_TARGET, "SendTextMessage Response received: {:?}", response); + let inbound = response.into_inner(); + + let _ = tx.send(inbound); + + Ok(()) + }) + .map_err(|e| { + panic!("RPC Client error = {:?}", e); + }); + + tokio::run(send_text_message); + thread::sleep(Duration::from_millis(100)); + + let inbound = rx.recv().unwrap(); + + println!("{:?}", inbound); + println!("{:?}", desired_response); + assert_eq!(inbound.success, desired_response.success); + assert_eq!(inbound.message, desired_response.message); +} + +fn get_text_messages_request(sent_messages: Vec, received_messages: Vec, contact: Option) { + let mut recv_msg: Vec = Vec::new(); + let mut send_msg: Vec = Vec::new(); + + // Check for new text messages up to 40 times with 100ms wait in between = 4000ms Timeout before moving on + for _ in 0..40 { + let move_contact = contact.clone(); + let (tx, rx) = bounded(2); + let uri: http::Uri = format!("http://127.0.0.1:{}", WALLET_GRPC_PORT).parse().unwrap(); + let dst = Destination::try_from_uri(uri.clone()).unwrap(); + let connector = util::Connector::new(HttpConnector::new(1)); + let settings = client::Builder::new().http2_only(true).clone(); + + let mut make_client = client::Connect::with_builder(connector, settings.clone()); + let get_text_messages = make_client + .make_service(dst.clone()) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri.clone()) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + WalletRpc::new(conn).ready() + }) + .and_then(|mut client| { + if move_contact.is_some() { + client.get_text_messages_by_contact(Request::new(move_contact.unwrap())) + } else { + client.get_text_messages(Request::new(VoidParams {})) + } + }) + .and_then(move |response| { + info!(target: LOG_TARGET, "GetTextMessages Response received: {:?}", response); + let inbound = response.into_inner(); + + let recv_msg = inbound.received_messages.iter().map(|m| m.message.clone()).collect(); + + let sent_msg = inbound.sent_messages.iter().map(|m| m.message.clone()).collect(); + + let _ = tx.send(recv_msg); + let _ = tx.send(sent_msg); + + Ok(()) + }) + .map_err(|e| { + panic!("RPC Client error = {:?}", e); + }); + + tokio::run(get_text_messages); + + recv_msg = rx.recv().unwrap(); + send_msg = rx.recv().unwrap(); + + if recv_msg.len() > 0 && send_msg.len() > 0 { + break; + } + thread::sleep(Duration::from_millis(100)); + } + + assert_eq!(recv_msg.len(), received_messages.len()); + assert_eq!(send_msg.len(), sent_messages.len()); + + recv_msg + .iter() + .for_each(|m| assert!(received_messages.iter().any(|m2| m == m2))); + send_msg + .iter() + .for_each(|m| assert!(sent_messages.iter().any(|m2| m == m2))); +} + +fn set_get_screen_name(name: String) { + let requested_name = name.clone(); + let uri: http::Uri = format!("http://127.0.0.1:{}", WALLET_GRPC_PORT).parse().unwrap(); + + let dst = Destination::try_from_uri(uri.clone()).unwrap(); + let connector = util::Connector::new(HttpConnector::new(1)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings.clone()); + + let set_screen_name = make_client + .make_service(dst.clone()) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri.clone()) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + WalletRpc::new(conn).ready() + }) + .and_then(|mut client| client.set_screen_name(Request::new(ScreenNameRpc { screen_name: name }))) + .and_then(move |response| { + info!(target: LOG_TARGET, "SetScreenName Response received: {:?}", response); + let inbound = response.into_inner(); + assert_eq!(inbound.success, true); + Ok(()) + }) + .map_err(|e| { + panic!("RPC Client error = {:?}", e); + }); + + tokio::run(set_screen_name); + thread::sleep(Duration::from_millis(100)); + let (tx, rx) = bounded(1); + let uri: http::Uri = format!("http://127.0.0.1:{}", WALLET_GRPC_PORT).parse().unwrap(); + let connector = util::Connector::new(HttpConnector::new(1)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings); + + let get_screen_name = make_client + .make_service(dst.clone()) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri.clone()) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + WalletRpc::new(conn).ready() + }) + .and_then(|mut client| client.get_screen_name(Request::new(VoidParams {}))) + .and_then(move |response| { + info!(target: LOG_TARGET, "GetScreenName Response received: {:?}", response); + + let _ = tx.send(response.into_inner()); + + Ok(()) + }) + .map_err(|e| { + panic!("RPC Client error = {:?}", e); + }); + + tokio::run(get_screen_name); + + let recv_screen_name = rx.recv().unwrap(); + assert_eq!(recv_screen_name.screen_name, requested_name); +} + +fn get_pub_key(pub_key: String) { + let (tx, rx) = bounded(1); + let uri: http::Uri = format!("http://127.0.0.1:{}", WALLET_GRPC_PORT).parse().unwrap(); + let dst = Destination::try_from_uri(uri.clone()).unwrap(); + let connector = util::Connector::new(HttpConnector::new(1)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings); + + let get_pub_key = make_client + .make_service(dst.clone()) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri.clone()) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + WalletRpc::new(conn).ready() + }) + .and_then(|mut client| client.get_public_key(Request::new(VoidParams {}))) + .and_then(move |response| { + info!(target: LOG_TARGET, "GetPubKey Response received: {:?}", response); + + let _ = tx.send(response.into_inner()); + + Ok(()) + }) + .map_err(|e| { + panic!("RPC Client error = {:?}", e); + }); + + tokio::run(get_pub_key); + + let recv_pub_key = rx.recv().unwrap(); + assert_eq!(recv_pub_key.pub_key, pub_key); +} + +fn add_contact(contact: ContactRpc) { + let uri: http::Uri = format!("http://127.0.0.1:{}", WALLET_GRPC_PORT).parse().unwrap(); + let dst = Destination::try_from_uri(uri.clone()).unwrap(); + let connector = util::Connector::new(HttpConnector::new(1)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings.clone()); + + let add_contact = make_client + .make_service(dst.clone()) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri.clone()) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + WalletRpc::new(conn).ready() + }) + .and_then(|mut client| client.add_contact(Request::new(contact))) + .and_then(move |response| { + info!(target: LOG_TARGET, "AddContact Response received: {:?}", response); + let inbound = response.into_inner(); + assert_eq!(inbound.success, true); + Ok(()) + }) + .map_err(|e| { + panic!("RPC Client error = {:?}", e); + }); + + tokio::run(add_contact); +} + +fn contacts_crud() { + let mut rng = rand::OsRng::new().unwrap(); + + let mut contacts: Vec = Vec::new(); + let screen_names = vec!["Andy".to_string(), "Bob".to_string(), "Carol".to_string()]; + for i in 0..3 { + let contact_secret_key = CommsSecretKey::random(&mut rng); + let contact_public_key = CommsPublicKey::from_secret_key(&contact_secret_key); + contacts.push(ContactRpc { + screen_name: screen_names[i].clone(), + pub_key: contact_public_key.to_hex(), + address: "127.0.0.1:37522".to_string(), + }); + } + + add_contact(contacts[0].clone()); + thread::sleep(Duration::from_millis(50)); + + add_contact(contacts[1].clone()); + thread::sleep(Duration::from_millis(50)); + + add_contact(contacts[2].clone()); + thread::sleep(Duration::from_millis(50)); + + // Remove a contact + let move_contact = contacts[1].clone(); + let uri: http::Uri = format!("http://127.0.0.1:{}", WALLET_GRPC_PORT).parse().unwrap(); + let dst = Destination::try_from_uri(uri.clone()).unwrap(); + let connector = util::Connector::new(HttpConnector::new(1)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings); + + let remove_contact = make_client + .make_service(dst.clone()) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri.clone()) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + WalletRpc::new(conn).ready() + }) + .and_then(|mut client| client.remove_contact(Request::new(move_contact))) + .and_then(move |response| { + info!(target: LOG_TARGET, "RemoveContact Response received: {:?}", response); + + let inbound = response.into_inner(); + assert_eq!(inbound.success, true); + + Ok(()) + }) + .map_err(|e| { + panic!("RPC Client error = {:?}", e); + }); + + tokio::run(remove_contact); + thread::sleep(Duration::from_millis(100)); + + // Update a contact + let updated_contact = ContactRpc { + screen_name: "Updated".to_string(), + pub_key: contacts[0].pub_key.clone(), + address: contacts[0].address.clone(), + }; + let uri: http::Uri = format!("http://127.0.0.1:{}", WALLET_GRPC_PORT).parse().unwrap(); + let dst = Destination::try_from_uri(uri.clone()).unwrap(); + let connector = util::Connector::new(HttpConnector::new(1)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings); + + let update_contact = make_client + .make_service(dst.clone()) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri.clone()) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + WalletRpc::new(conn).ready() + }) + .and_then(|mut client| client.update_contact(Request::new(updated_contact))) + .and_then(move |response| { + info!(target: LOG_TARGET, "UpdateContact Response received: {:?}", response); + + let inbound = response.into_inner(); + assert_eq!(inbound.success, true); + + Ok(()) + }) + .map_err(|e| { + panic!("RPC Client error = {:?}", e); + }); + + tokio::run(update_contact); + thread::sleep(Duration::from_millis(100)); + + // check contacts + let (tx, rx) = bounded(1); + let uri: http::Uri = format!("http://127.0.0.1:{}", WALLET_GRPC_PORT).parse().unwrap(); + let dst = Destination::try_from_uri(uri.clone()).unwrap(); + let connector = util::Connector::new(HttpConnector::new(1)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings); + + let get_contacts = make_client + .make_service(dst.clone()) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri.clone()) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + WalletRpc::new(conn).ready() + }) + .and_then(|mut client| client.get_contacts(Request::new(VoidParams {}))) + .and_then(move |response| { + info!(target: LOG_TARGET, "RemoveContact Response received: {:?}", response); + + let _ = tx.send(response.into_inner()); + + Ok(()) + }) + .map_err(|e| { + panic!("RPC Client error = {:?}", e); + }); + + tokio::run(get_contacts); + + let recv_contacts = rx.recv().unwrap(); + assert_eq!(recv_contacts.contacts.len(), 3); + assert_eq!(recv_contacts.contacts[1], ContactRpc { + screen_name: "Updated".to_string(), + pub_key: contacts[0].pub_key.clone(), + address: contacts[0].address.clone(), + }); + assert_eq!(recv_contacts.contacts[2], contacts[2]); +} + +fn create_peer(public_key: CommsPublicKey, net_address: NetAddress) -> Peer { + Peer::new( + public_key.clone(), + NodeId::from_key(&public_key).unwrap(), + NetAddressesWithStats::new(vec![NetAddressWithStats::new(net_address.clone())]), + PeerFlags::empty(), + ) +} + +pub fn random_string(len: usize) -> String { + let mut rng = OsRng::new().unwrap(); + iter::repeat(()).map(|_| rng.sample(Alphanumeric)).take(len).collect() +} + +#[test] +fn test_rpc_text_message_service() { + init(); + let mut rng = rand::OsRng::new().unwrap(); + let listener_address1: NetAddress = "127.0.0.1:32775".parse().unwrap(); + let secret_key1 = CommsSecretKey::random(&mut rng); + let public_key1 = CommsPublicKey::from_secret_key(&secret_key1); + + let listener_address2: NetAddress = "127.0.0.1:32776".parse().unwrap(); + let secret_key2 = CommsSecretKey::random(&mut rng); + let public_key2 = CommsPublicKey::from_secret_key(&secret_key2); + + let db_name1 = "test_rpc_text_message_service1.sqlite3"; + let db_path1 = get_path(Some(db_name1)); + init_sql_database(db_name1); + + let db_name2 = "test_rpc_text_message_service2.sqlite3"; + let db_path2 = get_path(Some(db_name2)); + init_sql_database(db_name2); + + let config1 = WalletConfig { + comms: CommsConfig { + control_service: ControlServiceConfig { + listener_address: listener_address1.clone(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(5000), + }, + socks_proxy_address: None, + host: "127.0.0.1".parse().unwrap(), + public_key: public_key1.clone(), + secret_key: secret_key1, + public_address: listener_address1.clone(), + datastore_path: TempDir::new(random_string(8).as_str()) + .unwrap() + .path() + .to_str() + .unwrap() + .to_string(), + peer_database_name: random_string(8), + }, + public_key: public_key1.clone(), + database_path: db_path1, + }; + + let config2 = WalletConfig { + comms: CommsConfig { + control_service: ControlServiceConfig { + listener_address: listener_address2.clone(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(5000), + }, + socks_proxy_address: None, + host: "127.0.0.1".parse().unwrap(), + public_key: public_key2.clone(), + secret_key: secret_key2, + public_address: listener_address2.clone(), + datastore_path: TempDir::new(random_string(8).as_str()) + .unwrap() + .path() + .to_str() + .unwrap() + .to_string(), + peer_database_name: random_string(8), + }, + public_key: public_key2.clone(), + database_path: db_path2, + }; + + let wallet1 = Wallet::new(config1).unwrap(); + + thread::spawn(move || { + let wallet_server = WalletServer::new(WALLET_GRPC_PORT, Arc::new(wallet1)); + let _ = wallet_server.start().unwrap(); + }); + + let screen_name = "Bob".to_string(); + let bob_contact = ContactRpc { + screen_name: screen_name.clone(), + pub_key: public_key2.to_hex(), + address: format!("{}", listener_address2.clone()), + }; + + add_contact(bob_contact.clone()); + + thread::sleep(Duration::from_millis(100)); + + let wallet2 = Wallet::new(config2).unwrap(); + + wallet2 + .comms_services + .peer_manager() + .add_peer(create_peer(public_key1.clone(), listener_address1.clone())) + .unwrap(); + let alice_contact = Contact { + screen_name: "Alice".to_string(), + pub_key: public_key1.clone(), + address: listener_address1.clone(), + }; + wallet2.text_message_service.add_contact(alice_contact).unwrap(); + + let test_msg = TextMessageToSendRpc { + dest_pub_key: public_key2.clone().to_hex(), + message: "Hey!".to_string(), + }; + + let test_msg2 = TextMessageToSendRpc { + dest_pub_key: public_key2.clone().to_hex(), + message: "Hoh!".to_string(), + }; + + let resp = RpcResponse { + success: true, + message: "Text Message Sent".to_string(), + }; + + send_text_message_request(test_msg, resp.clone()); + send_text_message_request(test_msg2, resp); + + wallet2 + .text_message_service + .send_text_message(public_key1.clone(), "Here we go!".to_string()) + .unwrap(); + + let sent_messages = vec!["Hey!".to_string(), "Hoh!".to_string()]; + let received_messages = vec!["Here we go!".to_string()]; + + get_text_messages_request(sent_messages.clone(), received_messages.clone(), None); + get_text_messages_request(sent_messages, received_messages, Some(bob_contact)); + set_get_screen_name("Alice".to_string()); + get_pub_key(public_key1.to_hex()); + contacts_crud(); + clean_up_sql_database(db_name1); + clean_up_sql_database(db_name2); +} diff --git a/base_layer/blockchain/Cargo.toml b/base_layer/blockchain/Cargo.toml deleted file mode 100644 index fee19df47b..0000000000 --- a/base_layer/blockchain/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "blockchain" -version = "0.0.1" -edition = "2018" - -[dependencies] -tari_core = { path = "../core"} -derive-error = "0.0.4" diff --git a/base_layer/core/Cargo.toml b/base_layer/core/Cargo.toml index 11e9789935..8a1d869f75 100644 --- a/base_layer/core/Cargo.toml +++ b/base_layer/core/Cargo.toml @@ -6,21 +6,32 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.0.1" +version = "0.0.5" edition = "2018" [dependencies] -tari_utilities = { version = "0.0.1" } +tari_utilities = { path = "../../infrastructure/tari_util", version = "^0.0", features = ["chrono_dt"]} +tari_infra_derive = { path = "../../infrastructure/derive", version = "^0.0" } +tari_crypto = { path = "../../infrastructure/crypto", version = "^0.0" } +tari_p2p = {path = "../../base_layer/p2p", version = "^0.0"} +tari_storage = { path = "../../infrastructure/storage", version = "^0.0" } +tari_comms = { version = "^0.0", path = "../../comms"} +tari_mmr = { path = "../../base_layer/mmr", version = "^0.0" } bitflags = "1.0.4" -chrono = "0.4.6" -tari_infra_derive = { version = "0.0.1" } +chrono = { version = "0.4.6", features = ["serde"]} digest = "0.8.0" -tari_crypto = { version = "0.0.1" } -curve25519-dalek = "1.0.2" derive-error = "0.0.4" rand = "0.5.5" -serde = "1.0.89" -serde_derive = "1.0.89" +serde = { version = "1.0.97", features = ["derive"] } rmp-serde = "0.13.7" base64 = "0.10.1" serde_json = "1.0" +lazy_static = "1.3.0" +newtype-ops = "0.1.4" +arrayref = "0.3.5" +bincode = "1.1.4" +log = "0.4" +blake2 = "^0.8.0" +bigint = "^4.4.1" +ttl_cache = "0.5.1" +croaring = "^0.4.0" diff --git a/base_layer/core/src/base_node/base_node.rs b/base_layer/core/src/base_node/base_node.rs new file mode 100644 index 0000000000..0e855854b7 --- /dev/null +++ b/base_layer/core/src/base_node/base_node.rs @@ -0,0 +1,25 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// `BaseNode` is the highest-level struct of the Tari full node implementation. `BaseNode` collects all the +/// sub-pieces of the Tari blockchain together, and exposes a unified API using a futures-based request-response model +pub struct BaseNode {} diff --git a/base_layer/core/src/base_node/block_validation_service.rs b/base_layer/core/src/base_node/block_validation_service.rs new file mode 100644 index 0000000000..7183d94666 --- /dev/null +++ b/base_layer/core/src/base_node/block_validation_service.rs @@ -0,0 +1,23 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub struct BlockValidationService; diff --git a/base_layer/core/src/base_node/mod.rs b/base_layer/core/src/base_node/mod.rs new file mode 100644 index 0000000000..4a31da4fc2 --- /dev/null +++ b/base_layer/core/src/base_node/mod.rs @@ -0,0 +1,41 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! The Tari base node implementation. +//! +//! Base nodes are the key pieces of infrastructure that maintain the security and integrity of the Tari +//! cryptocurrency. The role of the base node is to provide the following services: +//! * New transaction validation +//! * New block validation +//! * Chain synchronisation service +//! * A gRPC API exposing metrics and data about the blockchain state +//! +//! More details about the implementation are presented in +//! [RFC-0111](https://rfc.tari.com/RFC-0111_BaseNodeArchitecture.html). + +mod base_node; +// mod block_validation_service; +// mod synchronisation_service; +// mod transaction_validation_service; + +// Public re-exports +pub use base_node::BaseNode; diff --git a/base_layer/core/src/base_node/transaction_validation_service.rs b/base_layer/core/src/base_node/transaction_validation_service.rs new file mode 100644 index 0000000000..48a5ca80d3 --- /dev/null +++ b/base_layer/core/src/base_node/transaction_validation_service.rs @@ -0,0 +1,23 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub struct TransactionValidationService; diff --git a/base_layer/core/src/block.rs b/base_layer/core/src/block.rs deleted file mode 100644 index f6fdef8345..0000000000 --- a/base_layer/core/src/block.rs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2018 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, -// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. - -use crate::{ - blockheader::BlockHeader, - transaction::{TransactionError, TransactionInput, TransactionKernel, TransactionOutput}, -}; - -/// A Tari block. Blocks are linked together into a blockchain. -pub struct Block { - pub header: BlockHeader, - pub body: AggregateBody, -} - -/// The components of the block or transaction. The same struct can be used for either, since in Mimblewimble, -/// cut-through means that blocks and transactions have the same structure. -pub struct AggregateBody { - /// List of inputs spent by the transaction. - pub inputs: Vec, - /// List of outputs the transaction produces. - pub outputs: Vec, - /// Kernels contain the excesses and their signatures for transaction - pub kernels: Vec, -} - -impl AggregateBody { - /// Create an empty aggregate body - pub fn empty() -> AggregateBody { - AggregateBody { - inputs: vec![], - outputs: vec![], - kernels: vec![], - } - } - - /// Create a new aggregate body from provided inputs, outputs and kernels - pub fn new( - inputs: Vec, - outputs: Vec, - kernels: Vec, - ) -> AggregateBody - { - AggregateBody { - inputs, - outputs, - kernels, - } - } - - /// Add an input to the existing aggregate body - pub fn add_input(mut self, input: TransactionInput) -> AggregateBody { - self.inputs.push(input); - self.inputs.sort(); - self - } - - /// Add a series of inputs to the existing aggregate body - pub fn add_inputs(mut self, mut inputs: Vec) -> AggregateBody { - self.inputs.append(&mut inputs); - self.inputs.sort(); - self - } - - /// Add an output to the existing aggregate body - pub fn add_output(mut self, output: TransactionOutput) -> AggregateBody { - self.outputs.push(output); - self.outputs.sort(); - self - } - - /// Add an output to the existing aggregate body - pub fn add_outputs(mut self, mut outputs: Vec) -> AggregateBody { - self.outputs.append(&mut outputs); - self.outputs.sort(); - self - } - - /// Add a kernel to the existing aggregate body - pub fn add_kernel(mut self, kernel: TransactionKernel) -> AggregateBody { - self.kernels.push(kernel); - self.kernels.sort(); - self - } - - /// Set the kernel of the aggregate body, replacing any previous kernels - pub fn set_kernel(mut self, kernel: TransactionKernel) -> AggregateBody { - self.kernels = vec![kernel]; - self - } - - /// Sort the component lists of the aggregate body - pub fn sort(&mut self) { - self.inputs.sort(); - self.outputs.sort(); - self.kernels.sort(); - } - - /// Verify the signatures in all kernels contained in this aggregate body - pub fn verify_kernel_signatures(&self) -> Result<(), TransactionError> { - for kernel in self.kernels.iter() { - kernel.verify_signature()?; - } - Ok(()) - } -} diff --git a/base_layer/core/src/blockchain/chain.rs b/base_layer/core/src/blockchain/chain.rs new file mode 100644 index 0000000000..1c95d78c3a --- /dev/null +++ b/base_layer/core/src/blockchain/chain.rs @@ -0,0 +1,184 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! This file provides the structs and functions that persist the block chain state, and re-org logic. Because a re-org +//! can happen, we need to keep track of orphan blocks. The Merkle Mountain Range crate we use allows us to rewind +//! checkpoints. Internally it keeps track of what was changed between checkpoints. We use these rewind blocks in the +//! case where we need to do a re-org where a forked chain with a greater accumulated pow emerges. In the case of the +//! MMR, a checkpoint is equal to a block. +//! +//! The MMR also provides a method to save the MMR to disc. This is internally handled and we use LMDB to store the MMR. + +use crate::{ + blockchain::{ + block_chain_state::BlockchainState, + error::{ChainError, StateError}, + }, + blocks::block::Block, + consensus::ConsensusRules, +}; +use std::collections::HashMap; +use tari_utilities::Hashable; + +type BlockHash = [u8; 32]; +pub const MAX_ORPHAN_AGE: u64 = 1000; + +/// The Chain is the actual data structure to represent the blockchain +pub struct Chain { + /// This the the current UTXO set, kernels and headers + pub block_chain_state: BlockchainState, + /// This is all valid blocks which dont have a parent trace to the genesis block + orphans: HashMap, + /// The current head's total proof of work + pub current_total_pow: ProofOfWork, +} + +impl Chain { + + /// Adds a new block to the persistent state. + /// + /// This process includes: + /// * Adds the range proof hashes to the range proof MMR + /// * Marks all inputs in the block as spent + /// * Adds all outputs to the UTXO set and MMR + /// * Adds the transaction kernels to the kernel MMR + /// * Adds the block header to the header MMR + /// An error is returned if: + /// * the block has already been added (by checking whether the block header hash exists already) + /// * any of the inputs were not in the UTXO set or were marked as spent already + /// * There was a problem creating the checkpoints + pub fn add_new_block(&mut self, new_block: &Block) -> Result<(), StateError> { + if let Some(_) = self.headers.get_object(&new_block.header.hash()) { + return Err(StateError::DuplicateBlock); + } + // We add the range proofs just to strip them out again 2 lines later??? + // TODO Clearly there's an optimisation here somewhere + for output in &new_block.body.outputs { + self.range_proofs.push(output.proof().clone())?; + } + self.mark_outputs_as_spent(&new_block.body.inputs)?; + self.strip_range_proofs(&new_block.body.outputs); + // All seems valid, lets add the objects to the state + for output in &new_block.body.outputs { + self.utxos.push(output.clone().into())?; + } + self.kernels.append(new_block.body.kernels.clone())?; + self.headers.push(new_block.header.clone())?; + // Apply a new checkpoint so that we can rewind to this point in the future if necessary + self.check_point_state() + } + + + /// This function will process a newly received block + pub fn add_new_block(&mut self, new_block: Block, consensus_rules: &ConsensusRules) -> Result<(), ChainError> { + let result = match self.block_chain_state.add_new_block(&new_block) { + // block was processed fine and added to chain + Ok(_) => { + let height = new_block.header.height; + self.current_total_pow = new_block.header.pow; + self.orphans.retain(|_, b| height - b.header.height < MAX_ORPHAN_AGE); + // todo search for new orphans that we might apply + Ok(()) + }, + // block seems valid, but its orphaned + Err(StateError::OrphanBlock) => self.orphaned_block(new_block, consensus_rules), + Err(e) => Err(ChainError::StateProcessingError(e)), + }; + if result.is_err() { + self.block_chain_state.reset_chain_state()?; + } + result + } + + /// Internal helper function to do orphan logic + /// The function will store the new orphan block and check if it contains a re-org + fn orphaned_block(&mut self, new_block: Block) -> Result<(), ChainError> { + if self.orphans.contains_key(&new_block.header.hash()[..]) { + return Err(ChainError::StateProcessingError(StateError::DuplicateBlock)); + }; + let mut hash = [0; 32]; + hash.copy_from_slice(&new_block.header.hash()); + let pow = new_block.header.pow.clone(); + self.orphans.insert(hash, new_block); + let mut currently_used_orphans: Vec = Vec::new(); + if self.current_total_pow.has_more_accum_work_than(&pow) { + // we have a potential re-org here + let result = self.handle_re_org(&hash, &mut currently_used_orphans); + let result = if result.is_err() { + self.block_chain_state.reset_chain_state()?; + result + } else { + for hash in ¤tly_used_orphans { + self.orphans.remove(hash); + } + self.current_total_pow = pow; + self.block_chain_state + .save_state() + .map_err(ChainError::StateProcessingError) + }; + result + } else { + Err(ChainError::StateProcessingError(StateError::OrphanBlock)) + } + } + + /// Internal recursive function to handle re orgs + /// The function will go and search for a known parent and if found, will apply the list of orphan blocks to re-org + fn handle_re_org( + &mut self, + block_hash: &BlockHash, + mut unorphaned_blocks: &mut Vec, + ) -> Result<(), ChainError> + { + // The searched hash should always be in the orphan list + unorphaned_blocks.push(block_hash.clone()); // save all orphans we have used + let block = &self.orphans[block_hash].clone(); + let parent = self.block_chain_state.headers.get_object(&block.header.prev_hash); + if parent.is_some() { + // we know of parent so we can re-org + let h = parent.unwrap().height; + self.block_chain_state + .rewind_state((self.block_chain_state.get_tip_height() - h) as usize)?; + return self + .block_chain_state + .add_new_block(&block) + .map_err(ChainError::StateProcessingError); + }; + let prev_block = self.orphans.get(&block.header.prev_hash); + if prev_block.is_none() { + return Err(ChainError::StateProcessingError(StateError::OrphanBlock)); + }; + + let mut hash = [0; 32]; + hash.copy_from_slice(&prev_block.unwrap().header.hash()); + let result = self.handle_re_org(&hash, &mut unorphaned_blocks); + + if result.is_ok() { + return self + .block_chain_state + .add_new_block(&block) + .map_err(ChainError::StateProcessingError); + } + + result + } +} diff --git a/base_layer/core/src/blockheader.rs b/base_layer/core/src/blockchain/error.rs similarity index 57% rename from base_layer/core/src/blockheader.rs rename to base_layer/core/src/blockchain/error.rs index d58ea2d4f1..34313e3327 100644 --- a/base_layer/core/src/blockheader.rs +++ b/base_layer/core/src/blockchain/error.rs @@ -1,4 +1,4 @@ -// Copyright 2018 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -23,37 +23,36 @@ // Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, // Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. -use crate::{pow::ProofOfWork, types::BlindingFactor}; -use chrono::{DateTime, Utc}; +// this file is used for all blockchain error types +use crate::blocks::block::BlockValidationError; +use derive_error::Error; +use merklemountainrange::{error::MerkleMountainRangeError, merkle_storage::MerkleStorageError}; +use tari_storage::keyvalue_store::*; -type BlockHash = [u8; 32]; - -/// The BlockHeader contains all the metadata for the block, including proof of work, a link to the previous block -/// and the transaction kernels. -pub struct BlockHeader { - /// Version of the block - pub version: u16, - /// Height of this block since the genesis block (height 0) - pub height: u64, - /// Hash of the block previous to this in the chain. - pub prev_hash: BlockHash, - /// Timestamp at which the block was built. - pub timestamp: DateTime, - /// This is the MMR root of the outputs - pub output_mmr: BlockHash, - /// This is the MMR root of the kernels - pub kernel_mmr: BlockHash, - /// Total accumulated sum of kernel offsets since genesis block. We can derive the kernel offset sum for *this* - /// block from the total kernel offset of the previous block header. - pub total_kernel_offset: BlindingFactor, - /// Nonce used - /// Proof of work summary - pub pow: ProofOfWork, +/// The ChainError is used to present all generic chain error of the actual blockchain +#[derive(Debug, Error)] +pub enum ChainError { + // Could not initialise state + InitStateError(DatastoreError), + // Some kind of processing error in the state + StateProcessingError(StateError), } -impl BlockHeader { - /// This function will validate the proof of work in the header - pub fn validate_pow(&self) -> bool { - unimplemented!(); - } +/// The chainstate is used to present all generic chain error of the actual blockchain state +#[derive(Debug, Error)] +pub enum StateError { + // could not create a database + StoreError(DatastoreError), + // MerklestorageError + StorageError(MerkleStorageError), + // Unkown commitment spent + SpentUnknownCommitment(MerkleMountainRangeError), + // provided mmr states in headers mismatch + HeaderStateMismatch, + // block is not correctly constructed + InvalidBlock(BlockValidationError), + // block is orphaned + OrphanBlock, + // Duplicate block + DuplicateBlock, } diff --git a/base_layer/core/src/blocks/aggregated_body.rs b/base_layer/core/src/blocks/aggregated_body.rs new file mode 100644 index 0000000000..df51440de8 --- /dev/null +++ b/base_layer/core/src/blocks/aggregated_body.rs @@ -0,0 +1,208 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + blocks::block::KernelSum, + tari_amount::*, + transaction::*, + types::{BlindingFactor, Commitment, CommitmentFactory, PrivateKey, RangeProofService, COMMITMENT_FACTORY}, +}; +use serde::{Deserialize, Serialize}; +use tari_crypto::{commitment::HomomorphicCommitmentFactory, ristretto::pedersen::PedersenCommitment}; + +/// The components of the block or transaction. The same struct can be used for either, since in Mimblewimble, +/// cut-through means that blocks and transactions have the same structure. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct AggregateBody { + sorted: bool, + /// List of inputs spent by the transaction. + pub inputs: Vec, + /// List of outputs the transaction produces. + pub outputs: Vec, + /// Kernels contain the excesses and their signatures for transaction + pub kernels: Vec, +} + +impl AggregateBody { + /// Create an empty aggregate body + pub fn empty() -> AggregateBody { + AggregateBody { + sorted: false, + inputs: vec![], + outputs: vec![], + kernels: vec![], + } + } + + /// Create a new aggregate body from provided inputs, outputs and kernels + pub fn new( + inputs: Vec, + outputs: Vec, + kernels: Vec, + ) -> AggregateBody + { + AggregateBody { + sorted: false, + inputs, + outputs, + kernels, + } + } + + /// Add an input to the existing aggregate body + pub fn add_input(&mut self, input: TransactionInput) { + self.inputs.push(input); + self.sorted = false; + } + + /// Add a series of inputs to the existing aggregate body + pub fn add_inputs(&mut self, inputs: &mut Vec) { + self.inputs.append(inputs); + self.sorted = false; + } + + /// Add an output to the existing aggregate body + pub fn add_output(&mut self, output: TransactionOutput) { + self.outputs.push(output); + self.sorted = false; + } + + /// Add an output to the existing aggregate body + pub fn add_outputs(&mut self, outputs: &mut Vec) { + self.outputs.append(outputs); + self.sorted = false; + } + + /// Add a kernel to the existing aggregate body + pub fn add_kernel(&mut self, kernel: TransactionKernel) { + self.kernels.push(kernel); + } + + /// Set the kernel of the aggregate body, replacing any previous kernels + pub fn set_kernel(&mut self, kernel: TransactionKernel) { + self.kernels = vec![kernel]; + } + + /// Sort the component lists of the aggregate body + pub fn sort(&mut self) { + if self.sorted { + return; + } + self.inputs.sort(); + self.outputs.sort(); + self.kernels.sort(); + self.sorted = true; + } + + /// Verify the signatures in all kernels contained in this aggregate body. Clients must provide an offset that + /// will be added to the public key used in the signature verification. + pub fn verify_kernel_signatures(&self) -> Result<(), TransactionError> { + for kernel in self.kernels.iter() { + kernel.verify_signature()?; + } + Ok(()) + } + + pub fn get_total_fee(&self) -> MicroTari { + let mut fee = MicroTari::from(0); + for kernel in &self.kernels { + fee += kernel.fee; + } + fee + } + + /// Validate this transaction by checking the following: + /// 1. The sum of inputs, outputs and fees equal the (public excess value + offset) + /// 1. The signature signs the canonical message with the private excess + /// 1. Range proofs of the outputs are valid + /// + /// This function does NOT check that inputs come from the UTXO set + /// The reward is the amount of Tari rewarded for this block, this should be 0 for a transaction + pub fn validate_internal_consistency( + &self, + offset: &BlindingFactor, + reward: MicroTari, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result<(), TransactionError> + { + let total_offset = COMMITMENT_FACTORY.commit_value(&offset, reward.0); + self.verify_kernel_signatures()?; + self.validate_kernel_sum(total_offset, factory)?; + self.validate_range_proofs(prover) + } + + /// Calculate the sum of the inputs and outputs including fees + fn sum_commitments(&self, fees: u64, factory: &CommitmentFactory) -> Commitment { + let fee_commitment = factory.commit_value(&PrivateKey::default(), fees); + let sum_inputs = &self.inputs.iter().map(|i| &i.commitment).sum::(); + let sum_outputs = &self.outputs.iter().map(|o| &o.commitment).sum::(); + &(sum_outputs - sum_inputs) + &fee_commitment + } + + /// Calculate the sum of the kernels, taking into account the provided offset, and their constituent fees + fn sum_kernels(&self, offset: PedersenCommitment) -> KernelSum { + // Sum all kernel excesses and fees + self.kernels.iter().fold( + KernelSum { + fees: MicroTari(0), + sum: offset, + }, + |acc, val| KernelSum { + fees: &acc.fees + &val.fee, + sum: &acc.sum + &val.excess, + }, + ) + } + + /// Confirm that the (sum of the outputs) - (sum of inputs) = Kernel excess + fn validate_kernel_sum(&self, offset: Commitment, factory: &CommitmentFactory) -> Result<(), TransactionError> { + let kernel_sum = self.sum_kernels(offset); + let sum_io = self.sum_commitments(kernel_sum.fees.into(), factory); + + if kernel_sum.sum != sum_io { + return Err(TransactionError::ValidationError( + "Sum of inputs and outputs did not equal sum of kernels with fees".into(), + )); + } + + Ok(()) + } + + fn validate_range_proofs(&self, range_proof_service: &RangeProofService) -> Result<(), TransactionError> { + for o in &self.outputs { + if !o.verify_range_proof(&range_proof_service)? { + return Err(TransactionError::ValidationError( + "Range proof could not be verified".into(), + )); + } + } + Ok(()) + } +} + +/// This will strip away the offset of the transaction returning a pure aggregate body +impl From for AggregateBody { + fn from(transaction: Transaction) -> Self { + transaction.body + } +} diff --git a/base_layer/core/src/blocks/block.rs b/base_layer/core/src/blocks/block.rs new file mode 100644 index 0000000000..596dc3daa2 --- /dev/null +++ b/base_layer/core/src/blocks/block.rs @@ -0,0 +1,244 @@ +// Copyright 2018 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, +// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. +use crate::{ + blocks::{aggregated_body::AggregateBody, BlockHeader}, + consensus::ConsensusRules, + proof_of_work::PowError, + tari_amount::*, + transaction::*, + types::{Commitment, TariProofOfWork, COMMITMENT_FACTORY, PROVER}, +}; +use derive_error::Error; +use serde::{Deserialize, Serialize}; +use tari_utilities::Hashable; + +#[derive(Clone, Debug, PartialEq, Error)] +pub enum BlockValidationError { + // A transaction in the block failed to validate + TransactionError(TransactionError), + // Invalid Proof of work for the block + ProofOfWorkError(PowError), + // Invalid kernel in block + InvalidKernel, + // Invalid input in block + InvalidInput, + // Input maturity not reached + InputMaturity, + // Invalid coinbase maturity in block or more than one coinbase + InvalidCoinbase, +} + +/// A Tari block. Blocks are linked together into a blockchain. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Block { + pub header: BlockHeader, + pub body: AggregateBody, +} + +impl Block { + /// This function will check the block to ensure that all UTXO's are validly constructed and that all signatures are + /// valid. It does _not_ check that the inputs exist in the current UTXO set; + /// nor does it check that the PoW is the largest accumulated PoW value. + pub fn check_internal_consistency(&self, rules: &ConsensusRules) -> Result<(), BlockValidationError> { + let block_reward = rules.emission_schedule().block_reward(self.header.height); + let offset = &self.header.total_kernel_offset; + let total_coinbase = self.calculate_coinbase_and_fees(block_reward); + self.body + .validate_internal_consistency(&offset, total_coinbase, &PROVER, &COMMITMENT_FACTORY)?; + self.check_stxo_rules()?; + self.check_utxo_rules(rules)?; + self.check_pow() + } + + // create a total_coinbase offset containing all fees for the validation + fn calculate_coinbase_and_fees(&self, block_reward: MicroTari) -> MicroTari { + let mut coinbase = block_reward; + for kernel in &self.body.kernels { + coinbase += kernel.fee; + } + coinbase + } + + pub fn check_pow(&self) -> Result<(), BlockValidationError> { + Ok(()) + } + + /// This function will check spent kernel rules like tx lock height etc + pub fn check_kernel_rules(&self) -> Result<(), BlockValidationError> { + for kernel in &self.body.kernels { + if kernel.lock_height > self.header.height { + return Err(BlockValidationError::InvalidKernel); + } + } + Ok(()) + } + + /// This function will check all new utxo to ensure that feature flags where set + pub fn check_utxo_rules(&self, current_rules: &ConsensusRules) -> Result<(), BlockValidationError> { + let mut coinbase_counter = 0; // there should be exactly 1 coinbase + for utxo in &self.body.outputs { + if utxo.features.flags.contains(OutputFlags::COINBASE_OUTPUT) { + coinbase_counter += 1; + if utxo.features.maturity < (self.header.height + current_rules.coinbase_lock_height()) { + return Err(BlockValidationError::InvalidCoinbase); + } + } + } + if coinbase_counter != 1 { + return Err(BlockValidationError::InvalidCoinbase); + } + Ok(()) + } + + /// This function will check all stxo to ensure that feature flags where followed + pub fn check_stxo_rules(&self) -> Result<(), BlockValidationError> { + for input in &self.body.inputs { + if input.features.maturity > self.header.height { + return Err(BlockValidationError::InputMaturity); + } + } + Ok(()) + } + + /// Destroys the block and returns the pieces of the block: header, inputs, outputs and kernels + pub fn dissolve( + self, + ) -> ( + BlockHeader, + Vec, + Vec, + Vec, + ) { + (self.header, self.body.inputs, self.body.outputs, self.body.kernels) + } +} + +pub struct BlockBuilder { + pub header: BlockHeader, + pub inputs: Vec, + pub outputs: Vec, + pub kernels: Vec, + pub total_fee: MicroTari, +} + +impl BlockBuilder { + pub fn new() -> BlockBuilder { + BlockBuilder { + header: BlockHeader::new(ConsensusRules::current().blockchain_version()), + inputs: Vec::new(), + outputs: Vec::new(), + kernels: Vec::new(), + total_fee: MicroTari::from(0), + } + } + + /// This function adds a header to the block + pub fn with_header(mut self, header: BlockHeader) -> Self { + self.header = header; + self + } + + /// This function adds the provided transaction inputs to the block + pub fn add_inputs(mut self, mut inputs: Vec) -> Self { + self.inputs.append(&mut inputs); + self + } + + /// This function adds the provided transaction outputs to the block + pub fn add_outputs(mut self, mut outputs: Vec) -> Self { + self.outputs.append(&mut outputs); + self + } + + /// This function adds the provided transaction kernels to the block + pub fn add_kernels(mut self, mut kernels: Vec) -> Self { + for kernel in &kernels { + self.total_fee += kernel.fee; + } + self.kernels.append(&mut kernels); + self + } + + /// This functions add the provided transactions to the block + pub fn with_transactions(mut self, txs: Vec) -> Self { + let iter = txs.into_iter(); + for tx in iter { + self = self.add_inputs(tx.body.inputs); + self = self.add_outputs(tx.body.outputs); + self = self.add_kernels(tx.body.kernels); + self.header.total_kernel_offset = self.header.total_kernel_offset + tx.offset; + } + self + } + + /// This functions add the provided transactions to the block + pub fn add_transaction(mut self, tx: Transaction) -> Self { + self = self.add_inputs(tx.body.inputs); + self = self.add_outputs(tx.body.outputs); + self = self.add_kernels(tx.body.kernels); + self.header.total_kernel_offset = &self.header.total_kernel_offset + &tx.offset; + self + } + + /// This will add the given coinbase UTXO to the block + pub fn with_coinbase_utxo(mut self, coinbase_utxo: TransactionOutput, coinbase_kernel: TransactionKernel) -> Self { + self.kernels.push(coinbase_kernel); + self.outputs.push(coinbase_utxo); + self + } + + /// This will finish construction of the block and create the block + pub fn build(self) -> Block { + let mut block = Block { + header: self.header, + body: AggregateBody::new(self.inputs, self.outputs, self.kernels), + }; + block.body.sort(); + block + } + + /// Add the provided ProofOfWork to the block + pub fn with_pow(self, _pow: TariProofOfWork) -> Self { + // TODO + self + } +} + +/// This struct holds the result of calculating the sum of the kernels in a Transaction +/// and returns the summed commitments and the total fees +pub struct KernelSum { + pub sum: Commitment, + pub fees: MicroTari, +} + +impl Hashable for Block { + /// The block hash is just the header hash, since the inputs, outputs and range proofs are captured by their + /// respective MMR roots in the header itself. + fn hash(&self) -> Vec { + self.header.hash() + } +} + +//---------------------------------------- Tests ----------------------------------------------------// diff --git a/base_layer/core/src/blocks/blockheader.rs b/base_layer/core/src/blocks/blockheader.rs new file mode 100644 index 0000000000..499f859ee9 --- /dev/null +++ b/base_layer/core/src/blocks/blockheader.rs @@ -0,0 +1,172 @@ +// Copyright 2018 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, +// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. + +//! Blockchain state +//! +//! For [technical reasons](https://www.tari.com/2019/07/15/tari-protocol-discussion-42.html), the commitment in +//! block headers commits to the entire TXO set, rather than just UTXOs using a merkle mountain range. +//! However, it's really important to commit to the actual UTXO set at a given height and have the ability to +//! distinguish between spent and unspent outputs in the MMR. +//! +//! To solve this we commit to the MMR root in the header of each block. This will give as an immutable state at the +//! given height. But this does not provide us with a UTXO, only TXO set. To identify UTXOs we create a roaring bit map +//! of all the UTXO's positions inside of the MMR leaves. We hash this, and combine it with the MMR root, to provide us +//! with a TXO set that will represent the UTXO state of the chain at the given height: +//! state = Hash(Hash(mmr_root)|| Hash(roaring_bitmap)) +//! This hash is called the UTXO merkle root, and is used as the output_mr + +use crate::{ + proof_of_work::Difficulty, + types::{BlindingFactor, HashDigest, TariProofOfWork}, +}; +use chrono::{DateTime, NaiveDate, Utc}; +use digest::Digest; +use serde::{ + de::{self, Visitor}, + Deserialize, + Deserializer, + Serialize, + Serializer, +}; +use std::fmt; +use tari_utilities::{ByteArray, Hashable}; + +pub type BlockHash = Vec; + +/// The BlockHeader contains all the metadata for the block, including proof of work, a link to the previous block +/// and the transaction kernels. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct BlockHeader { + /// Version of the block + pub version: u16, + /// Height of this block since the genesis block (height 0) + pub height: u64, + /// Hash of the block previous to this in the chain. + #[serde(with = "hash_serializer")] + pub prev_hash: BlockHash, + /// Timestamp at which the block was built. + pub timestamp: DateTime, + /// This is the UTXO merkle root of the outputs + /// This is calculated as Hash (txo MMR root || roaring bitmap hash of UTXO indices) + #[serde(with = "hash_serializer")] + pub output_mr: BlockHash, + /// This is the MMR root of the range proofs + #[serde(with = "hash_serializer")] + pub range_proof_mr: BlockHash, + /// This is the MMR root of the kernels + #[serde(with = "hash_serializer")] + pub kernel_mr: BlockHash, + /// Total accumulated sum of kernel offsets since genesis block. We can derive the kernel offset sum for *this* + /// block from the total kernel offset of the previous block header. + pub total_kernel_offset: BlindingFactor, + /// Total accumulated difficulty since genesis block + pub total_difficulty: Difficulty, + /// Nonce increment used to mine this block. + pub nonce: u64, + /// Proof of work summary + pub pow: TariProofOfWork, +} + +impl BlockHeader { + pub fn new(blockchain_version: u16) -> BlockHeader { + BlockHeader { + version: blockchain_version, + height: 0, + prev_hash: vec![0; 32], + timestamp: DateTime::::from_utc(NaiveDate::from_ymd(2000, 1, 1).and_hms(1, 1, 1), Utc), + output_mr: vec![0; 32], + range_proof_mr: vec![0; 32], + kernel_mr: vec![0; 32], + total_kernel_offset: BlindingFactor::default(), + total_difficulty: Difficulty::default(), + nonce: 0, + pow: TariProofOfWork::default(), + } + } +} + +impl Hashable for BlockHeader { + fn hash(&self) -> Vec { + HashDigest::new() + .chain(self.version.to_le_bytes()) + .chain(self.height.to_le_bytes()) + .chain(self.prev_hash.as_bytes()) + .chain(self.timestamp.timestamp().to_le_bytes()) + .chain(self.output_mr.as_bytes()) + .chain(self.range_proof_mr.as_bytes()) + .chain(self.kernel_mr.as_bytes()) + .chain(self.total_kernel_offset.as_bytes()) + .chain(self.pow.as_bytes()) + .result() + .to_vec() + } +} + +impl PartialEq for BlockHeader { + fn eq(&self, other: &Self) -> bool { + self.hash() == other.hash() + } +} + +impl Eq for BlockHeader {} + +mod hash_serializer { + use super::*; + use tari_utilities::hex::Hex; + + pub fn serialize(bytes: &BlockHash, serializer: S) -> Result + where S: Serializer { + if serializer.is_human_readable() { + bytes.to_hex().serialize(serializer) + } else { + serializer.serialize_bytes(bytes.as_bytes()) + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where D: Deserializer<'de> { + struct BlockHashVisitor; + + impl<'de> Visitor<'de> for BlockHashVisitor { + type Value = BlockHash; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("A block header hash in binary format") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where E: de::Error { + BlockHash::from_bytes(v).map_err(E::custom) + } + } + + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + BlockHash::from_hex(&s).map_err(de::Error::custom) + } else { + deserializer.deserialize_bytes(BlockHashVisitor) + } + } +} diff --git a/base_layer/core/src/blocks/genesis_block.rs b/base_layer/core/src/blocks/genesis_block.rs new file mode 100644 index 0000000000..6fe3d1c8e1 --- /dev/null +++ b/base_layer/core/src/blocks/genesis_block.rs @@ -0,0 +1,62 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, +// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. + +// This file is used to store the genesis block +use crate::blocks::{aggregated_body::AggregateBody, block::Block, BlockBuilder}; + +use crate::{blocks::BlockHeader, types::TariProofOfWork}; +use chrono::{DateTime, NaiveDate, Utc}; +use tari_crypto::ristretto::*; + +pub fn get_genesis_block() -> Block { + let header = get_gen_header(); + BlockBuilder::new().with_header(header).build() +} + +pub fn get_gen_header() -> BlockHeader { + BlockHeader { + version: 0, + /// Height of this block since the genesis block (height 0) + height: 0, + /// Hash of the block previous to this in the chain. + prev_hash: vec![0; 32], + /// Timestamp at which the block was built. + timestamp: DateTime::::from_utc(NaiveDate::from_ymd(2020, 1, 1).and_hms(1, 1, 1), Utc), + /// This is the MMR root of the outputs + output_mr: vec![0; 32], + /// This is the MMR root of the range proofs + range_proof_mr: vec![0; 32], + /// This is the MMR root of the kernels + kernel_mr: vec![0; 32], + /// Total accumulated sum of kernel offsets since genesis block. We can derive the kernel offset sum for *this* + /// block from the total kernel offset of the previous block header. + total_kernel_offset: RistrettoSecretKey::from(0), + /// Proof of work summary + total_difficulty: Default::default(), + /// Nonce used + nonce: 0, + pow: TariProofOfWork::default(), + } +} diff --git a/base_layer/core/src/blocks/mod.rs b/base_layer/core/src/blocks/mod.rs new file mode 100644 index 0000000000..347494b809 --- /dev/null +++ b/base_layer/core/src/blocks/mod.rs @@ -0,0 +1,30 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub(crate) mod aggregated_body; +mod block; +pub(crate) mod blockheader; + +pub mod genesis_block; + +pub use block::{Block, BlockBuilder, BlockValidationError}; +pub use blockheader::BlockHeader; diff --git a/base_layer/core/src/bullet_rangeproofs.rs b/base_layer/core/src/bullet_rangeproofs.rs new file mode 100644 index 0000000000..f1ec3481e9 --- /dev/null +++ b/base_layer/core/src/bullet_rangeproofs.rs @@ -0,0 +1,110 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::types::*; +use digest::Digest; +use serde::{ + de::{self, Visitor}, + Deserialize, + Deserializer, + Serialize, + Serializer, +}; +use std::fmt; +use tari_utilities::{byte_array::*, hash::*, hex::*}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BulletRangeProof(pub Vec); +/// Implement the hashing function for RangeProof for use in the MMR +impl Hashable for BulletRangeProof { + fn hash(&self) -> Vec { + HashDigest::new().chain(&self.0).result().to_vec() + } +} + +impl ByteArray for BulletRangeProof { + fn to_vec(&self) -> Vec { + self.0.clone() + } + + fn from_vec(v: &Vec) -> Result { + Ok(BulletRangeProof { 0: v.clone() }) + } + + fn from_bytes(bytes: &[u8]) -> Result { + Ok(BulletRangeProof { 0: bytes.to_vec() }) + } + + fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl From> for BulletRangeProof { + fn from(v: Vec) -> Self { + BulletRangeProof(v) + } +} + +impl fmt::Display for BulletRangeProof { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + +impl Serialize for BulletRangeProof { + fn serialize(&self, serializer: S) -> Result + where S: Serializer { + if serializer.is_human_readable() { + self.to_hex().serialize(serializer) + } else { + serializer.serialize_bytes(self.as_bytes()) + } + } +} + +impl<'de> Deserialize<'de> for BulletRangeProof { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + struct RangeProofVisitor; + + impl<'de> Visitor<'de> for RangeProofVisitor { + type Value = BulletRangeProof; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a bulletproof range proof in binary format") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where E: de::Error { + BulletRangeProof::from_bytes(v).map_err(E::custom) + } + } + + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + BulletRangeProof::from_hex(&s).map_err(de::Error::custom) + } else { + deserializer.deserialize_bytes(RangeProofVisitor) + } + } +} diff --git a/base_layer/core/src/chain_storage/blockchain_database.rs b/base_layer/core/src/chain_storage/blockchain_database.rs new file mode 100644 index 0000000000..a477aa5d6a --- /dev/null +++ b/base_layer/core/src/chain_storage/blockchain_database.rs @@ -0,0 +1,501 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +use crate::{ + blocks::{Block, BlockBuilder, BlockHeader}, + chain_storage::{ + db_transaction::{DbKey, DbTransaction, DbValue, MetadataKey, MetadataValue, MmrTree}, + error::ChainStorageError, + ChainMetadata, + HistoricalBlock, + }, + proof_of_work::Difficulty, + transaction::{TransactionInput, TransactionKernel, TransactionOutput}, + types::{Commitment, HashOutput}, +}; +use croaring::Bitmap; +use log::*; +use std::sync::{Arc, RwLock, RwLockReadGuard}; +use tari_mmr::{Hash, MerkleCheckPoint, MerkleProof}; +use tari_utilities::Hashable; + +const LOG_TARGET: &str = "core::chain_storage::database"; + +#[derive(Clone, Debug, PartialEq)] +pub enum BlockAddResult { + Ok, + BlockExists, + OrphanBlock, + ChainReorg, +} + +/// Identify behaviour for Blockchain database back ends. Implementations must support `Send` and `Sync` so that +/// `BlockchainDatabase` can be thread-safe. The backend *must* also execute transactions atomically; i.e., every +/// operation within it must succeed, or they all fail. Failure to support this contract could lead to +/// synchronisation issues in your database backend. +/// +/// Data is passed to and from the backend via the [DbKey], [DbValue], and [DbValueKey] enums. This strategy allows +/// us to keep the reading and writing API extremely simple. Extending the types of data that the back ends can handle +/// will entail adding to those enums, and the back ends, while this trait can remain unchanged. +pub trait BlockchainBackend: Send + Sync { + /// Commit the transaction given to the backend. If there is an error, the transaction must be rolled back, and + /// the error condition returned. On success, every operation in the transaction will have been committed, and + /// the function will return `Ok(())`. + fn write(&self, tx: DbTransaction) -> Result<(), ChainStorageError>; + /// Fetch a value from the back end corresponding to the given key. If the value is not found, `get` must return + /// `Ok(None)`. It should only error if there is an access or integrity issue with the underlying back end. + fn fetch(&self, key: &DbKey) -> Result, ChainStorageError>; + /// Checks to see whether the given key exists in the back end. This function should only fail if there is an + /// access or integrity issue with the back end. + fn contains(&self, key: &DbKey) -> Result; + /// Fetches the merklish root for the MMR tree identified by the key. This function should only fail if there is an + /// access or integrity issue with the back end. + fn fetch_mmr_root(&self, tree: MmrTree) -> Result; + /// Constructs a merkle proof for the specified merkle mountain range and the given leaf position. + fn fetch_mmr_proof(&self, tree: MmrTree, pos: u64) -> Result; + /// The nth MMR checkpoint (the list of nodes added & deleted) for the given Merkle tree. The index is the n-th + /// checkpoint (block) from the pruning horizon block. + fn fetch_mmr_checkpoint(&self, tree: MmrTree, index: u64) -> Result; + /// Fetches the leaf node hash and its deletion status for the nth leaf node in the given MMR tree. + fn fetch_mmr_node(&self, tree: MmrTree, pos: u32) -> Result<(Hash, bool), ChainStorageError>; +} + +// Private macro that pulls out all the boiler plate of extracting a DB query result from its variants +macro_rules! fetch { + ($self:ident, $key_val:expr, $key_var:ident) => {{ + let key = DbKey::$key_var($key_val); + match $self.db.fetch(&key) { + Ok(None) => Err(ChainStorageError::ValueNotFound(key)), + Ok(Some(DbValue::$key_var(k))) => Ok(*k), + Ok(Some(other)) => unexpected_result(key, other), + Err(e) => log_error(key, e), + } + }}; + + (meta $db:expr, $meta_key:ident, $default:expr) => {{ + match $db.fetch(&DbKey::Metadata(MetadataKey::$meta_key)) { + Ok(None) => { + warn!( + target: LOG_TARGET, + "The {} entry is not present in the database. Assuming the database is empty.", + DbKey::Metadata(MetadataKey::$meta_key) + ); + $default + }, + Ok(Some(DbValue::Metadata(MetadataValue::$meta_key(v)))) => v, + Ok(Some(other)) => return unexpected_result(DbKey::Metadata(MetadataKey::$meta_key), other), + Err(e) => return log_error(DbKey::Metadata(MetadataKey::$meta_key), e), + } + }}; +} + +/// A generic blockchain storage mechanism. This struct defines the API for storing and retrieving Tari blockchain +/// components without being opinionated about the actual backend used. +/// +/// `BlockChainDatabase` is thread-safe, since the backend must implement `Sync` and `Send`. +/// +/// You typically don't interact with `BlockChainDatabase` directly, since it doesn't enforce any consensus rules; it +/// only really stores and fetches blockchain components. To create an instance of `BlockchainDatabase', you must +/// provide it with the backend it is going to use; for example, for a memory-backed DB: +/// +/// ``` +/// use tari_core::{ +/// chain_storage::{BlockchainDatabase, MemoryDatabase}, +/// types::HashDigest, +/// }; +/// let db_backend = MemoryDatabase::::default(); +/// let mut db = BlockchainDatabase::new(db_backend).unwrap(); +/// // Do stuff with db +/// ``` +pub struct BlockchainDatabase +where T: BlockchainBackend +{ + metadata: Arc>, + db: Arc, +} + +impl BlockchainDatabase +where T: BlockchainBackend +{ + /// Creates a new `BlockchainDatabase` using the provided backend. + pub fn new(db: T) -> Result { + let metadata = Self::read_metadata(&db)?; + Ok(BlockchainDatabase { + metadata: Arc::new(RwLock::new(metadata)), + db: Arc::new(db), + }) + } + + /// Reads the blockchain metadata (block height etc) from the underlying backend and returns it. + /// If the metadata values aren't in the database, (e.g. when running a node for the first time), + /// then log as much and return a reasonable default. + fn read_metadata(db: &T) -> Result { + let height = fetch!(meta db, ChainHeight, None); + let hash = fetch!(meta db, BestBlock, None); + let work = fetch!(meta db, AccumulatedWork, 0); + // Set a default of 2880 blocks (2 days with 1min blocks) + let horizon = fetch!(meta db, PruningHorizon, 2880); + Ok(ChainMetadata { + height_of_longest_chain: height, + best_block: hash, + total_accumulated_difficulty: work, + pruning_horizon: horizon, + }) + } + + /// If a call to any metadata function fails, you can try and force a re-sync with this function. If the RWLock + /// is poisoned because a write attempt failed, this function will replace the old lock with a new one with data + /// freshly read from the underlying database. If this still fails, there's probably something badly wrong. + /// + /// # Returns + /// Ok(true) - The lock was refreshed and data was successfully re-read from the database. Proceed with caution. + /// The database *may* be inconsistent. + /// Ok(false) - Everything looks fine. Why did you call this function again? + /// Err(ChainStorageError::CriticalError) - Refreshing the lock failed. We couldn't refresh the metadata from the DB + /// backend, so you should probably just shut things down and look at the logs. + pub fn try_recover_metadata(&mut self) -> Result { + if !self.metadata.is_poisoned() { + // metadata is fine. Nothing to do here + return Ok(false); + } + match BlockchainDatabase::read_metadata(self.db.as_ref()) { + Ok(data) => { + self.metadata = Arc::new(RwLock::new(data)); + Ok(true) + }, + Err(e) => { + error!( + target: LOG_TARGET, + "Could not read metadata from database. {}. We're going to panic here. Perhaps restarting will \ + fix things", + e.to_string() + ); + Err(ChainStorageError::CriticalError) + }, + } + } + + fn access_metadata(&self) -> Result, ChainStorageError> { + self.metadata.read().map_err(|e| { + error!( + target: LOG_TARGET, + "An attempt to get sa read lock on the blockchain metadata failed. {}", + e.to_string() + ); + ChainStorageError::AccessError("Read lock on blockchain metadata failed".into()) + }) + } + + fn update_metadata(&self, new_height: u64, new_hash: Vec) -> Result<(), ChainStorageError> { + let mut db = self.metadata.write().map_err(|_| { + ChainStorageError::AccessError( + "Could not obtain write access to blockchain metadata after storing block".into(), + ) + })?; + db.height_of_longest_chain = Some(new_height); + db.best_block = Some(new_hash); + Ok(()) + } + + /// Returns the height of the current longest chain. This method will only fail if there's a fairly serious + /// synchronisation problem on the database. You can try calling [BlockchainDatabase::try_recover_metadata] in + /// that case to re-sync the metadata; or else just exit the program. + /// + /// If the chain is empty (the genesis block hasn't been added yet), this function returns `None` + pub fn get_height(&self) -> Result, ChainStorageError> { + let metadata = self.access_metadata()?; + Ok(metadata.height_of_longest_chain) + } + + /// Returns a copy of the current blockchain database metadata + pub fn get_metadata(&self) -> Result { + let db = self.access_metadata()?; + Ok(db.clone()) + } + + /// Returns the total accumulated work/difficulty of the longest chain. + /// + /// This method will only fail if there's a fairly serious synchronisation problem on the database. You can try + /// calling [BlockchainDatabase::try_recover_metadata] in that case to re-sync the metadata; or else + /// just exit the program. + pub fn get_total_work(&self) -> Result { + let metadata = self.access_metadata()?; + Ok(metadata.total_accumulated_difficulty.into()) + } + + /// Returns the transaction kernel with the given hash. + pub fn fetch_kernel(&self, hash: HashOutput) -> Result { + fetch!(self, hash, TransactionKernel) + } + + /// Returns the block header at the given block height. + pub fn fetch_header(&self, block_num: u64) -> Result { + fetch!(self, block_num, BlockHeader) + } + + /// Returns the UTXO with the given hash. + pub fn fetch_utxo(&self, hash: HashOutput) -> Result { + fetch!(self, hash, UnspentOutput) + } + + /// Returns the STXO with the given hash. + pub fn fetch_stxo(&self, hash: HashOutput) -> Result { + fetch!(self, hash, SpentOutput) + } + + /// Returns the orphan block with the given hash. + pub fn fetch_orphan(&self, hash: HashOutput) -> Result { + fetch!(self, hash, OrphanBlock) + } + + /// Returns true if the given UTXO, represented by its hash exists in the UTXO set. + pub fn is_utxo(&self, hash: HashOutput) -> Result { + let key = DbKey::UnspentOutput(hash); + self.db.contains(&key) + } + + /// Calculate the Merklish root of the specified merkle mountain range. + pub fn fetch_mmr_root(&self, tree: MmrTree) -> Result { + self.db.fetch_mmr_root(tree) + } + + /// Fetch a Merklish proof for the given hash, tree and position in the MMR + pub fn fetch_mmr_proof(&self, tree: MmrTree, pos: u64) -> Result { + self.db.fetch_mmr_proof(tree, pos) + } + + /// Add a block to the longest chain. This function does some basic checks to maintain the chain integrity, but + /// does not perform a full block validation (this should have been done by this point). + /// + /// On completion, this function will have + /// * Checked that the previous block builds on the longest chain. + /// * If not - add orphan block and possibly re-org + /// * That the total accumulated work has increased. + /// * Mark all inputs in the block as spent. + /// * Updated the database metadata + /// + /// An error is returned if: + /// * the block has already been added + /// * any of the inputs were not in the UTXO set or were marked as spent already + /// + /// If an error does occur while writing the new block parts, all changes are reverted before returning. + pub fn add_block(&mut self, block: Block) -> Result { + if !self.is_new_best_block(&block)? { + return self.handle_possible_reorg(block); + } + let block_hash = block.hash(); + let block_height = block.header.height; + if self.db.contains(&DbKey::BlockHash(block_hash.clone()))? { + return Ok(BlockAddResult::BlockExists); + } + let mut txn = DbTransaction::new(); + let (header, inputs, outputs, kernels) = block.dissolve(); + txn.insert_header(header); + txn.spend_inputs(&inputs); + outputs.into_iter().for_each(|utxo| txn.insert_utxo(utxo)); + kernels.into_iter().for_each(|k| txn.insert_kernel(k)); + txn.commit_block(); + self.commit(txn)?; + self.update_metadata(block_height, block_hash)?; + Ok(BlockAddResult::Ok) + } + + /// Returns true if the given block -- assuming everything else is valid -- would be added to the tip of the + /// longest chain; i.e. the following conditions are met: + /// * The blockchain is empty, + /// * or ALL of: + /// * the block's parent hash is the hash of the block at the current chain tip, + /// * the block height is one greater than the parent block + /// * the total accumulated work has increased + pub fn is_new_best_block(&self, block: &Block) -> Result { + let (height, parent_hash) = { + let db = self.access_metadata()?; + if db.height_of_longest_chain.is_none() { + return Ok(true); + } + ( + db.height_of_longest_chain.clone().unwrap(), + db.best_block.clone().unwrap(), + ) + }; + let best_block = self.fetch_header(height - 1)?; + let result = block.header.prev_hash == parent_hash && + block.header.height == best_block.height + 1 && + block.header.total_difficulty > best_block.total_difficulty; + Ok(result) + } + + /// Fetch a block from the blockchain database. + /// + /// # Returns + /// This function returns an [HistoricalBlock] instance, which can be converted into a standard [Block], but also + /// contains some additional information given its retrospective perspective that will be of interest to block + /// explorers. For example, we know whether the outputs of this block have subsequently been spent or not and how + /// many blocks have been mined on top of this block. + /// + /// `fetch_block` can return a `ChainStorageError` in the following cases: + /// * There is an access problem on the back end. + /// * The height is beyond the current chain tip. + /// * The height is lower than the block at the pruning horizon. + pub fn fetch_block(&self, height: u64) -> Result { + let metadata = self.check_for_valid_height(height)?; + let header = self.fetch_header(height)?; + let kernel_cp = self.fetch_mmr_checkpoint(MmrTree::Kernel, height)?; + let (kernel_hashes, _) = kernel_cp.into_parts(); + let kernels = self.fetch_kernels(kernel_hashes)?; + let utxo_cp = self.db.fetch_mmr_checkpoint(MmrTree::Utxo, height)?; + let (utxo_hashes, deleted_nodes) = utxo_cp.into_parts(); + let inputs = self.fetch_inputs(deleted_nodes)?; + let (outputs, spent) = self.fetch_outputs(utxo_hashes)?; + let block = BlockBuilder::new() + .with_header(header) + .add_inputs(inputs) + .add_outputs(outputs) + .add_kernels(kernels) + .build(); + Ok(HistoricalBlock::new( + block, + metadata.height_of_longest_chain.unwrap() - height + 1, + spent, + )) + } + + fn check_for_valid_height(&self, height: u64) -> Result { + let metadata = self.get_metadata()?; + if metadata.height_of_longest_chain.is_none() { + return Err(ChainStorageError::InvalidQuery( + "Cannot retrieve block. Blockchain DB is empty".into(), + )); + } + if height > metadata.height_of_longest_chain.unwrap() { + return Err(ChainStorageError::InvalidQuery(format!( + "Cannot get block at height {}. Chain tip is at {}", + height, + metadata.height_of_longest_chain.unwrap() + ))); + } + // We can't actually provide full block beyond the pruning horizon + if height < metadata.horizon_block().unwrap() { + return Err(ChainStorageError::BeyondPruningHorizon); + } + Ok(metadata) + } + + fn fetch_kernels(&self, hashes: Vec) -> Result, ChainStorageError> { + hashes.into_iter().map(|hash| self.fetch_kernel(hash)).collect() + } + + fn fetch_inputs(&self, deleted_nodes: Bitmap) -> Result, ChainStorageError> { + // The inputs must all the in the current STXO set + let inputs: Result, ChainStorageError> = deleted_nodes + .iter() + .map(|pos| { + self.db + .fetch_mmr_node(MmrTree::Utxo, pos) + .and_then(|(hash, deleted)| { + assert!(deleted); + self.fetch_stxo(hash) + }) + .and_then(|stxo| Ok(TransactionInput::from(stxo))) + }) + .collect(); + inputs + } + + fn fetch_outputs(&self, hashes: Vec) -> Result<(Vec, Vec), ChainStorageError> { + let mut outputs = Vec::with_capacity(hashes.len()); + let mut spent = Vec::with_capacity(hashes.len()); + for hash in hashes.into_iter() { + // The outputs could come from either the UTXO or STXO set + match self.fetch_utxo(hash.clone()) { + Ok(utxo) => { + outputs.push(utxo); + continue; + }, + Err(ChainStorageError::ValueNotFound(_)) => {}, // Check STXO set below + Err(e) => return Err(e), // Something bad happened. Abort. + } + // Check the STXO set + let stxo = self.fetch_stxo(hash)?; + spent.push(stxo.commitment.clone()); + outputs.push(stxo); + } + Ok((outputs, spent)) + } + + fn fetch_mmr_checkpoint(&self, tree: MmrTree, height: u64) -> Result { + let metadata = self.get_metadata()?; + let horizon_block = match metadata.horizon_block() { + None => return Err(ChainStorageError::InvalidQuery("Blockchain database is empty".into())), + Some(i) => i, + }; + let index = height + .checked_sub(horizon_block) + .ok_or(ChainStorageError::BeyondPruningHorizon)? as u64; + self.db.fetch_mmr_checkpoint(tree, index) + } + + /// Atomically commit the provided transaction to the database backend. This function does not update the metadata. + pub(crate) fn commit(&mut self, txn: DbTransaction) -> Result<(), ChainStorageError> { + self.db.write(txn) + } + + /// Checks whether we should add the block as an orphan. If it is the case, the orphan block is added and the chain + /// is reorganised if necessary + fn handle_possible_reorg(&mut self, _block: Block) -> Result { + // TODO - check if block height > pruning horizon + // TODO - check if proof of work is valid and above some spam minimum?? + unimplemented!() + } + + fn handle_reorg(&mut self, _orphan_hash: Hash) -> Result<(), ChainStorageError> { + unimplemented!() + } +} + +fn unexpected_result(req: DbKey, res: DbValue) -> Result { + let msg = format!("Unexpected result for database query {}. Response: {}", req, res); + error!(target: LOG_TARGET, "{}", msg); + Err(ChainStorageError::UnexpectedResult(msg)) +} + +fn log_error(req: DbKey, err: ChainStorageError) -> Result { + error!( + target: LOG_TARGET, + "Database access error on request: {}: {}", + req, + err.to_string() + ); + Err(err) +} + +impl Clone for BlockchainDatabase +where T: BlockchainBackend +{ + fn clone(&self) -> Self { + BlockchainDatabase { + metadata: self.metadata.clone(), + db: self.db.clone(), + } + } +} diff --git a/base_layer/core/src/chain_storage/db_transaction.rs b/base_layer/core/src/chain_storage/db_transaction.rs new file mode 100644 index 0000000000..6c5bacf713 --- /dev/null +++ b/base_layer/core/src/chain_storage/db_transaction.rs @@ -0,0 +1,240 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +use crate::{ + blocks::{blockheader::BlockHash, Block, BlockHeader}, + transaction::{TransactionInput, TransactionKernel, TransactionOutput}, + types::HashOutput, +}; +use std::fmt::{Display, Error, Formatter}; +use tari_utilities::{hex::to_hex, Hashable}; + +#[derive(Debug)] +pub struct DbTransaction { + pub operations: Vec, +} + +impl Default for DbTransaction { + fn default() -> Self { + DbTransaction { + operations: Vec::with_capacity(128), + } + } +} + +impl DbTransaction { + /// Creates a new Database transaction. To commit the transactions call [BlockchainDatabase::execute] with the + /// transaction as a parameter. + pub fn new() -> Self { + DbTransaction::default() + } + + /// A general insert request. There are convenience functions for specific insert queries. + pub fn insert(&mut self, insert: DbKeyValuePair) { + self.operations.push(WriteOperation::Insert(insert)); + } + + /// A general insert request. There are convenience functions for specific delete queries. + pub fn delete(&mut self, delete: DbKey) { + self.operations.push(WriteOperation::Delete(delete)); + } + + /// Inserts a transaction kernel into the current transaction. + pub fn insert_kernel(&mut self, kernel: TransactionKernel) { + let hash = kernel.hash(); + self.insert(DbKeyValuePair::TransactionKernel(hash, Box::new(kernel))); + } + + /// Inserts a block header into the current transaction. + pub fn insert_header(&mut self, header: BlockHeader) { + let height = header.height; + self.insert(DbKeyValuePair::BlockHeader(height, Box::new(header))); + } + + /// Adds a UTXO into the current transaction. + pub fn insert_utxo(&mut self, utxo: TransactionOutput) { + let hash = utxo.hash(); + self.insert(DbKeyValuePair::UnspentOutput(hash, Box::new(utxo))); + } + + /// Stores an orphan block. No checks are made as to whether this is actually an orphan. That responsibility lies + /// with the calling function. + pub fn insert_orphan(&mut self, orphan: Block) { + let hash = orphan.hash(); + self.insert(DbKeyValuePair::OrphanBlock(hash, Box::new(orphan))); + } + + /// Moves a UTXO. If the UTXO is not in the UTXO set, the transaction will fail with an `UnspendableOutput` error. + pub fn move_utxo(&mut self, utxo_hash: HashOutput) { + self.operations + .push(WriteOperation::Spend(DbKey::UnspentOutput(utxo_hash))); + } + + /// Moves the given set of transaction inputs from the UTXO set to the STXO set. All the inputs *must* currently + /// exist in the UTXO set, or the transaction will error with `ChainStorageError::UnspendableOutput` + pub fn spend_inputs(&mut self, inputs: &[TransactionInput]) { + for input in inputs { + let input_hash = input.hash(); + self.move_utxo(input_hash); + } + } + + /// Adds a marker operation that allows the database to perform any additional work after adding a new block to + /// the database. + pub fn commit_block(&mut self) { + self.operations + .push(WriteOperation::Insert(DbKeyValuePair::CommitBlock)); + } + + /// Set the horizon beyond which we cannot be guaranteed provide detailed blockchain information anymore. + /// A value of zero indicates that no pruning should be carried out at all. That is, this state should act as a + /// archival node. + /// + /// This operation just sets the new horizon value. No pruning is done at this point. + pub fn set_pruning_horizon(&mut self, new_pruning_horizon: u64) { + self.operations.push(WriteOperation::Insert(DbKeyValuePair::Metadata( + MetadataKey::PruningHorizon, + MetadataValue::PruningHorizon(new_pruning_horizon), + ))); + } + + /// Rewind the blockchain state to the block height given. + /// + /// The operation will fail if + /// * The block height is in the future + /// * The block height is before pruning horizon + pub fn rewind_to_height(&mut self, _height: u64) { + unimplemented!() + } +} + +#[derive(Debug)] +pub enum WriteOperation { + Insert(DbKeyValuePair), + Delete(DbKey), + Spend(DbKey), + UnSpend(DbKey), + CreateMmrCheckpoint(MmrTree), +} + +#[derive(Debug)] +pub enum DbKeyValuePair { + Metadata(MetadataKey, MetadataValue), + BlockHeader(u64, Box), + UnspentOutput(HashOutput, Box), + SpentOutput(HashOutput, Box), + TransactionKernel(HashOutput, Box), + OrphanBlock(HashOutput, Box), + CommitBlock, +} + +#[derive(Debug)] +pub enum MmrTree { + Utxo, + Kernel, + RangeProof, + Header, +} + +#[derive(Debug, PartialEq)] +pub enum MetadataKey { + ChainHeight, + BestBlock, + AccumulatedWork, + PruningHorizon, +} + +#[derive(Debug)] +pub enum MetadataValue { + ChainHeight(Option), + BestBlock(Option), + AccumulatedWork(u64), + PruningHorizon(u64), +} + +#[derive(Debug, PartialEq)] +pub enum DbKey { + Metadata(MetadataKey), + BlockHeader(u64), + BlockHash(BlockHash), + UnspentOutput(HashOutput), + SpentOutput(HashOutput), + TransactionKernel(HashOutput), + OrphanBlock(HashOutput), +} + +#[derive(Debug)] +pub enum DbValue { + Metadata(MetadataValue), + BlockHeader(Box), + BlockHash(Box), + UnspentOutput(Box), + SpentOutput(Box), + TransactionKernel(Box), + OrphanBlock(Box), +} + +impl Display for DbValue { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + DbValue::Metadata(MetadataValue::ChainHeight(_)) => f.write_str("Current chain height"), + DbValue::Metadata(MetadataValue::AccumulatedWork(_)) => f.write_str("Total accumulated work"), + DbValue::Metadata(MetadataValue::PruningHorizon(_)) => f.write_str("Pruning horizon"), + DbValue::Metadata(MetadataValue::BestBlock(_)) => f.write_str("Chain tip block hash"), + DbValue::BlockHeader(_) => f.write_str("Block header"), + DbValue::BlockHash(_) => f.write_str("Block hash"), + DbValue::UnspentOutput(_) => f.write_str("Unspent output"), + DbValue::SpentOutput(_) => f.write_str("Spent output"), + DbValue::TransactionKernel(_) => f.write_str("Transaction kernel"), + DbValue::OrphanBlock(_) => f.write_str("Orphan block"), + } + } +} + +impl Display for DbKey { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + DbKey::Metadata(MetadataKey::ChainHeight) => f.write_str("Current chain height"), + DbKey::Metadata(MetadataKey::AccumulatedWork) => f.write_str("Total accumulated work"), + DbKey::Metadata(MetadataKey::PruningHorizon) => f.write_str("Pruning horizon"), + DbKey::Metadata(MetadataKey::BestBlock) => f.write_str("Chain tip block hash"), + DbKey::BlockHeader(v) => f.write_str(&format!("Block header (#{})", v)), + DbKey::BlockHash(v) => f.write_str(&format!("Block hash (#{})", to_hex(v))), + DbKey::UnspentOutput(v) => f.write_str(&format!("Unspent output ({})", to_hex(v))), + DbKey::SpentOutput(v) => f.write_str(&format!("Spent output ({})", to_hex(v))), + DbKey::TransactionKernel(v) => f.write_str(&format!("Transaction kernel ({})", to_hex(v))), + DbKey::OrphanBlock(v) => f.write_str(&format!("Orphan block hash ({})", to_hex(v))), + } + } +} + +impl Display for MmrTree { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + MmrTree::RangeProof => f.write_str("Range Proof"), + MmrTree::Utxo => f.write_str("UTXO"), + MmrTree::Kernel => f.write_str("Kernel"), + MmrTree::Header => f.write_str("Block header"), + } + } +} diff --git a/base_layer/core/src/chain_storage/error.rs b/base_layer/core/src/chain_storage/error.rs new file mode 100644 index 0000000000..087cb88dd2 --- /dev/null +++ b/base_layer/core/src/chain_storage/error.rs @@ -0,0 +1,57 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::chain_storage::db_transaction::DbKey; +use derive_error::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum ChainStorageError { + // Access to the underlying storage mechanism failed + #[error(non_std, no_from)] + AccessError(String), + // The database may be corrupted or otherwise be in an inconsistent state. Please check logs to try and identify + // the issue + #[error(non_std, no_from)] + CorruptedDatabase(String), + // A given input could not be spent because it was not in the UTXO set + UnspendableInput, + // A problem occurred trying to move a STXO back into the UTXO pool during a re-org. + UnspendError, + // An unexpected result type was received for the given database request. This suggests that there is an internal + // error or bug of sorts. + #[error(msg_embedded, non_std, no_from)] + UnexpectedResult(String), + // You tried to execute an invalid Database operation + #[error(msg_embedded, non_std, no_from)] + InvalidOperation(String), + // There appears to be a critical error on the back end. The database might be in an inconsistent state. Check + // the logs for more information. + CriticalError, + // Cannot return data for requests older than the current pruning horizon + BeyondPruningHorizon, + // A parameter to the request was invalid + #[error(msg_embedded, non_std, no_from)] + InvalidQuery(String), + // The requested value was not found in the database + #[error(non_std, no_from)] + ValueNotFound(DbKey), +} diff --git a/base_layer/core/src/chain_storage/historical_block.rs b/base_layer/core/src/chain_storage/historical_block.rs new file mode 100644 index 0000000000..d06b4f3824 --- /dev/null +++ b/base_layer/core/src/chain_storage/historical_block.rs @@ -0,0 +1,60 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{blocks::Block, transaction::TransactionOutput, types::Commitment}; + +/// The representation of a historical block in the blockchain. It is essentially identical to a protocol-defined +/// block but contains some extra metadata that clients such as Block Explorers will find interesting. +pub struct HistoricalBlock { + /// The number of blocks that have been mined since this block, including this one. The current tip will have one + /// confirmation. + confirmations: u64, + /// An array of commitments of the outputs from this block that have subsequently been spent. + spent_commitments: Vec, + /// The underlying block + block: Block, +} + +impl HistoricalBlock { + pub fn new(block: Block, confirmations: u64, spent_commitments: Vec) -> Self { + HistoricalBlock { + block, + confirmations, + spent_commitments, + } + } + + pub fn confirmations(&self) -> u64 { + self.confirmations + } + + /// Determines whether the given output (presumably an output of this block) has subsequently been spent + pub fn is_spent(&self, output: &TransactionOutput) -> bool { + self.spent_commitments.contains(&output.commitment) + } +} + +impl From for Block { + fn from(block: HistoricalBlock) -> Self { + block.block + } +} diff --git a/base_layer/core/src/chain_storage/memory_db.rs b/base_layer/core/src/chain_storage/memory_db.rs new file mode 100644 index 0000000000..e09f7dfac6 --- /dev/null +++ b/base_layer/core/src/chain_storage/memory_db.rs @@ -0,0 +1,330 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! This is a memory-based blockchain database, generally only useful for testing purposes + +use crate::{ + blocks::{Block, BlockHeader}, + chain_storage::{ + blockchain_database::BlockchainBackend, + db_transaction::{ + DbKey, + DbKeyValuePair, + DbTransaction, + DbValue, + MetadataKey, + MetadataValue, + MmrTree, + WriteOperation, + }, + error::ChainStorageError, + }, + transaction::{TransactionKernel, TransactionOutput}, + types::HashOutput, +}; +use digest::Digest; +use std::{ + collections::HashMap, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, +}; +use tari_mmr::{Hash as MmrHash, MerkleChangeTracker, MerkleCheckPoint, MerkleProof, MutableMmr}; +use tari_utilities::hash::Hashable; + +struct InnerDatabase +where D: Digest +{ + headers: HashMap, + block_hashes: HashMap, + utxos: HashMap, + stxos: HashMap, + kernels: HashMap, + orphans: HashMap, + // Define MMRs to use both a memory-backed base and a memory-backed pruned MMR + utxo_mmr: MerkleChangeTracker, Vec>, + header_mmr: MerkleChangeTracker, Vec>, + kernel_mmr: MerkleChangeTracker, Vec>, + range_proof_mmr: MerkleChangeTracker, Vec>, +} + +/// A memory-backed blockchain database. The data is stored in RAM; and so all data will be lost when the program +/// terminates. Thus this DB is intended for testing purposes. It's also not very efficient since a single Mutex +/// protects the entire database. Again: testing. +#[derive(Default)] +pub struct MemoryDatabase +where D: Digest +{ + db: Arc>>, +} + +impl MemoryDatabase +where D: Digest +{ + pub(self) fn db_access(&self) -> Result>, ChainStorageError> { + self.db + .read() + .map_err(|e| ChainStorageError::AccessError(e.to_string())) + } +} + +impl BlockchainBackend for MemoryDatabase +where D: Digest + Send + Sync +{ + fn write(&self, tx: DbTransaction) -> Result<(), ChainStorageError> { + let mut db = self + .db + .write() + .map_err(|e| ChainStorageError::AccessError(e.to_string()))?; + // Not **really** atomic, but.. + // Hashmap insertions don't typically fail and b) MemoryDB should not be used for production anyway. + for op in tx.operations.into_iter() { + match op { + WriteOperation::Insert(insert) => match insert { + DbKeyValuePair::Metadata(_, _) => {}, // no-op. Memory-based DB, so we don't store metadata + DbKeyValuePair::BlockHeader(k, v) => { + let hash = v.hash(); + db.header_mmr.push(&hash).unwrap(); + db.headers.insert(k, *v); + }, + DbKeyValuePair::UnspentOutput(k, v) => { + db.utxo_mmr.push(&k).unwrap(); + let proof_hash = v.proof().hash(); + let _ = db.range_proof_mmr.push(&proof_hash); + db.utxos.insert(k, *v); + }, + DbKeyValuePair::SpentOutput(k, v) => { + db.stxos.insert(k, *v); + }, + DbKeyValuePair::TransactionKernel(k, v) => { + db.kernel_mmr.push(&k).unwrap(); + db.kernels.insert(k, *v); + }, + DbKeyValuePair::OrphanBlock(k, v) => { + db.orphans.insert(k, *v); + }, + DbKeyValuePair::CommitBlock => db + .kernel_mmr + .commit() + .and(db.range_proof_mmr.commit()) + .and(db.utxo_mmr.commit()) + .and(db.header_mmr.commit()) + .map_err(|e| ChainStorageError::AccessError(e.to_string()))?, + }, + WriteOperation::Delete(delete) => match delete { + DbKey::Metadata(_) => {}, // no-op + DbKey::BlockHeader(k) => { + db.headers.remove(&k); + }, + DbKey::BlockHash(hash) => match db.block_hashes.remove(&hash) { + Some(i) => { + db.headers.remove(&i); + }, + None => {}, + }, + DbKey::UnspentOutput(k) => { + db.utxos.remove(&k); + }, + DbKey::SpentOutput(k) => { + db.stxos.remove(&k); + }, + DbKey::TransactionKernel(k) => { + db.kernels.remove(&k); + }, + DbKey::OrphanBlock(k) => { + db.orphans.remove(&k); + }, + }, + WriteOperation::Spend(key) => match key { + DbKey::UnspentOutput(hash) => { + let moved = spend_utxo(&mut db, hash); + if !moved { + return Err(ChainStorageError::UnspendableInput); + } + }, + _ => return Err(ChainStorageError::InvalidOperation("Only UTXOs can be spent".into())), + }, + WriteOperation::UnSpend(key) => match key { + DbKey::SpentOutput(hash) => { + let moved = unspend_stxo(&mut db, hash); + if !moved { + return Err(ChainStorageError::UnspendError); + } + }, + _ => return Err(ChainStorageError::InvalidOperation("Only STXOs can be unspent".into())), + }, + WriteOperation::CreateMmrCheckpoint(tree) => match tree { + MmrTree::Header => db + .header_mmr + .commit() + .map_err(|e| ChainStorageError::AccessError(e.to_string()))?, + MmrTree::Kernel => db + .kernel_mmr + .commit() + .map_err(|e| ChainStorageError::AccessError(e.to_string()))?, + MmrTree::Utxo => db + .utxo_mmr + .commit() + .map_err(|e| ChainStorageError::AccessError(e.to_string()))?, + MmrTree::RangeProof => db + .range_proof_mmr + .commit() + .map_err(|e| ChainStorageError::AccessError(e.to_string()))?, + }, + } + } + Ok(()) + } + + fn fetch(&self, key: &DbKey) -> Result, ChainStorageError> { + let db = self.db_access()?; + let result = match key { + DbKey::Metadata(MetadataKey::ChainHeight) => Some(DbValue::Metadata(MetadataValue::ChainHeight(None))), + DbKey::Metadata(MetadataKey::AccumulatedWork) => Some(DbValue::Metadata(MetadataValue::AccumulatedWork(0))), + DbKey::Metadata(MetadataKey::PruningHorizon) => Some(DbValue::Metadata(MetadataValue::PruningHorizon(0))), + DbKey::Metadata(MetadataKey::BestBlock) => Some(DbValue::Metadata(MetadataValue::BestBlock(None))), + DbKey::BlockHeader(k) => db.headers.get(k).map(|v| DbValue::BlockHeader(Box::new(v.clone()))), + DbKey::BlockHash(hash) => db + .block_hashes + .get(hash) + .and_then(|i| db.headers.get(i)) + .map(|v| DbValue::BlockHeader(Box::new(v.clone()))), + DbKey::UnspentOutput(k) => db.utxos.get(k).map(|v| DbValue::UnspentOutput(Box::new(v.clone()))), + DbKey::SpentOutput(k) => db.stxos.get(k).map(|v| DbValue::SpentOutput(Box::new(v.clone()))), + DbKey::TransactionKernel(k) => db + .kernels + .get(k) + .map(|v| DbValue::TransactionKernel(Box::new(v.clone()))), + DbKey::OrphanBlock(k) => db.orphans.get(k).map(|v| DbValue::OrphanBlock(Box::new(v.clone()))), + }; + Ok(result) + } + + fn contains(&self, key: &DbKey) -> Result { + let db = self.db_access()?; + let result = match key { + DbKey::Metadata(_) => true, + DbKey::BlockHeader(k) => db.headers.contains_key(k), + DbKey::BlockHash(h) => db.block_hashes.contains_key(h), + DbKey::UnspentOutput(k) => db.utxos.contains_key(k), + DbKey::SpentOutput(k) => db.stxos.contains_key(k), + DbKey::TransactionKernel(k) => db.kernels.contains_key(k), + DbKey::OrphanBlock(k) => db.orphans.contains_key(k), + }; + Ok(result) + } + + fn fetch_mmr_root(&self, tree: MmrTree) -> Result, ChainStorageError> { + let db = self.db_access()?; + let root = match tree { + MmrTree::Utxo => db.utxo_mmr.get_merkle_root(), + MmrTree::Kernel => db.kernel_mmr.get_merkle_root(), + MmrTree::RangeProof => db.range_proof_mmr.get_merkle_root(), + MmrTree::Header => db.header_mmr.get_merkle_root(), + }; + Ok(root) + } + + fn fetch_mmr_proof(&self, _tree: MmrTree, _pos: u64) -> Result { + unimplemented!() + } + + fn fetch_mmr_checkpoint(&self, tree: MmrTree, index: u64) -> Result { + let db = self.db_access()?; + let index = index as usize; + let cp = match tree { + MmrTree::Kernel => db.kernel_mmr.get_checkpoint(index), + MmrTree::Utxo => db.utxo_mmr.get_checkpoint(index), + MmrTree::RangeProof => db.range_proof_mmr.get_checkpoint(index), + MmrTree::Header => db.header_mmr.get_checkpoint(index), + }; + cp.map_err(|e| ChainStorageError::AccessError(format!("MMR Checkpoint error: {}", e.to_string()))) + } + + fn fetch_mmr_node(&self, tree: MmrTree, pos: u32) -> Result<(Vec, bool), ChainStorageError> { + let db = self.db_access()?; + let (hash, deleted) = match tree { + MmrTree::Kernel => db.kernel_mmr.get_leaf_status(pos), + MmrTree::Header => db.kernel_mmr.get_leaf_status(pos), + MmrTree::Utxo => db.kernel_mmr.get_leaf_status(pos), + MmrTree::RangeProof => db.kernel_mmr.get_leaf_status(pos), + }; + let hash = hash + .ok_or(ChainStorageError::UnexpectedResult(format!( + "A leaf node hash in the {} MMR tree was not found", + tree + )))? + .clone(); + Ok((hash, deleted)) + } +} + +impl Clone for MemoryDatabase +where D: Digest +{ + fn clone(&self) -> Self { + MemoryDatabase { db: self.db.clone() } + } +} + +impl Default for InnerDatabase +where D: Digest +{ + fn default() -> Self { + let utxo_mmr = MerkleChangeTracker::::new(MutableMmr::new(Vec::new()), Vec::new()).unwrap(); + let header_mmr = MerkleChangeTracker::::new(MutableMmr::new(Vec::new()), Vec::new()).unwrap(); + let kernel_mmr = MerkleChangeTracker::::new(MutableMmr::new(Vec::new()), Vec::new()).unwrap(); + let range_proof_mmr = MerkleChangeTracker::::new(MutableMmr::new(Vec::new()), Vec::new()).unwrap(); + InnerDatabase { + headers: HashMap::default(), + block_hashes: HashMap::default(), + utxos: HashMap::default(), + stxos: HashMap::default(), + kernels: HashMap::default(), + orphans: HashMap::default(), + utxo_mmr, + header_mmr, + kernel_mmr, + range_proof_mmr, + } + } +} + +// This is a private helper function. When it is called, we are guaranteed to have a write lock on self.db +fn spend_utxo(db: &mut RwLockWriteGuard>, hash: HashOutput) -> bool { + match db.utxos.remove(&hash) { + None => false, + Some(utxo) => { + db.stxos.insert(hash, utxo); + true + }, + } +} + +// This is a private helper function. When it is called, we are guaranteed to have a write lock on self.db +fn unspend_stxo(db: &mut RwLockWriteGuard>, hash: HashOutput) -> bool { + match db.stxos.remove(&hash) { + None => false, + Some(stxo) => { + db.utxos.insert(hash, stxo); + true + }, + } +} diff --git a/base_layer/core/src/chain_storage/metadata.rs b/base_layer/core/src/chain_storage/metadata.rs new file mode 100644 index 0000000000..b84f57cbcf --- /dev/null +++ b/base_layer/core/src/chain_storage/metadata.rs @@ -0,0 +1,79 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::blocks::blockheader::BlockHash; + +#[derive(Debug, Clone)] +pub struct ChainMetadata { + /// The current chain height, or the block number of the longest valid chain, or `None` if there is no chain + pub height_of_longest_chain: Option, + /// The block hash of the current tip of the longest valid chain, or `None` for an empty chain + pub best_block: Option, + /// The total accumulated difficulty, or work, on the longest valid chain since the genesis block. + pub total_accumulated_difficulty: u64, + /// The number of blocks back from the tip that this database tracks. A value of 0 indicates that all blocks are + /// tracked (i.e. the database is in full archival mode). + pub pruning_horizon: u64, +} + +impl ChainMetadata { + pub fn new(height: u64, hash: BlockHash, work: u64, horizon: u64) -> ChainMetadata { + ChainMetadata { + height_of_longest_chain: Some(height), + best_block: Some(hash), + total_accumulated_difficulty: work, + pruning_horizon: horizon, + } + } + + /// The block height at the pruning horizon. Typically database backends cannot provide any block data earlier + /// than this point. + /// + /// #Returns + /// + /// * `None`, if the chain is still empty + /// * `h`, the block number of the first block stored in the chain + #[inline(always)] + pub fn horizon_block(&self) -> Option { + if self.height_of_longest_chain.is_none() { + return None; + } + match self.pruning_horizon { + 0 => Some(0u64), + horizon => match self.height_of_longest_chain.unwrap().checked_sub(horizon) { + None => Some(0u64), + Some(v) => Some(v as u64), + }, + } + } +} + +impl Default for ChainMetadata { + fn default() -> Self { + ChainMetadata { + height_of_longest_chain: None, + best_block: None, + total_accumulated_difficulty: 0, + pruning_horizon: 2880, + } + } +} diff --git a/base_layer/core/src/chain_storage/mod.rs b/base_layer/core/src/chain_storage/mod.rs new file mode 100644 index 0000000000..5685fe2a7c --- /dev/null +++ b/base_layer/core/src/chain_storage/mod.rs @@ -0,0 +1,43 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! This module is responsible for handling logic responsible for storing the blockchain state. +//! +//! It is structured in such a way that clients (e.g. base nodes) can configure the various components of the state +//! (kernels, utxos, etc) in whichever way they like. It's possible to have the UTXO set in memory, and the kernels +//! backed by LMDB, while the merkle trees are stored in flat files for example. + +mod blockchain_database; +mod db_transaction; +mod error; +mod historical_block; +mod memory_db; +mod metadata; +#[cfg(test)] +mod test; + +// Public API exports +pub use blockchain_database::BlockchainDatabase; +pub use db_transaction::{DbTransaction, MmrTree}; +pub use historical_block::HistoricalBlock; +pub use memory_db::MemoryDatabase; +pub use metadata::ChainMetadata; diff --git a/base_layer/core/src/chain_storage/test/chain_storage.rs b/base_layer/core/src/chain_storage/test/chain_storage.rs new file mode 100644 index 0000000000..b1576f0c11 --- /dev/null +++ b/base_layer/core/src/chain_storage/test/chain_storage.rs @@ -0,0 +1,258 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +use crate::{ + blocks::{genesis_block::get_genesis_block, Block, BlockHeader}, + chain_storage::{ + blockchain_database::BlockAddResult, + db_transaction::DbKey, + error::ChainStorageError, + BlockchainDatabase, + DbTransaction, + MemoryDatabase, + MmrTree, + }, + tari_amount::MicroTari, + test_utils::builders::{create_test_block, create_test_kernel, create_test_tx, create_utxo}, + types::HashDigest, +}; +use std::thread; +use tari_mmr::MutableMmr; +use tari_utilities::{hex::Hex, Hashable}; + +#[test] +fn fetch_nonexistent_kernel() { + let store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + let h = vec![0u8; 32]; + assert_eq!( + store.fetch_kernel(h.clone()), + Err(ChainStorageError::ValueNotFound(DbKey::TransactionKernel(h))) + ); +} + +#[test] +fn insert_and_fetch_kernel() { + let mut store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + let kernel = create_test_kernel(5.into(), 0); + let hash = kernel.hash(); + + let mut txn = DbTransaction::new(); + txn.insert_kernel(kernel.clone()); + assert!(store.commit(txn).is_ok()); + assert_eq!(store.fetch_kernel(hash), Ok(kernel)); +} + +#[test] +fn fetch_nonexistent_header() { + let store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + assert_eq!( + store.fetch_header(0), + Err(ChainStorageError::ValueNotFound(DbKey::BlockHeader(0))) + ); +} +#[test] +fn insert_and_fetch_header() { + let mut store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + let mut header = BlockHeader::new(0); + header.height = 42; + + let mut txn = DbTransaction::new(); + txn.insert_header(header.clone()); + assert!(store.commit(txn).is_ok()); + assert_eq!( + store.fetch_header(0), + Err(ChainStorageError::ValueNotFound(DbKey::BlockHeader(0))) + ); + assert_eq!(store.fetch_header(42), Ok(header)); +} + +#[test] +fn insert_and_fetch_utxo() { + let mut store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + let (utxo, _) = create_utxo(MicroTari(10_000)); + let hash = utxo.hash(); + assert_eq!(store.is_utxo(hash.clone()).unwrap(), false); + let mut txn = DbTransaction::new(); + txn.insert_utxo(utxo.clone()); + assert!(store.commit(txn).is_ok()); + assert_eq!(store.is_utxo(hash.clone()).unwrap(), true); + assert_eq!(store.fetch_utxo(hash), Ok(utxo)); +} + +#[test] +fn insert_and_fetch_orphan() { + let mut store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + let txs = vec![ + create_test_tx(1000.into(), 10.into(), 0, 2, 1), + create_test_tx(2000.into(), 20.into(), 0, 1, 1), + ]; + let orphan = create_test_block(10, txs); + let orphan_hash = orphan.hash(); + let mut txn = DbTransaction::new(); + txn.insert_orphan(orphan.clone()); + assert!(store.commit(txn).is_ok()); + assert_eq!(store.fetch_orphan(orphan_hash), Ok(orphan)); +} + +#[test] +fn multiple_threads() { + let store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + // Save a kernel in thread A + let mut store_a = store.clone(); + let a = thread::spawn(move || { + let kernel = create_test_kernel(5.into(), 0); + let hash = kernel.hash(); + let mut txn = DbTransaction::new(); + txn.insert_kernel(kernel.clone()); + assert!(store_a.commit(txn).is_ok()); + hash + }); + // Save a kernel in thread B + let mut store_b = store.clone(); + let b = thread::spawn(move || { + let kernel = create_test_kernel(10.into(), 0); + let hash = kernel.hash(); + let mut txn = DbTransaction::new(); + txn.insert_kernel(kernel.clone()); + assert!(store_b.commit(txn).is_ok()); + hash + }); + let hash_a = a.join().unwrap(); + let hash_b = b.join().unwrap(); + // Get the kernels back + let kernel_a = store.fetch_kernel(hash_a).unwrap(); + assert_eq!(kernel_a.fee, 5.into()); + let kernel_b = store.fetch_kernel(hash_b).unwrap(); + assert_eq!(kernel_b.fee, 10.into()); +} + +#[test] +fn utxo_and_rp_merkle_root() { + let mut store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + let root = store.fetch_mmr_root(MmrTree::Utxo).unwrap(); + // This is the zero-length MMR of a mutable MMR with Blake256 as hasher + assert_eq!( + &root.to_hex(), + "26146a5435ef15e8cf7dc3354cb7268137e8be211794e93d04551576c6561565" + ); + let (utxo1, _) = create_utxo(MicroTari(10_000)); + let (utxo2, _) = create_utxo(MicroTari(10_000)); + let hash1 = utxo1.hash(); + let hash2 = utxo2.hash(); + // Calculate the Range proof MMR root as a check + let mut rp_mmr_check = MutableMmr::::new(Vec::new()); + assert_eq!(rp_mmr_check.push(&utxo1.proof.hash()).unwrap(), 1); + assert_eq!(rp_mmr_check.push(&utxo2.proof.hash()).unwrap(), 2); + // Store the UTXOs + let mut txn = DbTransaction::new(); + txn.insert_utxo(utxo1); + txn.insert_utxo(utxo2); + assert!(store.commit(txn).is_ok()); + let root = store.fetch_mmr_root(MmrTree::Utxo).unwrap(); + let rp_root = store.fetch_mmr_root(MmrTree::RangeProof).unwrap(); + let mut mmr_check = MutableMmr::::new(Vec::new()); + assert!(mmr_check.push(&hash1).is_ok()); + assert!(mmr_check.push(&hash2).is_ok()); + assert_eq!(root.to_hex(), mmr_check.get_merkle_root().to_hex()); + assert_eq!(rp_root.to_hex(), rp_mmr_check.get_merkle_root().to_hex()); +} + +#[test] +fn header_merkle_root() { + let mut store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + let root = store.fetch_mmr_root(MmrTree::Header).unwrap(); + // This is the zero-length MMR of a mutable MMR with Blake256 as hasher + assert_eq!( + &root.to_hex(), + "26146a5435ef15e8cf7dc3354cb7268137e8be211794e93d04551576c6561565" + ); + let header1 = BlockHeader::new(0); + let mut header2 = BlockHeader::new(0); + header2.height = 1; + let hash1 = header1.hash(); + let hash2 = header2.hash(); + let mut txn = DbTransaction::new(); + txn.insert_header(header1); + txn.insert_header(header2); + assert!(store.commit(txn).is_ok()); + let root = store.fetch_mmr_root(MmrTree::Header).unwrap(); + let mut mmr_check = MutableMmr::::new(Vec::new()); + assert!(mmr_check.push(&hash1).is_ok()); + assert!(mmr_check.push(&hash2).is_ok()); + assert_eq!(root.to_hex(), mmr_check.get_merkle_root().to_hex()); +} + +#[test] +fn kernel_merkle_root() { + let mut store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + let root = store.fetch_mmr_root(MmrTree::Kernel).unwrap(); + // This is the zero-length MMR of a mutable MMR with Blake256 as hasher + assert_eq!( + &root.to_hex(), + "26146a5435ef15e8cf7dc3354cb7268137e8be211794e93d04551576c6561565" + ); + let kernel1 = create_test_kernel(100.into(), 0); + let kernel2 = create_test_kernel(200.into(), 0); + let kernel3 = create_test_kernel(300.into(), 0); + let hash1 = kernel1.hash(); + let hash2 = kernel2.hash(); + let hash3 = kernel3.hash(); + let mut txn = DbTransaction::new(); + txn.insert_kernel(kernel1); + txn.insert_kernel(kernel2); + txn.insert_kernel(kernel3); + assert!(store.commit(txn).is_ok()); + let root = store.fetch_mmr_root(MmrTree::Kernel).unwrap(); + let mut mmr_check = MutableMmr::::new(Vec::new()); + assert!(mmr_check.push(&hash1).is_ok()); + assert!(mmr_check.push(&hash2).is_ok()); + assert!(mmr_check.push(&hash3).is_ok()); + assert_eq!(root.to_hex(), mmr_check.get_merkle_root().to_hex()); +} + +#[test] +fn store_and_retrieve_block() { + // Create new database + let mut store = BlockchainDatabase::new(MemoryDatabase::::default()).unwrap(); + let metadata = store.get_metadata().unwrap(); + assert_eq!(metadata.height_of_longest_chain, None); + assert_eq!(metadata.best_block, None); + // Add the Genesis block + let block = get_genesis_block(); + let hash = block.hash(); + assert_eq!(store.add_block(block.clone()), Ok(BlockAddResult::Ok)); + println!("Added genesis block"); + // Check the metadata + let metadata = store.get_metadata().unwrap(); + assert_eq!(metadata.height_of_longest_chain, Some(0)); + assert_eq!(metadata.best_block, Some(hash)); + assert_eq!(metadata.horizon_block(), Some(0)); + // Fetch the block back + println!("Fetching genesis block"); + let block2 = store.fetch_block(0).unwrap(); + println!("Fetched genesis block"); + assert_eq!(block2.confirmations(), 1); + // Compare the blocks + let block2 = Block::from(block2); + assert_eq!(block, block2); +} diff --git a/base_layer/core/src/chain_storage/test/mod.rs b/base_layer/core/src/chain_storage/test/mod.rs new file mode 100644 index 0000000000..8b274e1d65 --- /dev/null +++ b/base_layer/core/src/chain_storage/test/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +mod chain_storage; diff --git a/base_layer/blockchain/src/chain.rs b/base_layer/core/src/consensus.rs similarity index 57% rename from base_layer/blockchain/src/chain.rs rename to base_layer/core/src/consensus.rs index b382daee2c..60db3893d9 100644 --- a/base_layer/blockchain/src/chain.rs +++ b/base_layer/core/src/consensus.rs @@ -1,4 +1,4 @@ -// Copyright 2018 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -20,36 +20,40 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::{blockchainstate::BlockchainState, error::ChainError, store::Store}; -use tari_core::block::Block; +use crate::emission::EmissionSchedule; -use std::collections::HashMap; - -type BlockHash = [u8; 32]; - -/// The Chain is the actual data structure to represent the blockchain -pub struct Chain { - /// This is the database used to storepersistentt data in - pub store: Store, - /// This the the current UTXO set, kernels and headers - pub blockchainstate: BlockchainState, - /// This is all valid blocks which dont have a parent trace to the genesis block - pub orphans: HashMap, - /// This is our pruning horizon - pub pruning_horizon: Option, +/// This is used to control all consensus values. +pub struct ConsensusRules { + /// The min height maturity a coinbase utxo must have + coinbase_lock_height: u64, + /// The emission schedule to use for coinbase rewards + emission_schedule: EmissionSchedule, + /// Current version of the blockchain + blockchain_version: u16, } -impl Chain { - pub fn new(dbstore: Store, pruning_horizon: Option) -> Chain { - Chain { - store: dbstore, - blockchainstate: BlockchainState::new(), - orphans: HashMap::new(), - pruning_horizon, +impl ConsensusRules { + pub fn current() -> Self { + // CONSENSUS_RULES + ConsensusRules { + coinbase_lock_height: 1, + emission_schedule: EmissionSchedule::new(10_000_000.into(), 0.999, 100.into()), + blockchain_version: 1, } } - pub fn process_new_block(&self, _new_block: &Block) -> Result<(), ChainError> { - Ok(()) + /// The min height maturity a coinbase utxo must have + pub fn coinbase_lock_height(&self) -> u64 { + self.coinbase_lock_height + } + + /// Current version of the blockchain + pub fn blockchain_version(&self) -> u16 { + self.blockchain_version + } + + /// The emission schedule to use for coinbase rewards + pub fn emission_schedule(&self) -> &EmissionSchedule { + &self.emission_schedule } } diff --git a/base_layer/core/src/consts.rs b/base_layer/core/src/consts.rs new file mode 100644 index 0000000000..2872b123f7 --- /dev/null +++ b/base_layer/core/src/consts.rs @@ -0,0 +1,42 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::time::Duration; + +/// The maximum number of transactions that can be stored in the Unconfirmed Transaction pool +pub const MEMPOOL_UNCONFIRMED_POOL_STORAGE_CAPACITY: usize = 1000; +/// The maximum number of transactions that can be skipped when compiling a set of highest priority transactions, +/// skipping over large transactions are performed in an attempt to fit more transactions into the remaining space. +pub const MEMPOOL_UNCONFIRMED_POOL_WEIGHT_TRANSACTION_SKIP_COUNT: usize = 20; + +/// The maximum number of transactions that can be stored in the Orphan pool +pub const MEMPOOL_ORPHAN_POOL_STORAGE_CAPACITY: usize = 1000; +/// The time-to-live duration used for transactions stored in the OrphanPool +pub const MEMPOOL_ORPHAN_POOL_CACHE_TTL: Duration = Duration::from_secs(300); + +/// The maximum number of transactions that can be stored in the Pending pool +pub const MEMPOOL_PENDING_POOL_STORAGE_CAPACITY: usize = 1000; + +/// The maximum number of transactions that can be stored in the Reorg pool +pub const MEMPOOL_REORG_POOL_STORAGE_CAPACITY: usize = 1000; +/// The time-to-live duration used for transactions stored in the ReorgPool +pub const MEMPOOL_REORG_POOL_CACHE_TTL: Duration = Duration::from_secs(300); diff --git a/base_layer/core/src/emission.rs b/base_layer/core/src/emission.rs new file mode 100644 index 0000000000..570053f6f0 --- /dev/null +++ b/base_layer/core/src/emission.rs @@ -0,0 +1,167 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::tari_amount::MicroTari; + +/// The Tari emission schedule. The emission schedule determines how much Tari is mined as a block reward at every +/// block. +/// +/// NB: We don't know what the final emission schedule will be on Tari yet, so do not give any weight to values or +/// formulae provided in this file, they will almost certainly change ahead of main-net release. +#[derive(Clone)] +pub struct EmissionSchedule { + initial: MicroTari, + decay: f64, + tail: MicroTari, +} + +impl EmissionSchedule { + /// Create a new emission schedule instance. + /// + /// The Emission schedule follows a similar pattern to Monero; with an exponentially decaying emission rate with + /// a constant tail emission rate. + /// + /// The block reward is given by + /// $$ r_n = A_0 r^n + t $$ + /// + /// where + /// * $$A_0$$ is the genesis block reward + /// * $$1-r$$ is the decay rate + /// * $$t$$ is the constant tail emission rate + pub fn new(initial: MicroTari, decay: f64, tail: MicroTari) -> EmissionSchedule { + EmissionSchedule { initial, decay, tail } + } + + /// Calculate the block reward for the given block height, in µTari + pub fn block_reward(&self, block: u64) -> MicroTari { + let base = if block < std::i32::MAX as u64 { + let base_f = (f64::from(self.initial) * self.decay.powi(block as i32)).trunc(); + MicroTari::from(base_f as u64) + } else { + MicroTari::from(0) + }; + base + self.tail + } + + /// Calculate the exact emitted supply after the given block, in µTari. The value is calculated by summing up the + /// block reward for each block, making this a very inefficient function if you wanted to call it from a loop for + /// example. For those cases, use the `iter` function instead. + pub fn supply_at_block(&self, block: u64) -> MicroTari { + let mut total = MicroTari::from(0u64); + for i in 0..=block { + total += self.block_reward(i); + } + total + } + + /// Return an iterator over the block reward and total supply. This is the most efficient way to iterate through + /// the emission curve if you're interested in the supply as well as the reward. + /// + /// This is an infinite iterator, and each value returned is a tuple of (block number, reward, and total supply) + /// + /// ```edition2018 + /// use tari_core::emission::EmissionSchedule; + /// use tari_core::tari_amount::MicroTari; + /// // Print the reward and supply for first 100 blocks + /// let schedule = EmissionSchedule::new(10.into(), 0.9, 1.into()); + /// for (n, reward, supply) in schedule.iter().take(100) { + /// println!("{:3} {:9} {:9}", n, reward, supply); + /// } + /// ``` + pub fn iter(&self) -> EmissionValues { + EmissionValues::new(self) + } +} + +pub struct EmissionValues<'a> { + block_num: u64, + supply: MicroTari, + reward: MicroTari, + schedule: &'a EmissionSchedule, +} + +impl<'a> EmissionValues<'a> { + fn new(schedule: &'a EmissionSchedule) -> EmissionValues<'a> { + EmissionValues { + block_num: 0, + supply: MicroTari::default(), + reward: MicroTari::default(), + schedule, + } + } +} + +impl<'a> Iterator for EmissionValues<'a> { + type Item = (u64, MicroTari, MicroTari); + + fn next(&mut self) -> Option { + let n = self.block_num; + self.reward = self.schedule.block_reward(n); + self.supply += self.reward; + self.block_num += 1; + Some((n, self.reward, self.supply)) + } +} + +#[cfg(test)] +mod test { + use crate::{emission::EmissionSchedule, tari_amount::MicroTari}; + + #[test] + fn schedule() { + let schedule = EmissionSchedule::new(MicroTari::from(10_000_000), 0.999, MicroTari::from(100)); + let r0 = schedule.block_reward(0); + assert_eq!(r0, MicroTari::from(10_000_100)); + let s0 = schedule.supply_at_block(0); + assert_eq!(s0, MicroTari::from(10_000_100)); + assert_eq!(schedule.block_reward(100), MicroTari::from(9_048_021)); + assert_eq!(schedule.supply_at_block(100), MicroTari::from(961_136_499)); + } + + #[test] + fn huge_block_number() { + let mut n = (std::i32::MAX - 1) as u64; + let schedule = EmissionSchedule::new(MicroTari::from(1e21 as u64), 0.999_9999, MicroTari::from(100)); + for _ in 0..3 { + assert_eq!(schedule.block_reward(n), MicroTari::from(100)); + n += 1; + } + } + + #[test] + fn generate_emission_schedule_as_iterator() { + let schedule = EmissionSchedule::new(MicroTari::from(10_000_000), 0.999, MicroTari::from(100)); + let values: Vec<(u64, MicroTari, MicroTari)> = schedule.iter().take(101).collect(); + assert_eq!(values[0].0, 0); + assert_eq!(values[0].1, MicroTari::from(10_000_100)); + assert_eq!(values[0].2, MicroTari::from(10_000_100)); + assert_eq!(values[100].0, 100); + assert_eq!(values[100].1, MicroTari::from(9_048_021)); + assert_eq!(values[100].2, MicroTari::from(961_136_499)); + + let mut tot_supply = MicroTari::default(); + for (_, reward, supply) in schedule.iter().take(1000) { + tot_supply += reward; + assert_eq!(tot_supply, supply); + } + } +} diff --git a/base_layer/core/src/fee.rs b/base_layer/core/src/fee.rs new file mode 100644 index 0000000000..df9962b4fe --- /dev/null +++ b/base_layer/core/src/fee.rs @@ -0,0 +1,52 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{tari_amount::*, transaction::MINIMUM_TRANSACTION_FEE}; + +pub struct Fee {} + +pub const WEIGHT_PER_INPUT: u64 = 1; +pub const WEIGHT_PER_OUTPUT: u64 = 4; +pub const BASE_COST: u64 = 1; + +impl Fee { + /// Computes the absolute transaction fee given the fee-per-gram, and the size of the transaction + pub fn calculate(fee_per_gram: MicroTari, num_inputs: usize, num_outputs: usize) -> MicroTari { + (BASE_COST + Fee::calculate_weight(num_inputs, num_outputs) * u64::from(fee_per_gram)).into() + } + + /// Computes the absolute transaction fee using `calculate`, but the resulting fee will always be at least the + /// minimum network transaction fee. + pub fn calculate_with_minimum(fee_per_gram: MicroTari, num_inputs: usize, num_outputs: usize) -> MicroTari { + let fee = Fee::calculate(fee_per_gram, num_inputs, num_outputs); + if fee < MINIMUM_TRANSACTION_FEE { + MINIMUM_TRANSACTION_FEE + } else { + fee + } + } + + /// Calculate the weight of a transaction based on the number of inputs and outputs + pub fn calculate_weight(num_inputs: usize, num_outputs: usize) -> u64 { + WEIGHT_PER_INPUT * num_inputs as u64 + WEIGHT_PER_OUTPUT * num_outputs as u64 + } +} diff --git a/base_layer/core/src/lib.rs b/base_layer/core/src/lib.rs index 0672b79a86..219ec6cda4 100644 --- a/base_layer/core/src/lib.rs +++ b/base_layer/core/src/lib.rs @@ -22,11 +22,31 @@ #[macro_use] extern crate bitflags; +#[macro_use] +extern crate lazy_static; + +#[cfg(test)] +pub mod test_utils; -pub mod block; -pub mod blockheader; -pub mod message; -pub mod pow; -pub mod range_proof; +pub mod blocks; +pub mod bullet_rangeproofs; +pub mod consts; +pub mod fee; +pub mod mempool; +pub mod proof_of_work; +#[allow(clippy::op_ref)] pub mod transaction; +pub mod transaction_protocol; pub mod types; + +pub mod consensus; +pub mod emission; +pub mod tari_amount; + +mod base_node; +// mod blockchain; TODO refactoring + +pub mod chain_storage; + +// Re-export commonly used structs +pub use transaction_protocol::{recipient::ReceiverTransactionProtocol, sender::SenderTransactionProtocol}; diff --git a/base_layer/core/src/mempool/error.rs b/base_layer/core/src/mempool/error.rs new file mode 100644 index 0000000000..bfd2b71081 --- /dev/null +++ b/base_layer/core/src/mempool/error.rs @@ -0,0 +1,29 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::mempool::unconfirmed_pool::UnconfirmedPoolError; +use derive_error::Error; + +#[derive(Debug, Error)] +pub enum MempoolError { + UnconfirmedPoolError(UnconfirmedPoolError), +} diff --git a/base_layer/core/src/mempool/mempool.rs b/base_layer/core/src/mempool/mempool.rs new file mode 100644 index 0000000000..a7c0ba03bf --- /dev/null +++ b/base_layer/core/src/mempool/mempool.rs @@ -0,0 +1,169 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + blocks::Block, + mempool::{ + error::MempoolError, + orphan_pool::{OrphanPool, OrphanPoolConfig}, + pending_pool::{PendingPool, PendingPoolConfig}, + reorg_pool::{ReorgPool, ReorgPoolConfig}, + unconfirmed_pool::{UnconfirmedPool, UnconfirmedPoolConfig}, + }, + transaction::Transaction, + types::Signature, +}; +use std::sync::Arc; + +/// Configuration for the Mempool +#[derive(Clone, Copy)] +pub struct MempoolConfig { + pub unconfirmed_pool_config: UnconfirmedPoolConfig, + pub orphan_pool_config: OrphanPoolConfig, + pub pending_pool_config: PendingPoolConfig, + pub reorg_pool_config: ReorgPoolConfig, +} + +impl Default for MempoolConfig { + fn default() -> Self { + Self { + unconfirmed_pool_config: UnconfirmedPoolConfig::default(), + orphan_pool_config: OrphanPoolConfig::default(), + pending_pool_config: PendingPoolConfig::default(), + reorg_pool_config: ReorgPoolConfig::default(), + } + } +} + +/// The Mempool consists of an Unconfirmed Transaction Pool, Pending Pool, Orphan Pool and Reorg Pool and is responsible +/// for managing and maintaining all unconfirmed transactions have not yet been included in a block, and transactions +/// that have recently been included in a block. +pub struct Mempool { + unconfirmed_pool: UnconfirmedPool, + orphan_pool: OrphanPool, + pending_pool: PendingPool, + reorg_pool: ReorgPool, +} + +impl Mempool { + /// Create a new Mempool with a UnconfirmedPool, OrphanPool, PendingPool and ReOrgPool + pub fn new(config: MempoolConfig) -> Self { + Self { + unconfirmed_pool: UnconfirmedPool::new(config.unconfirmed_pool_config), + orphan_pool: OrphanPool::new(config.orphan_pool_config), + pending_pool: PendingPool::new(config.pending_pool_config), + reorg_pool: ReorgPool::new(config.reorg_pool_config), + } + } + + /// Insert an unconfirmed transaction into the Mempool + pub fn insert(&mut self, _utx: Transaction) -> Result<(), MempoolError> { + // TODO: Verify incoming txs and check for timelocks and that valid UTXOs are spent + + // TODO: UTxs that have passed all the verification steps and checks, except they attempt to spend UTXOs that + // don't exist should be added to Orphan Pool. + + // TODO: UTxs constrained by timelocks and attempt to spend nonexistent UTXOs should be added to orphan pool. + + // TODO: UTxs that have passed all the verification steps and checks, Time-locked utxs should be added to + // Pending Pool + + // TODO: Utxs that have been received, verified and have passed all checks, don't have time-locks and only spend + // valid UTXOs should be added to UTxPool. + Ok(()) + } + + /// Insert a set of new transactions into the UTxPool + fn insert_txs(&mut self, txs: Vec) -> Result<(), MempoolError> { + for tx in txs { + self.insert(tx)?; + } + + Ok(()) + } + + /// Update the Mempool based on the received published block + pub fn process_published_block(&mut self, _published_block: &Block) -> Result<(), MempoolError> { + // Move published txs to ReOrgPool and discard double spends + // self.reorg_pool.insert_txs(self.unconfirmed_pool.remove_published_and_discard_double_spends(published_block)? + // )?; + + // Move txs with valid input UTXOs and expired time-locks to UnconfirmedPool and discard double spends + // unconfirmed_pool.insert_txs(pending_pool.remove_unlocked_and_discard_double_spends()?)?; + + // Move Time-locked txs that have input UTXOs that have recently become valid to PendingPool. Move txs with no + // or recently expired time-locks that have input UTXOs that have recently become valid to the UnconfirmedPool + // let (txs,time_locked_txs)=orphan_pool.remove_valid(published_block.header.height,utxos)?; + // pending_pool.insert_txs(time_locked_txs)?; + // unconfirmed_pool.insert_txs(txs)?; + + // Txs stored in the OrphanPool and ReOrgPool will be removed when their TTLs have been reached. + + Ok(()) + } + + /// Update the Mempool based on the received set of published blocks + pub fn process_published_blocks(&mut self, published_blocks: &Vec) -> Result<(), MempoolError> { + for published_block in published_blocks { + self.process_published_block(published_block)?; + } + Ok(()) + } + + /// In the event of a ReOrg, resubmit all ReOrged transactions into the Mempool and process each newly introduced + /// block from the latest longest chain + pub fn process_reorg(&mut self, _removed_blocks: Vec, _new_blocks: Vec) -> Result<(), MempoolError> { + // let reorg_txs=self.reorg_pool.scan_for_and_remove_reorged_txs(removed_blocks); + // self.insert_txs(reorg_txs)?; + // self.process_published_blocks(&new_blocks)?; + Ok(()) + } + + /// Returns all unconfirmed transaction stored in the Mempool, except the transactions stored in the ReOrgPool. + // TODO: Investigate returning an iterator rather than a large vector of transactions + pub fn snapshot(&self) -> Result>, MempoolError> { + // return content of UnconfirmedPool, OrphanPool and PendingPool + + Ok(Vec::new()) + } + + /// Returns a list of transaction ranked by transaction priority up to a given weight. + pub fn retrieve(&self, _total_weight: usize) -> Result>, MempoolError> { + Ok(Vec::new()) + } + + /// Check if the specified transaction is stored in the Mempool. + pub fn has_tx_with_excess_sig(&self, _excess_sig: &Signature) -> Result<(), MempoolError> { + // Return Some(Sub-pool enum) or None when it is not stored in the Mempool + + Ok(()) + } + + /// Returns the Mempool stats for the Mempool + pub fn stats(&self) -> Result<(), MempoolError> { + // Return the stats of the Mempool, including subpools (OrphanPool, PendingPool and ReOrgPool). The number of + // unconfirmed transactions. The number of orphaned transactions. The current size of the mempool (in + // transaction weight). + + Ok(()) + } +} diff --git a/base_layer/core/src/mempool/mod.rs b/base_layer/core/src/mempool/mod.rs new file mode 100644 index 0000000000..3c5a2e58ea --- /dev/null +++ b/base_layer/core/src/mempool/mod.rs @@ -0,0 +1,33 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod mempool; +mod orphan_pool; +mod pending_pool; +mod priority; +mod reorg_pool; +mod unconfirmed_pool; + +// Public re-exports +pub use error::MempoolError; +pub use mempool::Mempool; diff --git a/base_layer/core/src/mempool/orphan_pool/mod.rs b/base_layer/core/src/mempool/orphan_pool/mod.rs new file mode 100644 index 0000000000..1304d842de --- /dev/null +++ b/base_layer/core/src/mempool/orphan_pool/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod orphan_pool; + +// Public re-exports +pub use orphan_pool::{OrphanPool, OrphanPoolConfig}; diff --git a/base_layer/core/src/mempool/orphan_pool/orphan_pool.rs b/base_layer/core/src/mempool/orphan_pool/orphan_pool.rs new file mode 100644 index 0000000000..127261a188 --- /dev/null +++ b/base_layer/core/src/mempool/orphan_pool/orphan_pool.rs @@ -0,0 +1,240 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + consts::{MEMPOOL_ORPHAN_POOL_CACHE_TTL, MEMPOOL_ORPHAN_POOL_STORAGE_CAPACITY}, + transaction::{Transaction, TransactionInput}, + types::Signature, +}; +use std::{sync::Arc, time::Duration}; +use ttl_cache::TtlCache; + +/// Configuration for the OrphanPool +#[derive(Clone, Copy)] +pub struct OrphanPoolConfig { + /// The maximum number of transactions that can be stored in the Orphan pool + pub storage_capacity: usize, + /// The Time-to-live for each stored transaction + pub tx_ttl: Duration, +} + +impl Default for OrphanPoolConfig { + fn default() -> Self { + Self { + storage_capacity: MEMPOOL_ORPHAN_POOL_STORAGE_CAPACITY, + tx_ttl: MEMPOOL_ORPHAN_POOL_CACHE_TTL, + } + } +} + +/// The Orphan Pool contains all the received transactions that attempt to spend UTXOs that don't exist. These UTXOs +/// might exist in the future if these transactions are from a series or set of transactions that need to be processed +/// in a specific order. Some of these transactions might still be constrained by pending time-locks. +pub struct OrphanPool { + config: OrphanPoolConfig, + txs_by_signature: TtlCache>, +} + +impl OrphanPool { + /// Create a new OrphanPool with the specified configuration + pub fn new(config: OrphanPoolConfig) -> Self { + Self { + config, + txs_by_signature: TtlCache::new(config.storage_capacity), + } + } + + /// Insert a new transaction into the OrphanPool. Orphaned transactions will have a limited Time-to-live and will be + /// discarded if the UTXOs they require are not created before the Time-to-live threshold is reached. + pub fn insert(&mut self, tx: Transaction) { + let tx_key = tx.body.kernels[0].excess_sig.clone(); + let _ = self.txs_by_signature.insert(tx_key, Arc::new(tx), self.config.tx_ttl); + } + + /// Insert a set of new transactions into the OrphanPool + pub fn insert_txs(&mut self, txs: Vec) { + for tx in txs.into_iter() { + self.insert(tx); + } + } + + /// Check if a transaction is stored in the OrphanPool + pub fn has_tx_with_excess_sig(&self, excess_sig: &Signature) -> bool { + self.txs_by_signature.contains_key(excess_sig) + } + + /// Check if the required UTXOs have been created and if the status of any of the transactions in the OrphanPool has + /// changed. Remove valid transactions and valid transactions with time-locks from the OrphanPool. + // TODO: A reference to the UTXO set should not be passed in like this, but rather a handle or stream to the Chain + // (BlockchainDatabase or BlockchainService) should be provided to the OrphanPool during creation allowing a + // Chain call to be used to query the current block_height and UTXO set + pub fn scan_for_and_remove_unorphaned_txs( + &mut self, + block_height: u64, + utxos: &[TransactionInput], + ) -> (Vec>, Vec>) + { + let mut removed_tx_keys: Vec = Vec::new(); + let mut removed_timelocked_tx_keys: Vec = Vec::new(); + for (tx_key, tx) in self.txs_by_signature.iter() { + if tx.body.inputs.iter().all(|input| utxos.contains(input)) { + if tx.body.kernels[0].lock_height <= block_height { + removed_tx_keys.push(tx_key.clone()); + } else { + removed_timelocked_tx_keys.push(tx_key.clone()); + } + } + } + + let mut removed_txs: Vec> = Vec::with_capacity(removed_tx_keys.len()); + removed_tx_keys.iter().for_each(|tx_key| { + if let Some(tx) = self.txs_by_signature.remove(&tx_key) { + removed_txs.push(tx); + } + }); + + let mut removed_timelocked_txs: Vec> = Vec::with_capacity(removed_timelocked_tx_keys.len()); + removed_timelocked_tx_keys.iter().for_each(|tx_key| { + if let Some(tx) = self.txs_by_signature.remove(&tx_key) { + removed_timelocked_txs.push(tx); + } + }); + + (removed_txs, removed_timelocked_txs) + } + + /// Returns the total number of orphaned transactions stored in the OrphanPool + pub fn len(&mut self) -> usize { + let mut count = 0; + self.txs_by_signature.iter().for_each(|_| count += 1); + (count) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + tari_amount::MicroTari, + test_utils::builders::{create_test_block, create_test_tx, extract_outputs_as_inputs}, + transaction::TransactionInput, + }; + use std::{thread, time::Duration}; + + #[test] + fn test_insert_rlu_and_ttl() { + let tx1 = create_test_tx(MicroTari(10_000), MicroTari(500), 4000, 2, 1); + let tx2 = create_test_tx(MicroTari(10_000), MicroTari(300), 3000, 2, 1); + let tx3 = create_test_tx(MicroTari(10_000), MicroTari(100), 2500, 2, 1); + let tx4 = create_test_tx(MicroTari(10_000), MicroTari(200), 1000, 2, 1); + let tx5 = create_test_tx(MicroTari(10_000), MicroTari(500), 2000, 2, 1); + let tx6 = create_test_tx(MicroTari(10_000), MicroTari(600), 5500, 2, 1); + + let mut orphan_pool = OrphanPool::new(OrphanPoolConfig { + storage_capacity: 3, + tx_ttl: Duration::from_millis(50), + }); + orphan_pool.insert_txs(vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]); + // Check that oldest utx was removed to make room for new incoming transaction + assert!(!orphan_pool.has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig)); + assert!(orphan_pool.has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig)); + assert!(orphan_pool.has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig)); + assert!(orphan_pool.has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig)); + + // Check that transactions that have been in the pool for longer than their Time-to-live have been removed + thread::sleep(Duration::from_millis(51)); + orphan_pool.insert_txs(vec![tx5.clone(), tx6.clone()]); + assert_eq!( + orphan_pool.has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig), + false + ); + assert_eq!( + orphan_pool.has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig), + false + ); + assert_eq!( + orphan_pool.has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig), + false + ); + assert_eq!( + orphan_pool.has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig), + false + ); + assert_eq!( + orphan_pool.has_tx_with_excess_sig(&tx5.body.kernels[0].excess_sig), + true + ); + assert_eq!( + orphan_pool.has_tx_with_excess_sig(&tx6.body.kernels[0].excess_sig), + true + ); + assert_eq!(orphan_pool.len(), 2); + } + + #[test] + fn remove_remove_valid() { + let tx1 = create_test_tx(MicroTari(10_000), MicroTari(500), 1100, 2, 1); + let tx2 = create_test_tx(MicroTari(10_000), MicroTari(300), 1700, 2, 1); + let tx3 = create_test_tx(MicroTari(10_000), MicroTari(100), 2500, 1, 1); + let mut tx4 = create_test_tx(MicroTari(10_000), MicroTari(200), 3100, 2, 1); + let mut tx5 = create_test_tx(MicroTari(10_000), MicroTari(500), 4500, 2, 1); + let tx6 = create_test_tx(MicroTari(10_000), MicroTari(600), 5200, 2, 1); + // Publishing of tx1 and tx2 will create the UTXOs required by tx4 and tx5 + tx4.body.inputs.clear(); + tx1.body + .outputs + .iter() + .for_each(|output| tx4.body.inputs.push(TransactionInput::from(output.clone()))); + + tx5.body.inputs.clear(); + tx2.body + .outputs + .iter() + .for_each(|output| tx5.body.inputs.push(TransactionInput::from(output.clone()))); + + let mut orphan_pool = OrphanPool::new(OrphanPoolConfig::default()); + orphan_pool.insert_txs(vec![tx3.clone(), tx4.clone(), tx5.clone()]); + + let published_block = create_test_block(3000, vec![tx6.clone()]); + let mut utxos = Vec::new(); + extract_outputs_as_inputs(&mut utxos, &published_block); + let (txs, timelocked_txs) = + orphan_pool.scan_for_and_remove_unorphaned_txs(published_block.header.height, &utxos); + assert_eq!(orphan_pool.len(), 3); + assert_eq!(txs.len(), 0); + assert_eq!(timelocked_txs.len(), 0); + assert!(orphan_pool.has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig)); + assert!(orphan_pool.has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig)); + assert!(orphan_pool.has_tx_with_excess_sig(&tx5.body.kernels[0].excess_sig)); + + let published_block = create_test_block(3500, vec![tx1.clone(), tx2.clone()]); + extract_outputs_as_inputs(&mut utxos, &published_block); + let (txs, timelocked_txs) = + orphan_pool.scan_for_and_remove_unorphaned_txs(published_block.header.height, &utxos); + assert_eq!(orphan_pool.len(), 1); + assert_eq!(txs.len(), 1); + assert_eq!(timelocked_txs.len(), 1); + assert!(orphan_pool.has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig)); + assert!(txs.iter().any(|tx| **tx == tx4)); + assert!(timelocked_txs.iter().any(|tx| **tx == tx5)); + } +} diff --git a/infrastructure/comms/src/connection/mod.rs b/base_layer/core/src/mempool/pending_pool/error.rs similarity index 84% rename from infrastructure/comms/src/connection/mod.rs rename to base_layer/core/src/mempool/pending_pool/error.rs index 0de90adfd5..c94d472e0c 100644 --- a/infrastructure/comms/src/connection/mod.rs +++ b/base_layer/core/src/mempool/pending_pool/error.rs @@ -20,20 +20,14 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub mod i2p; -pub mod net_address; -pub mod onion; -pub mod p2p; - +use crate::mempool::priority::PriorityError; use derive_error::Error; -pub use self::net_address::{NetAddress, NetAddressError}; - #[derive(Debug, Error)] -pub enum ConnectionError { - NetAddressError(NetAddressError), - /// Connection timed out - Timeout, +pub enum PendingPoolError { + /// The HashMap and BTreeMap are out of sync + StorageOutofSync, + /// The Thread Safety has been breached and the data access has become poisoned + PoisonedAccess, + PriorityError(PriorityError), } - -pub trait Connection {} diff --git a/base_layer/core/src/mempool/pending_pool/mod.rs b/base_layer/core/src/mempool/pending_pool/mod.rs new file mode 100644 index 0000000000..c0654f26da --- /dev/null +++ b/base_layer/core/src/mempool/pending_pool/mod.rs @@ -0,0 +1,30 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod pending_pool; +mod pending_pool_storage; + +// Public re-exports +pub use error::PendingPoolError; +pub use pending_pool::{PendingPool, PendingPoolConfig}; +pub use pending_pool_storage::PendingPoolStorage; diff --git a/base_layer/core/src/mempool/pending_pool/pending_pool.rs b/base_layer/core/src/mempool/pending_pool/pending_pool.rs new file mode 100644 index 0000000000..39313ffa85 --- /dev/null +++ b/base_layer/core/src/mempool/pending_pool/pending_pool.rs @@ -0,0 +1,239 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + blocks::Block, + consts::MEMPOOL_PENDING_POOL_STORAGE_CAPACITY, + mempool::pending_pool::{PendingPoolError, PendingPoolStorage}, + transaction::Transaction, + types::Signature, +}; +use std::sync::{Arc, RwLock}; + +/// Configuration for the PendingPool. +#[derive(Clone, Copy)] +pub struct PendingPoolConfig { + /// The maximum number of transactions that can be stored in the Pending pool. + pub storage_capacity: usize, +} + +impl Default for PendingPoolConfig { + fn default() -> Self { + Self { + storage_capacity: MEMPOOL_PENDING_POOL_STORAGE_CAPACITY, + } + } +} + +/// The Pending Pool contains all transactions that are restricted by time-locks. Once the time-locks have expired then +/// the transactions can be moved to the Unconfirmed Transaction Pool for inclusion in future blocks. +pub struct PendingPool { + pool_storage: RwLock, +} + +impl PendingPool { + /// Create a new PendingPool with the specified configuration. + pub fn new(config: PendingPoolConfig) -> Self { + Self { + pool_storage: RwLock::new(PendingPoolStorage::new(config)), + } + } + + /// Insert a new transaction into the PendingPool. Low priority transactions will be removed to make space for + /// higher priority transactions. The lowest priority transactions will be removed when the maximum capacity is + /// reached and the new transaction has a higher priority than the currently stored lowest priority transaction. + pub fn insert(&mut self, transaction: Transaction) -> Result<(), PendingPoolError> { + self.pool_storage + .write() + .map_err(|_| PendingPoolError::PoisonedAccess)? + .insert(transaction) + } + + /// Insert a set of new transactions into the PendingPool. + pub fn insert_txs(&mut self, transactions: Vec) -> Result<(), PendingPoolError> { + self.pool_storage + .write() + .map_err(|_| PendingPoolError::PoisonedAccess)? + .insert_txs(transactions) + } + + /// Check if a specific transaction is available in the PendingPool. + pub fn has_tx_with_excess_sig(&self, excess_sig: &Signature) -> Result { + Ok(self + .pool_storage + .read() + .map_err(|_| PendingPoolError::PoisonedAccess)? + .has_tx_with_excess_sig(excess_sig)) + } + + /// Remove transactions with expired time-locks so that they can be move to the UnconfirmedPool. Double spend + /// transactions are also removed. + pub fn remove_unlocked_and_discard_double_spends( + &mut self, + published_block: &Block, + ) -> Result>, PendingPoolError> + { + self.pool_storage + .write() + .map_err(|_| PendingPoolError::PoisonedAccess)? + .remove_unlocked_and_discard_double_spends(published_block) + } + + /// Returns the total number of time-locked transactions stored in the PendingPool. + pub fn len(&self) -> Result { + Ok(self + .pool_storage + .read() + .map_err(|_| PendingPoolError::PoisonedAccess)? + .len()) + } + + #[cfg(test)] + /// Checks the consistency status of the Hashmap and BtreeMaps. + pub fn check_status(&self) -> Result { + Ok(self + .pool_storage + .read() + .map_err(|_| PendingPoolError::PoisonedAccess)? + .check_status()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + tari_amount::MicroTari, + test_utils::builders::{create_test_block, create_test_tx}, + }; + + #[test] + fn test_insert_and_lru() { + let tx1 = create_test_tx(MicroTari(10_000), MicroTari(500), 500, 2, 1); + let tx2 = create_test_tx(MicroTari(10_000), MicroTari(100), 2150, 1, 2); + let tx3 = create_test_tx(MicroTari(10_000), MicroTari(1000), 1000, 2, 1); + let tx4 = create_test_tx(MicroTari(10_000), MicroTari(200), 2450, 2, 2); + let tx5 = create_test_tx(MicroTari(10_000), MicroTari(500), 1000, 3, 3); + let tx6 = create_test_tx(MicroTari(10_000), MicroTari(750), 1850, 2, 2); + + let mut pending_pool = PendingPool::new(PendingPoolConfig { storage_capacity: 3 }); + pending_pool + .insert_txs(vec![ + tx1.clone(), + tx2.clone(), + tx3.clone(), + tx4.clone(), + tx5.clone(), + tx6.clone(), + ]) + .unwrap(); + // Check that lowest priority txs were removed to make room for higher priority transactions + assert_eq!(pending_pool.len().unwrap(), 3); + assert_eq!( + pending_pool + .has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig) + .unwrap(), + true + ); + assert_eq!( + pending_pool + .has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + pending_pool + .has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig) + .unwrap(), + true + ); + assert_eq!( + pending_pool + .has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + pending_pool + .has_tx_with_excess_sig(&tx5.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + pending_pool + .has_tx_with_excess_sig(&tx6.body.kernels[0].excess_sig) + .unwrap(), + true + ); + + assert!(pending_pool.check_status().unwrap()); + } + + #[test] + fn test_remove_unlocked_and_discard_double_spends() { + let tx1 = create_test_tx(MicroTari(10_000), MicroTari(500), 500, 2, 1); + let tx2 = create_test_tx(MicroTari(10_000), MicroTari(100), 2150, 1, 2); + let tx3 = create_test_tx(MicroTari(10_000), MicroTari(1000), 1000, 2, 1); + let tx4 = create_test_tx(MicroTari(10_000), MicroTari(200), 2450, 2, 2); + let tx5 = create_test_tx(MicroTari(10_000), MicroTari(500), 1000, 3, 3); + let tx6 = create_test_tx(MicroTari(10_000), MicroTari(750), 1450, 2, 2); + + let mut pending_pool = PendingPool::new(PendingPoolConfig { storage_capacity: 10 }); + pending_pool + .insert_txs(vec![ + tx1.clone(), + tx2.clone(), + tx3.clone(), + tx4.clone(), + tx5.clone(), + tx6.clone(), + ]) + .unwrap(); + assert_eq!(pending_pool.len().unwrap(), 6); + + let published_block = create_test_block(1500, vec![tx6.clone()]); + let unlocked_txs = pending_pool + .remove_unlocked_and_discard_double_spends(&published_block) + .unwrap(); + + assert_eq!(pending_pool.len().unwrap(), 2); + assert_eq!( + pending_pool + .has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig) + .unwrap(), + true + ); + assert_eq!( + pending_pool + .has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig) + .unwrap(), + true + ); + + assert_eq!(unlocked_txs.len(), 3); + assert!(unlocked_txs.iter().any(|tx| **tx == tx1)); + assert!(unlocked_txs.iter().any(|tx| **tx == tx3)); + assert!(unlocked_txs.iter().any(|tx| **tx == tx5)); + + assert!(pending_pool.check_status().unwrap()); + } +} diff --git a/base_layer/core/src/mempool/pending_pool/pending_pool_storage.rs b/base_layer/core/src/mempool/pending_pool/pending_pool_storage.rs new file mode 100644 index 0000000000..fd169f5eda --- /dev/null +++ b/base_layer/core/src/mempool/pending_pool/pending_pool_storage.rs @@ -0,0 +1,194 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + blocks::Block, + mempool::{ + pending_pool::{PendingPoolConfig, PendingPoolError}, + priority::{FeePriority, TimelockPriority, TimelockedTransaction}, + }, + transaction::Transaction, + types::Signature, +}; +use std::{ + collections::{BTreeMap, HashMap}, + convert::TryFrom, + sync::Arc, +}; + +/// PendingPool makes use of PendingPoolStorage to provide thread safe access to its Hashmap and BTreeMaps. +/// The txs_by_signature HashMap is used to find a transaction using its excess_sig, this functionality is used to match +/// transactions included in blocks with transactions stored in the pool. +/// The txs_by_fee_priority BTreeMap prioritize the transactions in the pool according to FeePriority, it allows +/// transactions to be inserted in sorted order based on their priority. The txs_by_timelock_priority BTreeMap +/// prioritize the transactions in the pool according to TimelockPriority, it allows transactions to be inserted in +/// sorted order based on the expiry of their time-locks. +pub struct PendingPoolStorage { + config: PendingPoolConfig, + txs_by_signature: HashMap, + txs_by_fee_priority: BTreeMap, + txs_by_timelock_priority: BTreeMap, +} + +impl PendingPoolStorage { + /// Create a new PendingPoolStorage with the specified configuration + pub fn new(config: PendingPoolConfig) -> Self { + Self { + config, + txs_by_signature: HashMap::new(), + txs_by_fee_priority: BTreeMap::new(), + txs_by_timelock_priority: BTreeMap::new(), + } + } + + fn lowest_fee_priority(&self) -> &FeePriority { + self.txs_by_fee_priority.iter().next().unwrap().0 + } + + fn remove_tx_with_lowest_fee_priority(&mut self) { + if let Some((_, tx_key)) = self + .txs_by_fee_priority + .iter() + .next() + .map(|(p, s)| (p.clone(), s.clone())) + { + if let Some(removed_tx) = self.txs_by_signature.remove(&tx_key) { + self.txs_by_fee_priority.remove(&removed_tx.fee_priority); + self.txs_by_timelock_priority.remove(&removed_tx.timelock_priority); + } + } + } + + /// Insert a new transaction into the PendingPoolStorage. Low priority transactions will be removed to make space + /// for higher priority transactions. + pub fn insert(&mut self, tx: Transaction) -> Result<(), PendingPoolError> { + let tx_key = tx.body.kernels[0].excess_sig.clone(); + if !self.txs_by_signature.contains_key(&tx_key) { + let prioritized_tx = TimelockedTransaction::try_from(tx)?; + if self.txs_by_signature.len() >= self.config.storage_capacity { + if prioritized_tx.fee_priority < *self.lowest_fee_priority() { + return Ok(()); + } + self.remove_tx_with_lowest_fee_priority(); + } + + self.txs_by_fee_priority + .insert(prioritized_tx.fee_priority.clone(), tx_key.clone()); + self.txs_by_timelock_priority + .insert(prioritized_tx.timelock_priority.clone(), tx_key.clone()); + self.txs_by_signature.insert(tx_key, prioritized_tx); + } + Ok(()) + } + + /// Insert a set of new transactions into the PendingPoolStorage + pub fn insert_txs(&mut self, txs: Vec) -> Result<(), PendingPoolError> { + for tx in txs.into_iter() { + self.insert(tx)?; + } + Ok(()) + } + + /// Check if a transaction is stored in the PendingPoolStorage + pub fn has_tx_with_excess_sig(&self, excess_sig: &Signature) -> bool { + self.txs_by_signature.contains_key(excess_sig) + } + + /// Remove double-spends from the PendingPoolStorage. These transactions were orphaned by the provided published + /// block. Check if any of the unspent transactions in the PendingPool has inputs that was spent by the provided + /// published block. + fn discard_double_spends(&mut self, published_block: &Block) { + let mut removed_tx_keys: Vec = Vec::new(); + for (tx_key, ptx) in self.txs_by_signature.iter() { + for input in &ptx.transaction.body.inputs { + if published_block.body.inputs.contains(input) { + self.txs_by_fee_priority.remove(&ptx.fee_priority); + self.txs_by_timelock_priority.remove(&ptx.timelock_priority); + removed_tx_keys.push(tx_key.clone()); + } + } + } + + for tx_key in &removed_tx_keys { + self.txs_by_signature.remove(&tx_key); + } + } + + /// Remove all published transactions from the UnconfirmedPoolStorage and discard double spends + pub fn remove_unlocked_and_discard_double_spends( + &mut self, + published_block: &Block, + ) -> Result>, PendingPoolError> + { + self.discard_double_spends(published_block); + + let mut removed_txs: Vec> = Vec::new(); + let mut removed_tx_keys: Vec = Vec::new(); + for (_, tx_key) in self.txs_by_timelock_priority.iter() { + if self + .txs_by_signature + .get(tx_key) + .ok_or(PendingPoolError::StorageOutofSync)? + .transaction + .body + .kernels[0] + .lock_height > + published_block.header.height + { + break; + } + + if let Some(removed_ptx) = self.txs_by_signature.remove(&tx_key) { + self.txs_by_fee_priority.remove(&removed_ptx.fee_priority); + removed_tx_keys.push(removed_ptx.timelock_priority); + removed_txs.push(removed_ptx.transaction); + } + } + + for tx_key in &removed_tx_keys { + self.txs_by_timelock_priority.remove(&tx_key); + } + + Ok(removed_txs) + } + + /// Returns the total number of unconfirmed transactions stored in the PendingPoolStorage + pub fn len(&self) -> usize { + self.txs_by_signature.len() + } + + #[cfg(test)] + /// Checks the consistency status of the Hashmap and the BtreeMaps + pub fn check_status(&self) -> bool { + if (self.txs_by_fee_priority.len() != self.txs_by_signature.len()) || + (self.txs_by_timelock_priority.len() != self.txs_by_signature.len()) + { + return false; + } + self.txs_by_fee_priority + .iter() + .all(|(_, tx_key)| self.txs_by_signature.contains_key(tx_key)) && + self.txs_by_timelock_priority + .iter() + .all(|(_, tx_key)| self.txs_by_signature.contains_key(tx_key)) + } +} diff --git a/base_layer/core/src/mempool/priority/error.rs b/base_layer/core/src/mempool/priority/error.rs new file mode 100644 index 0000000000..912a4a40fd --- /dev/null +++ b/base_layer/core/src/mempool/priority/error.rs @@ -0,0 +1,29 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; +use tari_utilities::message_format::MessageFormatError; + +#[derive(Debug, Error)] +pub enum PriorityError { + MessageFormatError(MessageFormatError), +} diff --git a/base_layer/core/src/mempool/priority/mod.rs b/base_layer/core/src/mempool/priority/mod.rs new file mode 100644 index 0000000000..cbcaf3a75f --- /dev/null +++ b/base_layer/core/src/mempool/priority/mod.rs @@ -0,0 +1,30 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod prioritized_transaction; +mod timelocked_transaction; + +// Public re-exports +pub use error::PriorityError; +pub use prioritized_transaction::{FeePriority, PrioritizedTransaction}; +pub use timelocked_transaction::{TimelockPriority, TimelockedTransaction}; diff --git a/base_layer/core/src/mempool/priority/prioritized_transaction.rs b/base_layer/core/src/mempool/priority/prioritized_transaction.rs new file mode 100644 index 0000000000..585540fb02 --- /dev/null +++ b/base_layer/core/src/mempool/priority/prioritized_transaction.rs @@ -0,0 +1,67 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{mempool::priority::PriorityError, transaction::Transaction}; +use std::{convert::TryFrom, sync::Arc}; +use tari_utilities::message_format::MessageFormat; + +/// Create a unique unspent transaction priority based on the transaction fee, age of the oldest input UTXO and the +/// excess_sig. The excess_sig is included to ensure the the priority key unique so it can be used with a BTreeMap. +/// Normally, duplicate keys will be overwritten in a BTreeMap. +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct FeePriority(Vec); + +impl FeePriority { + pub fn try_from(transaction: &Transaction) -> Result { + let fee_per_byte = (transaction.calculate_ave_fee_per_gram() * 1000.0) as usize; // Include 3 decimal places before flooring + let mut priority = fee_per_byte.to_binary()?; + priority.reverse(); // Fee needs to be in Big-endian for sorting with BtreeMap to work correctly + // TODO: Add oldest input UTXO age + priority.append(&mut transaction.body.kernels[0].to_binary()?); + Ok(Self(priority)) + } +} + +impl Clone for FeePriority { + fn clone(&self) -> Self { + FeePriority(self.0.clone()) + } +} + +/// A prioritized transaction includes a transaction and the calculated priority of the transaction. +pub struct PrioritizedTransaction { + pub transaction: Arc, + pub priority: FeePriority, + pub weight: u64, +} + +impl TryFrom for PrioritizedTransaction { + type Error = PriorityError; + + fn try_from(transaction: Transaction) -> Result { + Ok(Self { + priority: FeePriority::try_from(&transaction)?, + weight: transaction.calculate_weight(), + transaction: Arc::new(transaction), + }) + } +} diff --git a/base_layer/core/src/mempool/priority/timelocked_transaction.rs b/base_layer/core/src/mempool/priority/timelocked_transaction.rs new file mode 100644 index 0000000000..067b470bf7 --- /dev/null +++ b/base_layer/core/src/mempool/priority/timelocked_transaction.rs @@ -0,0 +1,69 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + mempool::priority::{FeePriority, PriorityError}, + transaction::Transaction, +}; +use std::{convert::TryFrom, sync::Arc}; +use tari_utilities::message_format::MessageFormat; + +/// Create a unique transaction priority based on the lock_height and the excess_sig, allowing transactions to be sorted +/// according to their time-lock expiry. The excess_sig is included to ensure the priority key is unique so it can be +/// used with a BTreeMap. +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct TimelockPriority(Vec); + +impl TimelockPriority { + pub fn try_from(transaction: &Transaction) -> Result { + let mut priority = transaction.body.kernels[0].lock_height.to_binary()?; + priority.reverse(); // Timelock needs to be in Big-endian for sorting with BtreeMap to work correctly + priority.append(&mut transaction.body.kernels[0].to_binary()?); + Ok(Self(priority)) + } +} + +impl Clone for TimelockPriority { + fn clone(&self) -> Self { + TimelockPriority(self.0.clone()) + } +} + +/// A Timelocked prioritized transaction includes a transaction and the calculated FeePriority and TimelockPriority of +/// the transaction. +pub struct TimelockedTransaction { + pub transaction: Arc, + pub fee_priority: FeePriority, + pub timelock_priority: TimelockPriority, +} + +impl TryFrom for TimelockedTransaction { + type Error = PriorityError; + + fn try_from(transaction: Transaction) -> Result { + Ok(Self { + fee_priority: FeePriority::try_from(&transaction)?, + timelock_priority: TimelockPriority::try_from(&transaction)?, + transaction: Arc::new(transaction), + }) + } +} diff --git a/base_layer/blockchain/src/store.rs b/base_layer/core/src/mempool/reorg_pool/mod.rs similarity index 92% rename from base_layer/blockchain/src/store.rs rename to base_layer/core/src/mempool/reorg_pool/mod.rs index d649147c3c..7ab7b6ce50 100644 --- a/base_layer/blockchain/src/store.rs +++ b/base_layer/core/src/mempool/reorg_pool/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2018 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -20,6 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// This file is where the database lives +mod reorg_pool; -pub struct Store {} +// Public re-exports +pub use reorg_pool::{ReorgPool, ReorgPoolConfig}; diff --git a/base_layer/core/src/mempool/reorg_pool/reorg_pool.rs b/base_layer/core/src/mempool/reorg_pool/reorg_pool.rs new file mode 100644 index 0000000000..2ff79790f2 --- /dev/null +++ b/base_layer/core/src/mempool/reorg_pool/reorg_pool.rs @@ -0,0 +1,231 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + blocks::Block, + consts::{MEMPOOL_REORG_POOL_CACHE_TTL, MEMPOOL_REORG_POOL_STORAGE_CAPACITY}, + transaction::Transaction, + types::Signature, +}; +use std::{sync::Arc, time::Duration}; +use ttl_cache::TtlCache; + +/// Configuration for the ReorgPool +#[derive(Clone, Copy)] +pub struct ReorgPoolConfig { + /// The maximum number of transactions that can be stored in the ReorgPool + pub storage_capacity: usize, + /// The Time-to-live for each stored transaction + pub tx_ttl: Duration, +} + +impl Default for ReorgPoolConfig { + fn default() -> Self { + Self { + storage_capacity: MEMPOOL_REORG_POOL_STORAGE_CAPACITY, + tx_ttl: MEMPOOL_REORG_POOL_CACHE_TTL, + } + } +} + +/// The ReorgPool consists of all transactions that have recently been added to blocks. +/// When a potential blockchain reorganization occurs the transactions can be recovered from the ReorgPool and can be +/// added back into the UnconfirmedPool. Transactions in the ReOrg pool have a limited Time-to-live and will be removed +/// from the pool when the Time-to-live thresholds is reached. Also, when the capacity of the pool has been reached, the +/// oldest transactions will be removed to make space for incoming transactions. +pub struct ReorgPool { + config: ReorgPoolConfig, + txs_by_signature: TtlCache>, +} + +impl ReorgPool { + /// Create a new ReorgPool with the specified configuration + pub fn new(config: ReorgPoolConfig) -> Self { + Self { + config, + txs_by_signature: TtlCache::new(config.storage_capacity), + } + } + + /// Insert a new transaction into the ReorgPool. Published transactions will have a limited Time-to-live in the + /// ReorgPool and will be discarded once the Time-to-live threshold has been reached. + pub fn insert(&mut self, tx: Transaction) { + let tx_key = tx.body.kernels[0].excess_sig.clone(); + let _ = self.txs_by_signature.insert(tx_key, Arc::new(tx), self.config.tx_ttl); + } + + /// Insert a set of new transactions into the ReorgPool + pub fn insert_txs(&mut self, txs: Vec) { + for tx in txs.into_iter() { + self.insert(tx); + } + } + + /// Check if a transaction is stored in the ReorgPool + pub fn has_tx_with_excess_sig(&self, excess_sig: &Signature) -> bool { + self.txs_by_signature.contains_key(excess_sig) + } + + /// Remove the transactions from the ReorgPool that were used in provided removed blocks. The transactions can be + /// resubmitted to the Unconfirmed Pool. + pub fn scan_for_and_remove_reorged_txs(&mut self, removed_blocks: Vec) -> Vec> { + let mut removed_txs: Vec> = Vec::new(); + for block in &removed_blocks { + for kernel in &block.body.kernels { + if let Some(removed_tx) = self.txs_by_signature.remove(&kernel.excess_sig) { + removed_txs.push(removed_tx); + } + } + } + removed_txs + } + + /// Returns the total number of published transactions stored in the ReorgPool + pub fn len(&mut self) -> usize { + let mut count = 0; + self.txs_by_signature.iter().for_each(|_| count += 1); + (count) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + tari_amount::MicroTari, + test_utils::builders::{create_test_block, create_test_tx}, + transaction::TransactionInput, + }; + use std::{thread, time::Duration}; + + #[test] + fn test_insert_rlu_and_ttl() { + let tx1 = create_test_tx(MicroTari(10_000), MicroTari(500), 4000, 2, 1); + let tx2 = create_test_tx(MicroTari(10_000), MicroTari(300), 3000, 2, 1); + let tx3 = create_test_tx(MicroTari(10_000), MicroTari(100), 2500, 2, 1); + let tx4 = create_test_tx(MicroTari(10_000), MicroTari(200), 1000, 2, 1); + let tx5 = create_test_tx(MicroTari(10_000), MicroTari(500), 2000, 2, 1); + let tx6 = create_test_tx(MicroTari(10_000), MicroTari(600), 5500, 2, 1); + + let mut reorg_pool = ReorgPool::new(ReorgPoolConfig { + storage_capacity: 3, + tx_ttl: Duration::from_millis(50), + }); + reorg_pool.insert_txs(vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]); + // Check that oldest utx was removed to make room for new incoming transactions + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig), + false + ); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig), true); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig), true); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig), true); + + // Check that transactions that have been in the pool for longer than their Time-to-live have been removed + thread::sleep(Duration::from_millis(51)); + reorg_pool.insert_txs(vec![tx5.clone(), tx6.clone()]); + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig), + false + ); + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig), + false + ); + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig), + false + ); + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig), + false + ); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx5.body.kernels[0].excess_sig), true); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx6.body.kernels[0].excess_sig), true); + assert_eq!(reorg_pool.len(), 2); + } + + #[test] + fn remove_scan_for_and_remove_reorged_txs() { + let tx1 = create_test_tx(MicroTari(10_000), MicroTari(500), 4000, 2, 1); + let tx2 = create_test_tx(MicroTari(10_000), MicroTari(300), 3000, 2, 1); + let tx3 = create_test_tx(MicroTari(10_000), MicroTari(100), 2500, 2, 1); + let tx4 = create_test_tx(MicroTari(10_000), MicroTari(200), 1000, 2, 1); + let tx5 = create_test_tx(MicroTari(10_000), MicroTari(500), 2000, 2, 1); + let tx6 = create_test_tx(MicroTari(10_000), MicroTari(600), 5500, 2, 1); + + let mut reorg_pool = ReorgPool::new(ReorgPoolConfig { + storage_capacity: 5, + tx_ttl: Duration::from_millis(50), + }); + reorg_pool.insert_txs(vec![ + tx1.clone(), + tx2.clone(), + tx3.clone(), + tx4.clone(), + tx5.clone(), + tx6.clone(), + ]); + // Oldest transaction tx1 is removed to make space for new incoming transactions + assert_eq!(reorg_pool.len(), 5); + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig), + false + ); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig), true); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig), true); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig), true); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx5.body.kernels[0].excess_sig), true); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx6.body.kernels[0].excess_sig), true); + + let reorg_blocks = vec![ + create_test_block(3000, vec![tx3.clone(), tx4.clone()]), + create_test_block(4000, vec![tx1.clone(), tx2.clone()]), + ]; + + let removed_txs = reorg_pool.scan_for_and_remove_reorged_txs(reorg_blocks); + assert_eq!(removed_txs.len(), 3); + assert!(removed_txs.iter().any(|tx| **tx == tx2)); + assert!(removed_txs.iter().any(|tx| **tx == tx3)); + assert!(removed_txs.iter().any(|tx| **tx == tx4)); + + assert_eq!(reorg_pool.len(), 2); + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig), + false + ); + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig), + false + ); + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig), + false + ); + assert_eq!( + reorg_pool.has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig), + false + ); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx5.body.kernels[0].excess_sig), true); + assert_eq!(reorg_pool.has_tx_with_excess_sig(&tx6.body.kernels[0].excess_sig), true); + } +} diff --git a/base_layer/core/src/mempool/unconfirmed_pool/error.rs b/base_layer/core/src/mempool/unconfirmed_pool/error.rs new file mode 100644 index 0000000000..3c62d15a26 --- /dev/null +++ b/base_layer/core/src/mempool/unconfirmed_pool/error.rs @@ -0,0 +1,33 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::mempool::priority::PriorityError; +use derive_error::Error; + +#[derive(Debug, Error)] +pub enum UnconfirmedPoolError { + /// The HashMap and BTreeMap are out of sync + StorageOutofSync, + /// The Thread Safety has been breached and the data access has become poisoned + PoisonedAccess, + PriorityError(PriorityError), +} diff --git a/base_layer/core/src/mempool/unconfirmed_pool/mod.rs b/base_layer/core/src/mempool/unconfirmed_pool/mod.rs new file mode 100644 index 0000000000..c5568aa70a --- /dev/null +++ b/base_layer/core/src/mempool/unconfirmed_pool/mod.rs @@ -0,0 +1,30 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod unconfirmed_pool; +mod unconfirmed_pool_storage; + +// Public re-exports +pub use error::UnconfirmedPoolError; +pub use unconfirmed_pool::{UnconfirmedPool, UnconfirmedPoolConfig}; +pub use unconfirmed_pool_storage::UnconfirmedPoolStorage; diff --git a/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs b/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs new file mode 100644 index 0000000000..8fb8afce7a --- /dev/null +++ b/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs @@ -0,0 +1,335 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + blocks::Block, + consts::{MEMPOOL_UNCONFIRMED_POOL_STORAGE_CAPACITY, MEMPOOL_UNCONFIRMED_POOL_WEIGHT_TRANSACTION_SKIP_COUNT}, + mempool::unconfirmed_pool::{UnconfirmedPoolError, UnconfirmedPoolStorage}, + transaction::Transaction, + types::Signature, +}; +use std::sync::{Arc, RwLock}; + +/// Configuration for the UnconfirmedPool +#[derive(Clone, Copy)] +pub struct UnconfirmedPoolConfig { + /// The maximum number of transactions that can be stored in the Unconfirmed Transaction pool + pub storage_capacity: usize, + /// The maximum number of transactions that can be skipped when compiling a set of highest priority transactions, + /// skipping over large transactions are performed in an attempt to fit more transactions into the remaining space. + pub weight_tx_skip_count: usize, +} + +impl Default for UnconfirmedPoolConfig { + fn default() -> Self { + Self { + storage_capacity: MEMPOOL_UNCONFIRMED_POOL_STORAGE_CAPACITY, + weight_tx_skip_count: MEMPOOL_UNCONFIRMED_POOL_WEIGHT_TRANSACTION_SKIP_COUNT, + } + } +} + +/// The Unconfirmed Transaction Pool consists of all unconfirmed transactions that are ready to be included in a block +/// and they are prioritised according to the priority metric. +pub struct UnconfirmedPool { + pool_storage: RwLock, +} + +impl UnconfirmedPool { + /// Create a new UnconfirmedPool with the specified configuration + pub fn new(config: UnconfirmedPoolConfig) -> Self { + Self { + pool_storage: RwLock::new(UnconfirmedPoolStorage::new(config)), + } + } + + /// Insert a new transaction into the UnconfirmedPool. Low priority transactions will be removed to make space for + /// higher priority transactions. The lowest priority transactions will be removed when the maximum capacity is + /// reached and the new transaction has a higher priority than the currently stored lowest priority transaction. + pub fn insert(&mut self, transaction: Transaction) -> Result<(), UnconfirmedPoolError> { + self.pool_storage + .write() + .map_err(|_| UnconfirmedPoolError::PoisonedAccess)? + .insert(transaction) + } + + /// Insert a set of new transactions into the UnconfirmedPool + pub fn insert_txs(&mut self, transactions: Vec) -> Result<(), UnconfirmedPoolError> { + self.pool_storage + .write() + .map_err(|_| UnconfirmedPoolError::PoisonedAccess)? + .insert_txs(transactions) + } + + /// Check if a transaction is available in the UnconfirmedPool + pub fn has_tx_with_excess_sig(&self, excess_sig: &Signature) -> Result { + Ok(self + .pool_storage + .read() + .map_err(|_| UnconfirmedPoolError::PoisonedAccess)? + .has_tx_with_excess_sig(excess_sig)) + } + + /// Returns a set of the highest priority unconfirmed transactions, that can be included in a block + pub fn highest_priority_txs(&self, total_weight: u64) -> Result>, UnconfirmedPoolError> { + self.pool_storage + .read() + .map_err(|_| UnconfirmedPoolError::PoisonedAccess)? + .highest_priority_txs(total_weight) + } + + /// Remove all published transactions from the UnconfirmedPool and discard all double spend transactions + pub fn remove_published_and_discard_double_spends( + &mut self, + published_block: &Block, + ) -> Result>, UnconfirmedPoolError> + { + Ok(self + .pool_storage + .write() + .map_err(|_| UnconfirmedPoolError::PoisonedAccess)? + .remove_published_and_discard_double_spends(published_block)) + } + + /// Returns the total number of unconfirmed transactions stored in the UnconfirmedPool + pub fn len(&self) -> Result { + Ok(self + .pool_storage + .read() + .map_err(|_| UnconfirmedPoolError::PoisonedAccess)? + .len()) + } + + #[cfg(test)] + /// Checks the consistency status of the Hashmap and BtreeMap + pub fn check_status(&self) -> Result { + Ok(self + .pool_storage + .read() + .map_err(|_| UnconfirmedPoolError::PoisonedAccess)? + .check_status()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + tari_amount::MicroTari, + test_utils::builders::{create_test_block, create_test_tx}, + }; + + #[test] + fn test_insert_and_retrieve_highest_priority_txs() { + let tx1 = create_test_tx(MicroTari(5_000), MicroTari(500), 0, 2, 1); + let tx2 = create_test_tx(MicroTari(5_000), MicroTari(100), 0, 4, 1); + let tx3 = create_test_tx(MicroTari(5_000), MicroTari(1000), 0, 5, 1); + let tx4 = create_test_tx(MicroTari(5_000), MicroTari(200), 0, 3, 1); + let tx5 = create_test_tx(MicroTari(5_000), MicroTari(500), 0, 5, 1); + + let mut unconfirmed_pool = UnconfirmedPool::new(UnconfirmedPoolConfig { + storage_capacity: 4, + weight_tx_skip_count: 3, + }); + unconfirmed_pool + .insert_txs(vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone(), tx5.clone()]) + .unwrap(); + // Check that lowest priority tx was removed to make room for new incoming transactions + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig) + .unwrap(), + true + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig) + .unwrap(), + true + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig) + .unwrap(), + true + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx5.body.kernels[0].excess_sig) + .unwrap(), + true + ); + // Retrieve the set of highest priority unspent transactions + let desired_weight = tx1.calculate_weight() + tx3.calculate_weight() + tx4.calculate_weight(); + let selected_txs = unconfirmed_pool.highest_priority_txs(desired_weight).unwrap(); + assert_eq!(selected_txs.len(), 3); + assert_eq!(selected_txs[0].body.kernels[0].fee, MicroTari(1000)); + assert_eq!(selected_txs[1].body.kernels[0].fee, MicroTari(500)); + assert_eq!(selected_txs[2].body.kernels[0].fee, MicroTari(200)); + // Note that transaction tx5 could not be included as its weight was to big to fit into the remaining allocated + // space, the second best transaction was then included + + assert!(unconfirmed_pool.check_status().unwrap()); + } + + #[test] + fn test_remove_published_txs() { + let tx1 = create_test_tx(MicroTari(10_000), MicroTari(500), 0, 2, 1); + let tx2 = create_test_tx(MicroTari(10_000), MicroTari(100), 0, 3, 1); + let tx3 = create_test_tx(MicroTari(10_000), MicroTari(1000), 0, 2, 1); + let tx4 = create_test_tx(MicroTari(10_000), MicroTari(200), 0, 4, 1); + let tx5 = create_test_tx(MicroTari(10_000), MicroTari(500), 0, 3, 1); + let tx6 = create_test_tx(MicroTari(10_000), MicroTari(750), 0, 2, 1); + + let mut unconfirmed_pool = UnconfirmedPool::new(UnconfirmedPoolConfig { + storage_capacity: 10, + weight_tx_skip_count: 3, + }); + unconfirmed_pool + .insert_txs(vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone(), tx5.clone()]) + .unwrap(); + // utx6 should not be added to unconfirmed_pool as it is an unknown transactions that was included in the block + // by another node + + let published_block = create_test_block(0, vec![tx1.clone(), tx3.clone(), tx5.clone()]); + let _ = unconfirmed_pool.remove_published_and_discard_double_spends(&published_block); + + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig) + .unwrap(), + true + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig) + .unwrap(), + true + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx5.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx6.body.kernels[0].excess_sig) + .unwrap(), + false + ); + + assert!(unconfirmed_pool.check_status().unwrap()); + } + + #[test] + fn test_discard_double_spend_txs() { + let tx1 = create_test_tx(MicroTari(5_000), MicroTari(500), 0, 2, 1); + let tx2 = create_test_tx(MicroTari(5_000), MicroTari(100), 0, 3, 1); + let tx3 = create_test_tx(MicroTari(5_000), MicroTari(1000), 0, 2, 1); + let tx4 = create_test_tx(MicroTari(5_000), MicroTari(200), 0, 2, 1); + let mut tx5 = create_test_tx(MicroTari(5_000), MicroTari(500), 0, 3, 1); + let mut tx6 = create_test_tx(MicroTari(5_000), MicroTari(750), 0, 2, 1); + // tx1 and tx5 have a shared input. Also, tx3 and tx6 have a shared input + tx5.body.inputs[0] = tx1.body.inputs[0].clone(); + tx6.body.inputs[1] = tx3.body.inputs[1].clone(); + + let mut unconfirmed_pool = UnconfirmedPool::new(UnconfirmedPoolConfig { + storage_capacity: 10, + weight_tx_skip_count: 3, + }); + unconfirmed_pool + .insert_txs(vec![ + tx1.clone(), + tx2.clone(), + tx3.clone(), + tx4.clone(), + tx5.clone(), + tx6.clone(), + ]) + .unwrap(); + + // The publishing of tx1 and tx3 will be double-spends and orphan tx5 and tx6 + let published_block = create_test_block(0, vec![tx1.clone(), tx2.clone(), tx3.clone()]); + + let _ = unconfirmed_pool + .remove_published_and_discard_double_spends(&published_block) + .unwrap(); // Double spends are discarded + + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx1.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx2.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx3.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx4.body.kernels[0].excess_sig) + .unwrap(), + true + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx5.body.kernels[0].excess_sig) + .unwrap(), + false + ); + assert_eq!( + unconfirmed_pool + .has_tx_with_excess_sig(&tx6.body.kernels[0].excess_sig) + .unwrap(), + false + ); + + assert!(unconfirmed_pool.check_status().unwrap()); + } +} diff --git a/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool_storage.rs b/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool_storage.rs new file mode 100644 index 0000000000..27485f5f9e --- /dev/null +++ b/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool_storage.rs @@ -0,0 +1,180 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + blocks::Block, + mempool::{ + priority::{FeePriority, PrioritizedTransaction}, + unconfirmed_pool::{UnconfirmedPoolConfig, UnconfirmedPoolError}, + }, + transaction::Transaction, + types::Signature, +}; +use std::{ + collections::{BTreeMap, HashMap}, + convert::TryFrom, + sync::Arc, +}; + +/// UnconfirmedPool makes use of UnconfirmedPoolStorage to provide thread save access to its Hashmap and BTreeMap. +/// The txs_by_signature HashMap is used to find a transaction using its excess_sig, this functionality is used to match +/// transactions included in blocks with transactions stored in the pool. The txs_by_priority BTreeMap prioritise the +/// transactions in the pool according to TXPriority, it allows transactions to be inserted in sorted order by their +/// priority. The txs_by_priority BTreeMap makes it easier to select the set of highest priority transactions that can +/// be included in a block. The excess_sig of a transaction is used a key to uniquely identify a specific transaction in +/// these containers. +pub struct UnconfirmedPoolStorage { + config: UnconfirmedPoolConfig, + txs_by_signature: HashMap, + txs_by_priority: BTreeMap, +} + +impl UnconfirmedPoolStorage { + /// Create a new UnconfirmedPoolStorage with the specified configuration + pub fn new(config: UnconfirmedPoolConfig) -> Self { + Self { + config, + txs_by_signature: HashMap::new(), + txs_by_priority: BTreeMap::new(), + } + } + + fn lowest_priority(&self) -> &FeePriority { + self.txs_by_priority.iter().next().unwrap().0 + } + + fn remove_lowest_priority_tx(&mut self) { + if let Some((priority, sig)) = self.txs_by_priority.iter().next().map(|(p, s)| (p.clone(), s.clone())) { + self.txs_by_signature.remove(&sig); + self.txs_by_priority.remove(&priority); + } + } + + /// Insert a new transaction into the UnconfirmedPoolStorage. Low priority transactions will be removed to make + /// space for higher priority transactions. The lowest priority transactions will be removed when the maximum + /// capacity is reached and the new transaction has a higher priority than the currently stored lowest priority + /// transaction. + pub fn insert(&mut self, tx: Transaction) -> Result<(), UnconfirmedPoolError> { + let tx_key = tx.body.kernels[0].excess_sig.clone(); + if !self.txs_by_signature.contains_key(&tx_key) { + let prioritized_tx = PrioritizedTransaction::try_from(tx)?; + if self.txs_by_signature.len() >= self.config.storage_capacity { + if prioritized_tx.priority < *self.lowest_priority() { + return Ok(()); + } + self.remove_lowest_priority_tx(); + } + + self.txs_by_priority + .insert(prioritized_tx.priority.clone(), tx_key.clone()); + self.txs_by_signature.insert(tx_key, prioritized_tx); + } + Ok(()) + } + + /// Insert a set of new transactions into the UnconfirmedPoolStorage + pub fn insert_txs(&mut self, txs: Vec) -> Result<(), UnconfirmedPoolError> { + for tx in txs.into_iter() { + self.insert(tx)?; + } + Ok(()) + } + + /// Check if a transaction is stored in the UnconfirmedPoolStorage + pub fn has_tx_with_excess_sig(&self, excess_sig: &Signature) -> bool { + self.txs_by_signature.contains_key(excess_sig) + } + + /// Returns a set of the highest priority unconfirmed transactions, that can be included in a block. + pub fn highest_priority_txs(&self, total_weight: u64) -> Result>, UnconfirmedPoolError> { + let mut selected_txs: Vec> = Vec::new(); + let mut curr_weight: u64 = 0; + let mut curr_skip_count: usize = 0; + for (_, tx_key) in self.txs_by_priority.iter().rev() { + let ptx = self + .txs_by_signature + .get(tx_key) + .ok_or(UnconfirmedPoolError::StorageOutofSync)?; + + if curr_weight + ptx.weight <= total_weight { + curr_weight += ptx.weight; + selected_txs.push(ptx.transaction.clone()); + } else { + // Check if some the next few txs with slightly lower priority wont fit in the remaining space. + curr_skip_count += 1; + if curr_skip_count >= self.config.weight_tx_skip_count { + break; + } + } + } + Ok(selected_txs) + } + + // Remove double-spends from the UnconfirmedPoolStorage. These transactions were orphaned by the provided published + // block. Check if any of the unspent transactions in the UnconfirmedPool has inputs that was spent by the provided + // published block. + fn discard_double_spends(&mut self, published_block: &Block) { + let mut removed_tx_keys: Vec = Vec::new(); + for (tx_key, ptx) in self.txs_by_signature.iter() { + for input in &ptx.transaction.body.inputs { + if published_block.body.inputs.contains(input) { + self.txs_by_priority.remove(&ptx.priority); + removed_tx_keys.push(tx_key.clone()); + } + } + } + + for tx_key in &removed_tx_keys { + self.txs_by_signature.remove(&tx_key); + } + } + + /// Remove all published transactions from the UnconfirmedPoolStorage and discard double spends + pub fn remove_published_and_discard_double_spends(&mut self, published_block: &Block) -> Vec> { + self.discard_double_spends(published_block); + + let mut removed_txs: Vec> = Vec::new(); + published_block.body.kernels.iter().for_each(|kernel| { + if let Some(ptx) = self.txs_by_signature.get(&kernel.excess_sig) { + self.txs_by_priority.remove(&ptx.priority); + removed_txs.push(self.txs_by_signature.remove(&kernel.excess_sig).unwrap().transaction); + } + }); + removed_txs + } + + /// Returns the total number of unconfirmed transactions stored in the UnconfirmedPoolStorage + pub fn len(&self) -> usize { + self.txs_by_signature.len() + } + + #[cfg(test)] + /// Checks the consistency status of the Hashmap and BtreeMap + pub fn check_status(&self) -> bool { + if self.txs_by_priority.len() != self.txs_by_signature.len() { + return false; + } + self.txs_by_priority + .iter() + .all(|(_, tx_key)| self.txs_by_signature.contains_key(tx_key)) + } +} diff --git a/base_layer/core/src/message.rs b/base_layer/core/src/message.rs deleted file mode 100644 index ca9f58b8a9..0000000000 --- a/base_layer/core/src/message.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2018 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -use base64; -use derive_error::Error; -use rmp_serde; -use serde::{Deserialize, Serialize}; -use serde_json; - -#[derive(Debug, Error)] -pub enum MessageError { - // An error occurred serialising an object into binary - BinarySerializeError(rmp_serde::encode::Error), - // An error occurred deserialising binary data into an object - BinaryDeserializeError(rmp_serde::decode::Error), - // An error occurred de-/serialising an object from/into JSON - JSONError(serde_json::error::Error), - // An error occurred deserialising an object from Base64 - Base64DeserializeError(base64::DecodeError), -} - -pub trait MessageFormat: Sized { - fn to_binary(&self) -> Result, MessageError>; - fn to_json(&self) -> Result; - fn to_base64(&self) -> Result; - - fn from_binary(msg: &[u8]) -> Result; - fn from_json(msg: &str) -> Result; - fn from_base64(msg: &str) -> Result; -} - -impl<'a, T> MessageFormat for T -where T: Deserialize<'a> + Serialize -{ - fn to_binary(&self) -> Result, MessageError> { - let mut buf = Vec::new(); - self.serialize(&mut rmp_serde::Serializer::new(&mut buf)) - .map_err(|e| MessageError::BinarySerializeError(e))?; - Ok(buf.to_vec()) - } - - fn to_json(&self) -> Result { - serde_json::to_string(self).map_err(|e| MessageError::JSONError(e)) - } - - fn to_base64(&self) -> Result { - let val = self.to_binary()?; - Ok(base64::encode(&val)) - } - - fn from_binary(msg: &[u8]) -> Result { - let mut de = rmp_serde::Deserializer::new(msg); - Deserialize::deserialize(&mut de).map_err(|e| MessageError::BinaryDeserializeError(e)) - } - - fn from_json(msg: &str) -> Result { - let mut de = serde_json::Deserializer::from_reader(msg.as_bytes()); - Deserialize::deserialize(&mut de).map_err(|e| MessageError::JSONError(e)) - } - - fn from_base64(msg: &str) -> Result { - let buf = base64::decode(msg)?; - Self::from_binary(&buf) - } -} - -#[cfg(test)] -mod test { - use super::*; - use base64::DecodeError as Base64Error; - use rmp_serde::decode::Error as RMPError; - use serde_derive::{Deserialize, Serialize}; - use std::io::ErrorKind; - - #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] - struct TestMessage { - key: String, - value: u64, - sub_message: Option>, - } - - impl TestMessage { - pub fn new(key: &str, value: u64) -> TestMessage { - TestMessage { - key: key.to_string(), - value, - sub_message: None, - } - } - - pub fn set_sub_message(&mut self, msg: TestMessage) { - self.sub_message = Some(Box::new(msg)); - } - } - - #[test] - fn binary_simple() { - let val = TestMessage::new("twenty", 20); - let msg = val.to_binary().unwrap(); - assert_eq!(msg, b"\x93\xA6\x74\x77\x65\x6E\x74\x79\x14\xC0"); - let val2 = TestMessage::from_binary(&msg).unwrap(); - assert_eq!(val, val2); - } - - #[test] - fn base64_simple() { - let val = TestMessage::new("twenty", 20); - let msg = val.to_base64().unwrap(); - assert_eq!(msg, "k6Z0d2VudHkUwA=="); - let val2 = TestMessage::from_base64(&msg).unwrap(); - assert_eq!(val, val2); - } - - #[test] - fn json_simple() { - let val = TestMessage::new("twenty", 20); - let msg = val.to_json().unwrap(); - assert_eq!(msg, "{\"key\":\"twenty\",\"value\":20,\"sub_message\":null}"); - let val2 = TestMessage::from_json(&msg).unwrap(); - assert_eq!(val, val2); - } - - #[test] - fn nested_message() { - let inner = TestMessage::new("today", 100); - let mut val = TestMessage::new("tomorrow", 50); - val.set_sub_message(inner); - - let msg_json = val.to_json().unwrap(); - assert_eq!( - msg_json, - "{\"key\":\"tomorrow\",\"value\":50,\"sub_message\":{\"key\":\"today\",\"value\":100,\"sub_message\":\ - null}}" - ); - - let msg_base64 = val.to_base64().unwrap(); - assert_eq!(msg_base64, "k6h0b21vcnJvdzKTpXRvZGF5ZMA="); - - let msg_bin = val.to_binary().unwrap(); - assert_eq!( - msg_bin, - b"\x93\xA8\x74\x6F\x6D\x6F\x72\x72\x6F\x77\x32\x93\xA5\x74\x6F\x64\x61\x79\x64\xC0" - ); - - let val2 = TestMessage::from_json(&msg_json).unwrap(); - assert_eq!(val, val2); - - let val2 = TestMessage::from_base64(&msg_base64).unwrap(); - assert_eq!(val, val2); - - let val2 = TestMessage::from_binary(&msg_bin).unwrap(); - assert_eq!(val, val2); - } - - #[test] - fn fail_json() { - let err = TestMessage::from_json("{\"key\":5}").err().unwrap(); - match err { - MessageError::JSONError(e) => { - assert_eq!(e.line(), 1); - assert_eq!(e.column(), 9); - assert!(e.is_data()); - }, - _ => panic!("JSON conversion should fail"), - }; - } - - #[test] - fn fail_base64() { - let err = TestMessage::from_base64("aaaaa$aaaaa").err().unwrap(); - match err { - MessageError::Base64DeserializeError(Base64Error::InvalidByte(offset, val)) => { - assert_eq!(offset, 5); - assert_eq!(val, '$' as u8); - }, - _ => panic!("Base64 conversion should fail"), - }; - - let err = TestMessage::from_base64("j6h0b21vcnJvdzKTpXRvZGF5ZMA=").err().unwrap(); - match err { - MessageError::BinaryDeserializeError(RMPError::Syntax(s)) => { - assert_eq!(s, "invalid type: sequence, expected field identifier"); - }, - _ => panic!("Base64 conversion should fail"), - }; - } - - #[test] - fn fail_binary() { - let err = TestMessage::from_binary(b"").err().unwrap(); - match err { - MessageError::BinaryDeserializeError(RMPError::InvalidMarkerRead(e)) => { - assert_eq!(e.kind(), ErrorKind::UnexpectedEof, "Unexpected error type: {:?}", e); - }, - _ => { - panic!("Base64 conversion should fail"); - }, - } - } -} diff --git a/base_layer/core/src/proof_of_work/blake_pow.rs b/base_layer/core/src/proof_of_work/blake_pow.rs new file mode 100644 index 0000000000..5d88f76e9d --- /dev/null +++ b/base_layer/core/src/proof_of_work/blake_pow.rs @@ -0,0 +1,118 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + blocks::BlockHeader, + proof_of_work::{Difficulty, PowError, ProofOfWork}, +}; +use bigint::uint::U256; +use blake2::Blake2b; +use digest::Digest; +use serde::{Deserialize, Serialize}; +use tari_crypto::common::Blake256; +use tari_utilities::{ByteArray, ByteArrayError, Hashable}; + +const MAX_TARGET: U256 = U256::MAX; + +/// A simple Blake2b-based proof of work. This is currently intended to be used for testing and perhaps Testnet until +/// Monero merge-mining is active. +/// +/// The proof of work difficulty is given by `H256(H512(header || nonce))` where Hnnn is the Blake2b digest of length +/// `nnn` bits. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct BlakePow; + +impl BlakePow { + /// A simple miner. It starts at nonce = 0 and iterates until it finds a header hash that meets the desired target + pub fn mine(target_difficulty: Difficulty, header: &BlockHeader) -> u64 { + let mut nonce = 0u64; + // We're mining over here! + while let Ok(d) = header.pow.calculate_difficulty(nonce, &header) { + if d >= target_difficulty { + break; + } + nonce += 1; + } + nonce + } +} + +impl ProofOfWork for BlakePow { + fn calculate_difficulty(&self, nonce: u64, header: &BlockHeader) -> Result { + let bytes = header.hash(); + let hash = Blake2b::new() + .chain(&bytes) + .chain(nonce.to_le_bytes()) + .result() + .to_vec(); + let hash = Blake256::digest(&hash).to_vec(); + let scalar = U256::from_little_endian(&hash); + let result = MAX_TARGET / scalar; + let difficulty = u64::from(result).into(); + Ok(difficulty) + } +} + +impl Default for BlakePow { + fn default() -> Self { + BlakePow + } +} + +impl ByteArray for BlakePow { + fn from_bytes(_bytes: &[u8]) -> Result { + Ok(BlakePow) + } + + fn as_bytes(&self) -> &[u8] { + &[] + } +} + +impl Hashable for BlakePow { + fn hash(&self) -> Vec { + vec![] + } +} + +#[cfg(test)] +mod test { + use crate::{blocks::BlockHeader, proof_of_work::ProofOfWork}; + + #[test] + fn validate_max_target() { + let header = BlockHeader::new(0); + assert_eq!(header.pow.calculate_difficulty(2, &header), Ok(1.into())); + } + + #[test] + fn difficulty_1000() { + let header = BlockHeader::new(0); + assert_eq!(header.pow.calculate_difficulty(108, &header), Ok(1273.into())); + } + + #[test] + fn difficulty_1mil() { + let header = BlockHeader::new(0); + assert_eq!(header.pow.calculate_difficulty(134_390, &header), Ok(3_250_351.into())); + } +} diff --git a/base_layer/core/src/proof_of_work/difficulty.rs b/base_layer/core/src/proof_of_work/difficulty.rs new file mode 100644 index 0000000000..b09e7e8105 --- /dev/null +++ b/base_layer/core/src/proof_of_work/difficulty.rs @@ -0,0 +1,91 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bitflags::_core::ops::Div; +use newtype_ops::newtype_ops; +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// Minimum difficulty, enforced in diff retargetting +/// avoids getting stuck when trying to increase difficulty subject to dampening +pub const MIN_DIFFICULTY: u64 = 1; + +/// The difficulty is defined as the maximum target divided by the block hash. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Deserialize, Serialize)] +pub struct Difficulty(u64); + +impl Difficulty { + /// Difficulty of MIN_DIFFICULTY + pub fn min() -> Difficulty { + Difficulty(MIN_DIFFICULTY) + } +} + +impl Default for Difficulty { + fn default() -> Self { + Difficulty(0) + } +} + +// You can only add or subtract Difficulty from Difficulty +newtype_ops! { [Difficulty] {add sub} {:=} Self Self } +newtype_ops! { [Difficulty] {add sub} {:=} &Self &Self } +newtype_ops! { [Difficulty] {add sub} {:=} Self &Self } + +// Multiplication and division of difficulty by scalar is Difficulty +newtype_ops! { [Difficulty] {mul div rem} {:=} Self u64 } + +// Division of difficulty by difficulty is a difficulty ratio (scalar) (newtype_ops doesn't handle this case) +impl Div for Difficulty { + type Output = u64; + + fn div(self, rhs: Self) -> Self::Output { + self.0 / rhs.0 + } +} + +impl fmt::Display for Difficulty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Difficulty { + fn from(value: u64) -> Self { + Difficulty(value) + } +} + +#[cfg(test)] +mod test { + use crate::proof_of_work::difficulty::Difficulty; + + #[test] + fn add_difficulty() { + assert_eq!( + Difficulty::from(1_000) + Difficulty::from(8_000), + Difficulty::from(9_000) + ); + assert_eq!(Difficulty::default() + Difficulty::from(42), Difficulty::from(42)); + assert_eq!(&Difficulty::from(15) + &Difficulty::from(5), Difficulty::from(20)); + } +} diff --git a/infrastructure/merklemountainrange/src/error.rs b/base_layer/core/src/proof_of_work/error.rs similarity index 91% rename from infrastructure/merklemountainrange/src/error.rs rename to base_layer/core/src/proof_of_work/error.rs index 923489beb8..22a7e84cb3 100644 --- a/infrastructure/merklemountainrange/src/error.rs +++ b/base_layer/core/src/proof_of_work/error.rs @@ -1,4 +1,4 @@ -// Copyright 2019 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -22,7 +22,8 @@ use derive_error::Error; -#[derive(Debug, Error)] -pub enum MerkleMountainRangeError { - Error, // place holder for real error +#[derive(Clone, Debug, PartialEq, Error)] +pub enum PowError { + // ProofOfWorkFailed + InvalidProofOfWork, } diff --git a/base_layer/core/src/proof_of_work/mod.rs b/base_layer/core/src/proof_of_work/mod.rs new file mode 100644 index 0000000000..32bf162ff8 --- /dev/null +++ b/base_layer/core/src/proof_of_work/mod.rs @@ -0,0 +1,31 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod blake_pow; +mod difficulty; +mod error; +mod pow; + +pub use blake_pow::BlakePow; +pub use difficulty::Difficulty; +pub use error::PowError; +pub use pow::ProofOfWork; diff --git a/base_layer/core/src/proof_of_work/pow.rs b/base_layer/core/src/proof_of_work/pow.rs new file mode 100644 index 0000000000..7a329525fa --- /dev/null +++ b/base_layer/core/src/proof_of_work/pow.rs @@ -0,0 +1,41 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +use crate::{ + blocks::BlockHeader, + proof_of_work::{difficulty::Difficulty, error::PowError}, +}; +use tari_utilities::{ByteArray, Hashable}; + +/// `WorkProof` is a trait that captures common functionality for different proof of work algorithms. +pub trait ProofOfWork: ByteArray + Hashable { + /// This function will calculate the difficulty for the proof of work given the nonce and block header. This + /// function is used to validate proofs of work generated by miners. + /// + /// Generally speaking, the difficulty is roughly how many mining attempts a miner will make, _on average_ before + /// finding a nonce that meets the difficulty target. + /// + /// In actuality, the difficulty is _defined_ as the maximum target value (u265) divided by the block header hash + /// (as a u256) + fn calculate_difficulty(&self, nonce: u64, header: &BlockHeader) -> Result; +} diff --git a/base_layer/core/src/tari_amount.rs b/base_layer/core/src/tari_amount.rs new file mode 100644 index 0000000000..94359c9036 --- /dev/null +++ b/base_layer/core/src/tari_amount.rs @@ -0,0 +1,186 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use newtype_ops::newtype_ops; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Error, Formatter}; + +use std::{iter::Sum, ops::Add}; +use tari_crypto::ristretto::RistrettoSecretKey; + +/// All calculations using Tari amounts should use these newtypes to prevent bugs related to rounding errors, unit +/// conversion errors etc. +/// +/// ```edition2018 +/// use tari_core::tari_amount::MicroTari; +/// +/// let a = MicroTari::from(500); +/// let b = MicroTari::from(50); +/// assert_eq!(a + b, MicroTari::from(550)); +/// ``` +#[derive(Copy, Default, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct MicroTari(pub u64); + +// You can only add or subtract µT from µT +newtype_ops! { [MicroTari] {add sub} {:=} Self Self } +newtype_ops! { [MicroTari] {add sub} {:=} &Self &Self } +newtype_ops! { [MicroTari] {add sub} {:=} Self &Self } + +// Multiplication and division only makes sense when µT is multiplied/divided by a scalar +newtype_ops! { [MicroTari] {mul div rem} {:=} Self u64 } + +impl MicroTari { + pub fn checked_sub(self, v: MicroTari) -> Option { + if self.0 >= v.0 { + return Some(self - v); + } + None + } +} + +impl Display for MicroTari { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + f.write_fmt(format_args!("{} µT", self.0)) + } +} + +impl From for u64 { + fn from(v: MicroTari) -> Self { + v.0 + } +} + +impl From for MicroTari { + fn from(v: u64) -> Self { + MicroTari(v) + } +} + +impl From for f64 { + fn from(v: MicroTari) -> Self { + v.0 as f64 + } +} + +impl From for RistrettoSecretKey { + fn from(v: MicroTari) -> Self { + v.0.into() + } +} + +impl<'a> Sum<&'a MicroTari> for MicroTari { + fn sum>(iter: I) -> MicroTari { + iter.fold(MicroTari::from(0), Add::add) + } +} + +impl Sum for MicroTari { + fn sum>(iter: I) -> MicroTari { + iter.fold(MicroTari::from(0), Add::add) + } +} + +/// A convenience struct for representing full Tari. You should **never** use Tari in consensus calculations, because +/// Tari wraps a floating point value. Use MicroTari for that instead. +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] +pub struct Tari(f64); + +newtype_ops! { [Tari] {add sub} {:=} Self Self } +newtype_ops! { [Tari] {mul div rem} {:=} Self f64 } + +impl Display for Tari { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + f.write_fmt(format_args!("{:0.6} T", self.0)) + } +} + +impl From for f64 { + fn from(v: Tari) -> Self { + v.0 + } +} + +impl From for Tari { + fn from(v: f64) -> Self { + Tari(v) + } +} + +impl From for Tari { + fn from(v: MicroTari) -> Self { + Tari(v.0 as f64 * 1e-6) + } +} + +#[cfg(test)] +mod test { + use crate::tari_amount::{MicroTari, Tari}; + + #[test] + fn micro_tari_arithmetic() { + let mut a = MicroTari::from(500); + let b = MicroTari::from(50); + assert_eq!(a + b, MicroTari::from(550)); + assert_eq!(a - b, MicroTari::from(450)); + assert_eq!(a * 5, MicroTari::from(2_500)); + assert_eq!(a / 10, MicroTari::from(50)); + a += b; + assert_eq!(a, MicroTari::from(550)); + a -= MicroTari::from(45); + assert_eq!(a, MicroTari::from(505)); + assert_eq!(a % 50, MicroTari::from(5)); + } + + #[test] + fn micro_tari_display() { + let s = format!("{}", MicroTari::from(1234)); + assert_eq!(s, "1234 µT"); + } + + #[test] + fn add_tari_and_microtari() { + let a = MicroTari::from(100_000); + let b = Tari::from(0.23); + let sum: Tari = b + a.into(); + assert_eq!(sum, Tari::from(0.33)); + } + + #[test] + fn tari_arithmetic() { + let mut a = Tari::from(1.5); + let b = Tari::from(2.25); + assert_eq!(a + b, Tari::from(3.75)); + assert_eq!(a - b, Tari::from(-0.75)); + assert_eq!(a * 10.0, Tari::from(15.0)); + assert_eq!(b / 2.0, Tari::from(1.125)); + a += b; + assert_eq!(a, Tari::from(3.75)); + a -= Tari::from(0.75); + assert_eq!(a, Tari::from(3.0)); + } + + #[test] + fn tari_display() { + let s = format!("{}", Tari::from(1.234)); + assert_eq!(s, "1.234000 T"); + } +} diff --git a/base_layer/core/src/test_utils/builders.rs b/base_layer/core/src/test_utils/builders.rs new file mode 100644 index 0000000000..e799bcc2c1 --- /dev/null +++ b/base_layer/core/src/test_utils/builders.rs @@ -0,0 +1,143 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +use crate::{ + blocks::{aggregated_body::AggregateBody, Block, BlockHeader}, + tari_amount::MicroTari, + transaction::{KernelBuilder, OutputFeatures, Transaction, TransactionInput, TransactionKernel, TransactionOutput}, + transaction_protocol::{build_challenge, TransactionMetadata}, + types::{Commitment, PrivateKey, PublicKey, RangeProof, Signature, COMMITMENT_FACTORY, PROVER}, +}; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::{PublicKey as PK, SecretKey}, + range_proof::RangeProofService, +}; + +/// Create an unconfirmed transaction for testing with a valid fee, unique access_sig, random inputs and outputs, the +/// transaction is only partially constructed +pub fn create_test_tx( + amount: MicroTari, + fee: MicroTari, + lock_height: u64, + input_count: usize, + output_count: usize, +) -> Transaction +{ + let mut rng = rand::OsRng::new().unwrap(); + let kernel = create_test_kernel(fee, lock_height); + let mut body = AggregateBody::empty(); + body.kernels.push(kernel); + + for _ in 0..input_count { + let input = TransactionInput::new( + OutputFeatures::default(), + COMMITMENT_FACTORY.commit(&PrivateKey::random(&mut rng), &amount.into()), + ); + body.inputs.push(input); + } + + for _ in 0..output_count { + let output = TransactionOutput::new( + OutputFeatures::default(), + COMMITMENT_FACTORY.commit(&PrivateKey::random(&mut rng), &MicroTari(10).into()), + RangeProof::default(), + ); + body.outputs.push(output); + } + + Transaction { + offset: PrivateKey::random(&mut rng), + body, + } +} + +/// Create a transaction kernel with the given fee, using random keys to generate the signature +pub fn create_test_kernel(fee: MicroTari, lock_height: u64) -> TransactionKernel { + let (excess, s) = create_random_signature(fee); + KernelBuilder::new() + .with_fee(fee) + .with_lock_height(lock_height) + .with_excess(&Commitment::from_public_key(&excess)) + .with_signature(&s) + .build() + .unwrap() +} + +/// Create a partially constructed block using the provided set of transactions +pub fn create_test_block(block_height: u64, transactions: Vec) -> Block { + let mut header = BlockHeader::new(0); + header.height = block_height; + let mut body = AggregateBody::empty(); + transactions.iter().for_each(|tx| { + body.kernels.push(tx.body.kernels[0].clone()); + body.inputs.append(&mut tx.body.inputs.clone()); + body.outputs.append(&mut tx.body.outputs.clone()); + }); + + Block { header, body } +} + +/// Create a partially constructed utxo set using the outputs of a test block +pub fn extract_outputs_as_inputs(utxos: &mut Vec, published_block: &Block) { + for output in &published_block.body.outputs { + let input = TransactionInput::from(output.clone()); + if !utxos.contains(&input) { + utxos.push(input); + } + } +} + +/// Generate a random signature, returning the public key (excess) and the signature. +pub fn create_random_signature(fee: MicroTari) -> (PublicKey, Signature) { + let mut rng = rand::OsRng::new().unwrap(); + let r = SecretKey::random(&mut rng); + let (k, p) = PublicKey::random_keypair(&mut rng); + let tx_meta = TransactionMetadata { fee, lock_height: 0 }; + let e = build_challenge(&PublicKey::from_secret_key(&r), &tx_meta); + (p, Signature::sign(k, r, &e).unwrap()) +} + +/// A convenience struct for a set of public-private keys and a public-private nonce +pub struct TestKeySet { + k: PrivateKey, + pk: PublicKey, + r: PrivateKey, + pr: PublicKey, +} + +pub fn generate_keys() -> TestKeySet { + let mut rng = rand::OsRng::new().unwrap(); + let (k, pk) = PublicKey::random_keypair(&mut rng); + let (r, pr) = PublicKey::random_keypair(&mut rng); + TestKeySet { k, pk, r, pr } +} + +/// Create a new UTXO for the specified value and return the output and spending key +pub fn create_utxo(value: MicroTari) -> (TransactionOutput, PrivateKey) { + let keys = generate_keys(); + let commitment = COMMITMENT_FACTORY.commit_value(&keys.k, value.into()); + let proof = PROVER.construct_proof(&keys.k, value.into()).unwrap(); + let utxo = TransactionOutput::new(OutputFeatures::default(), commitment, proof.into()); + (utxo, keys.k) +} diff --git a/base_layer/core/src/test_utils/mod.rs b/base_layer/core/src/test_utils/mod.rs new file mode 100644 index 0000000000..85a7c558f9 --- /dev/null +++ b/base_layer/core/src/test_utils/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// Helper functions to simplify generated test blockchain data +pub mod builders; diff --git a/base_layer/core/src/transaction.rs b/base_layer/core/src/transaction.rs index 12971712fb..0ebe20c985 100644 --- a/base_layer/core/src/transaction.rs +++ b/base_layer/core/src/transaction.rs @@ -24,58 +24,194 @@ // Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. use crate::{ - block::AggregateBody, - range_proof::RangeProof, + blocks::aggregated_body::AggregateBody, + tari_amount::MicroTari, types::{BlindingFactor, Commitment, CommitmentFactory, Signature}, }; -use crate::types::SignatureHash; +use crate::{ + consensus::ConsensusRules, + fee::Fee, + transaction_protocol::{build_challenge, TransactionMetadata}, + types::{HashDigest, RangeProof, RangeProofService}, +}; use derive_error::Error; -use digest::Digest; +use digest::Input; +use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use tari_crypto::{ - challenge::Challenge, - commitment::{HomomorphicCommitment, HomomorphicCommitmentFactory}, - common::Blake256, - ristretto::RistrettoSecretKey, + commitment::HomomorphicCommitmentFactory, + range_proof::{RangeProofError, RangeProofService as RangeProofServiceTrait}, }; -use tari_infra_derive::HashableOrdering; use tari_utilities::{ByteArray, Hashable}; +// These are set fairly arbitrarily at the moment. We'll need to do some modelling / testing to tune these values. +pub const MAX_TRANSACTION_INPUTS: usize = 500; +pub const MAX_TRANSACTION_OUTPUTS: usize = 100; +pub const MAX_TRANSACTION_RECIPIENTS: usize = 15; +pub const MINIMUM_TRANSACTION_FEE: MicroTari = MicroTari(100); + +//-------------------------------------- Output features --------------------------------------------------// + bitflags! { /// Options for a kernel's structure or use. /// TODO: expand to accommodate Tari DAN transaction types, such as namespace and validator node registrations + #[derive(Deserialize, Serialize)] pub struct KernelFeatures: u8 { /// Coinbase transaction const COINBASE_KERNEL = 1u8; } } +/// Options for UTXO's +#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] +pub struct OutputFeatures { + /// Flags are the feature flags that differentiate between outputs, eg Coinbase all of which has different rules + pub flags: OutputFlags, + /// the maturity of the specific UTXO. This is the min lock height at which an UTXO can be spend. Coinbase UTXO + /// require a min maturity of the Coinbase_lock_height, this should be checked on receiving new blocks. + pub maturity: u64, +} + +impl OutputFeatures { + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::new(); + bincode::serialize_into(&mut buf, self).unwrap(); // this should not fail + buf + } + + pub fn create_coinbase(current_block_height: u64, consensus_rules: &ConsensusRules) -> OutputFeatures { + OutputFeatures { + flags: OutputFlags::COINBASE_OUTPUT, + maturity: consensus_rules.coinbase_lock_height() + current_block_height, + } + } +} + +impl Default for OutputFeatures { + fn default() -> Self { + OutputFeatures { + flags: OutputFlags::empty(), + maturity: 0, + } + } +} + +impl PartialOrd for OutputFeatures { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for OutputFeatures { + fn cmp(&self, other: &Self) -> Ordering { + self.maturity.cmp(&other.maturity) + } +} + bitflags! { - pub struct OutputFeatures: u8 { + #[derive(Deserialize, Serialize)] + pub struct OutputFlags: u8 { /// Output is a coinbase output, must not be spent until maturity - const COINBASE_OUTPUT = 0b00000001; + const COINBASE_OUTPUT = 0b0000_0001; } } -type Hasher = Blake256; +//---------------------------------------- TransactionError ----------------------------------------------------// -#[derive(Debug, PartialEq, Error)] +#[derive(Clone, Debug, PartialEq, Error)] pub enum TransactionError { // Error validating the transaction - ValidationError, + #[error(msg_embedded, no_from, non_std)] + ValidationError(String), // Signature could not be verified InvalidSignatureError, // Transaction kernel does not contain a signature NoSignatureError, + // A range proof construction or verification has produced an error + RangeProofError(RangeProofError), +} + +//----------------------------------------- UnblindedOutput ----------------------------------------------------// + +/// An unblinded output is one where the value and spending key (blinding factor) are known. This can be used to +/// build both inputs and outputs (every input comes from an output) +#[derive(Debug, Clone, Hash)] +pub struct UnblindedOutput { + pub value: MicroTari, + pub spending_key: BlindingFactor, + pub features: OutputFeatures, +} + +impl UnblindedOutput { + /// Creates a new un-blinded output + pub fn new(value: MicroTari, spending_key: BlindingFactor, features: Option) -> UnblindedOutput { + UnblindedOutput { + value, + spending_key, + features: features.unwrap_or_else(|| OutputFeatures::default()), + } + } + + /// Commits an UnblindedOutput into a Transaction input + pub fn as_transaction_input(&self, factory: &CommitmentFactory, features: OutputFeatures) -> TransactionInput { + let commitment = factory.commit(&self.spending_key, &self.value.into()); + TransactionInput { commitment, features } + } + + pub fn as_transaction_output( + &self, + prover: &RangeProofService, + factory: &CommitmentFactory, + features: OutputFeatures, + ) -> Result + { + let commitment = factory.commit(&self.spending_key, &self.value.into()); + let output = TransactionOutput { + features, + commitment, + proof: RangeProof::from_bytes(&prover.construct_proof(&self.spending_key, self.value.into())?) + .map_err(|_| TransactionError::RangeProofError(RangeProofError::ProofConstructionError))?, + }; + // A range proof can be constructed for an invalid value so we should confirm that the proof can be verified. + if !output.verify_range_proof(&prover)? { + return Err(TransactionError::ValidationError( + "Range proof could not be verified".into(), + )); + } + Ok(output) + } } +// These implementations are used for order these outputs for UTXO selection which will be done by comparing the values +impl Eq for UnblindedOutput {} + +impl PartialEq for UnblindedOutput { + fn eq(&self, other: &UnblindedOutput) -> bool { + self.value == other.value + } +} + +impl PartialOrd for UnblindedOutput { + fn partial_cmp(&self, other: &Self) -> Option { + self.value.partial_cmp(&other.value) + } +} + +impl Ord for UnblindedOutput { + fn cmp(&self, other: &Self) -> Ordering { + self.value.cmp(&other.value) + } +} + +//---------------------------------------- TransactionInput ----------------------------------------------------// + /// A transaction input. /// /// Primarily a reference to an output being spent by the transaction. -#[derive(Debug, Clone, HashableOrdering)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct TransactionInput { - /// The features of the output being spent. We will check maturity for coinbase output. + /// The features of the output being spent. We will check maturity for all outputs. pub features: OutputFeatures, /// The commitment referencing the output being spent. pub commitment: Commitment, @@ -89,25 +225,42 @@ impl TransactionInput { } /// Accessor method for the commitment contained in an input - pub fn commitment(&self) -> Commitment { - self.commitment + pub fn commitment(&self) -> &Commitment { + &self.commitment + } + + /// Checks if the given un-blinded input instance corresponds to this blinded Transaction Input + pub fn opened_by(&self, input: &UnblindedOutput, factory: &CommitmentFactory) -> bool { + factory.open(&input.spending_key, &input.value.into(), &self.commitment) + } +} + +impl From for TransactionInput { + fn from(item: TransactionOutput) -> Self { + TransactionInput { + features: item.features, + commitment: item.commitment, + } } } /// Implement the canonical hashing function for TransactionInput for use in ordering impl Hashable for TransactionInput { fn hash(&self) -> Vec { - let mut hasher = Hasher::new(); - hasher.input(vec![self.features.bits]); - hasher.input(self.commitment.as_bytes()); - hasher.result().to_vec() + HashDigest::new() + .chain(self.features.to_bytes()) + .chain(self.commitment.as_bytes()) + .result() + .to_vec() } } +//---------------------------------------- TransactionOutput ----------------------------------------------------// + /// Output for a transaction, defining the new ownership of coins that are being transferred. The commitment is a /// blinded value for the output while the range proof guarantees the commitment includes a positive value without /// overflow and the ownership of the private key. -#[derive(Debug, Copy, Clone, HashableOrdering)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct TransactionOutput { /// Options for an output's structure or use pub features: OutputFeatures, @@ -117,7 +270,7 @@ pub struct TransactionOutput { pub proof: RangeProof, } -/// An output for a transaction, includes a rangeproof +/// An output for a transaction, includes a range proof impl TransactionOutput { /// Create new Transaction Output pub fn new(features: OutputFeatures, commitment: Commitment, proof: RangeProof) -> TransactionOutput { @@ -129,120 +282,189 @@ impl TransactionOutput { } /// Accessor method for the commitment contained in an output - pub fn commitment(&self) -> Commitment { - self.commitment + pub fn commitment(&self) -> &Commitment { + &self.commitment } /// Accessor method for the range proof contained in an output - pub fn proof(&self) -> RangeProof { - self.proof + pub fn proof(&self) -> &RangeProof { + &self.proof + } + + /// Verify that range proof is valid + pub fn verify_range_proof(&self, prover: &RangeProofService) -> Result { + Ok(prover.verify(&self.proof.to_vec(), &self.commitment)) } } -/// Implement the canonical hashing function for TransactionOutput for use in ordering +/// Implement the canonical hashing function for TransactionOutput for use in ordering. +/// +/// We can exclude the range proof from this hash. The rationale for this is: +/// a) It is a significant performance boost, since the RP is the biggest part of an output +/// b) Range proofs are committed to elsewhere and so we'd be hashing them twice (and as mentioned, this is slow) +/// c) TransactionInputs will now have the same hash as UTXOs, which makes locating STXOs easier when doing re-orgs impl Hashable for TransactionOutput { fn hash(&self) -> Vec { - let mut hasher = Hasher::new(); - hasher.input(vec![self.features.bits]); - hasher.input(self.commitment.as_bytes()); - hasher.input(self.proof.0); - hasher.result().to_vec() + HashDigest::new() + .chain(self.features.to_bytes()) + .chain(self.commitment.as_bytes()) + // .chain(range proof) // See docs as to why we exclude this + .result() + .to_vec() } } +impl Default for TransactionOutput { + fn default() -> Self { + TransactionOutput::new( + OutputFeatures::default(), + CommitmentFactory::default().zero(), + RangeProof::default(), + ) + } +} + +//---------------------------------------- Transaction Kernel ----------------------------------------------------// + /// The transaction kernel tracks the excess for a given transaction. For an explanation of what the excess is, and /// why it is necessary, refer to the /// [Mimblewimble TLU post](https://tlu.tarilabs.com/protocols/mimblewimble-1/sources/PITCHME.link.html?highlight=mimblewimble#mimblewimble). /// The kernel also tracks other transaction metadata, such as the lock height for the transaction (i.e. the earliest /// this transaction can be mined) and the transaction fee, in cleartext. -#[derive(Debug, Clone, HashableOrdering)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct TransactionKernel { /// Options for a kernel's structure or use pub features: KernelFeatures, /// Fee originally included in the transaction this proof is for. - pub fee: u64, + pub fee: MicroTari, /// This kernel is not valid earlier than lock_height blocks /// The max lock_height of all *inputs* to this transaction pub lock_height: u64, /// Remainder of the sum of all transaction commitments. If the transaction /// is well formed, amounts components should sum to zero and the excess /// is hence a valid public key. - pub excess: Option, + pub excess: Commitment, /// The signature proving the excess is a valid public key, which signs /// the transaction fee. - pub excess_sig: Option, + pub excess_sig: Signature, +} + +/// A version of Transaction kernel with optional fields. This struct is only used in constructing transaction kernels +pub struct KernelBuilder { + features: KernelFeatures, + fee: MicroTari, + lock_height: u64, + excess: Option, + excess_sig: Option, } /// Implementation of the transaction kernel -impl TransactionKernel { +impl KernelBuilder { /// Creates an empty transaction kernel - pub fn empty() -> TransactionKernel { - TransactionKernel { - features: KernelFeatures::empty(), - fee: 0, - lock_height: 0, - excess: None, - excess_sig: None, - } + pub fn new() -> KernelBuilder { + KernelBuilder::default() + } + + /// Build a transaction kernel with the provided features + pub fn with_features(mut self, features: KernelFeatures) -> KernelBuilder { + self.features = features; + self } /// Build a transaction kernel with the provided fee - pub fn with_fee(mut self, fee: u64) -> TransactionKernel { + pub fn with_fee(mut self, fee: MicroTari) -> KernelBuilder { self.fee = fee; self } /// Build a transaction kernel with the provided lock height - pub fn with_lock_height(mut self, lock_height: u64) -> TransactionKernel { + pub fn with_lock_height(mut self, lock_height: u64) -> KernelBuilder { self.lock_height = lock_height; self } - pub fn verify_signature(&self) -> Result<(), TransactionError> { + /// Add the excess (sum of public spend keys minus the offset) + pub fn with_excess(mut self, excess: &Commitment) -> KernelBuilder { + self.excess = Some(excess.clone()); + self + } + + /// Add the excess signature + pub fn with_signature(mut self, signature: &Signature) -> KernelBuilder { + self.excess_sig = Some(signature.clone()); + self + } + + pub fn build(self) -> Result { if self.excess.is_none() || self.excess_sig.is_none() { return Err(TransactionError::NoSignatureError); } + Ok(TransactionKernel { + features: self.features, + fee: self.fee, + lock_height: self.lock_height, + excess: self.excess.unwrap(), + excess_sig: self.excess_sig.unwrap(), + }) + } +} - let signature = self.excess_sig.unwrap(); - let excess = self.excess.unwrap(); - let excess = excess.as_public_key(); - let r = signature.get_public_nonce(); - let c = Challenge::::new() - .concat(r.as_bytes()) - .concat(excess.clone().as_bytes()) - .concat(&self.fee.to_le_bytes()) - .concat(&self.lock_height.to_le_bytes()); - - if signature.verify_challenge(excess, c) { - return Ok(()); +impl Default for KernelBuilder { + fn default() -> Self { + KernelBuilder { + features: KernelFeatures::empty(), + fee: MicroTari::from(0), + lock_height: 0, + excess: None, + excess_sig: None, + } + } +} + +impl TransactionKernel { + pub fn verify_signature(&self) -> Result<(), TransactionError> { + let excess = self.excess.as_public_key(); + let r = self.excess_sig.get_public_nonce(); + let m = TransactionMetadata { + lock_height: self.lock_height, + fee: self.fee, + }; + let c = build_challenge(r, &m); + if self.excess_sig.verify_challenge(excess, &c) { + Ok(()) } else { - return Err(TransactionError::InvalidSignatureError); + Err(TransactionError::InvalidSignatureError) } } } -/// Implement the canonical hashing function for TransactionKernel for use in ordering impl Hashable for TransactionKernel { + /// Produce a canonical hash for a transaction kernel. The hash is given by + /// $$ H(feature_bits | fee | lock_height | P_excess | R_sum | s_sum) fn hash(&self) -> Vec { - let mut hasher = Hasher::new(); - hasher.input(vec![self.features.bits]); - hasher.input(self.fee.to_le_bytes()); - hasher.input(self.lock_height.to_le_bytes()); - if self.excess.is_some() { - hasher.input(self.excess.unwrap().as_bytes()); - } - if self.excess_sig.is_some() { - hasher.input(self.excess_sig.unwrap().get_signature().as_bytes()); - } - hasher.result().to_vec() + HashDigest::new() + .chain(&[self.features.bits]) + .chain(u64::from(self.fee).to_le_bytes()) + .chain(self.lock_height.to_le_bytes()) + .chain(self.excess.as_bytes()) + .chain(self.excess_sig.get_public_nonce().as_bytes()) + .chain(self.excess_sig.get_signature().as_bytes()) + .result() + .to_vec() } } +//---------------------------------------- Transaction ----------------------------------------------------// + /// A transaction which consists of a kernel offset and an aggregate body made up of inputs, outputs and kernels. +/// This struct is used to describe single transactions only. The common part between transactions and Tari blocks is +/// accessible via the `body` field, but single transactions also need to carry the public offset around with them so +/// that these can be aggregated into block offsets. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Transaction { /// This kernel offset will be accumulated when transactions are aggregated to prevent the "subset" problem where /// kernels can be linked to inputs and outputs by testing a series of subsets and see which produce valid - /// transactions + /// transactions. pub offset: BlindingFactor, /// The constituents of a transaction which has the same structure as the body of a block. pub body: AggregateBody, @@ -263,72 +485,38 @@ impl Transaction { } } - /// Calculate the sum of the inputs and outputs including the fees - fn sum_commitments(&self, fees: u64) -> Commitment { - let fee_commitment = CommitmentFactory::create(&RistrettoSecretKey::default(), &RistrettoSecretKey::from(fees)); - - let outputs_minus_inputs = &self - .body - .outputs - .iter() - .fold(CommitmentFactory::zero(), |acc, val| &acc + &val.commitment) - - &self - .body - .inputs - .iter() - .fold(CommitmentFactory::zero(), |acc, val| &acc + &val.commitment); - - &outputs_minus_inputs + &fee_commitment - } - - /// Calculate the sum of the kernels, taking into account the offset if it exists, and their constituent fees - fn sum_kernels(&self) -> KernelSum { - // Sum all kernel excesses and fees - let mut kernel_sum = self.body.kernels.iter().fold( - KernelSum { - fees: 0u64, - sum: CommitmentFactory::zero(), - }, - |acc, val| KernelSum { - fees: &acc.fees + val.fee, - sum: &acc.sum + &val.excess.unwrap_or(CommitmentFactory::zero()), - }, - ); - - // Add the offset commitment - kernel_sum.sum = - kernel_sum.sum + CommitmentFactory::create(&self.offset.into(), &RistrettoSecretKey::default()); - - kernel_sum - } - - /// Confirm that the (sum of the outputs) - (sum of inputs) = Kernel excess - fn validate_kernel_sum(&self) -> Result<(), TransactionError> { - let kernel_sum = self.sum_kernels(); - let sum_io = self.sum_commitments(kernel_sum.fees); - - if kernel_sum.sum != sum_io { - return Err(TransactionError::ValidationError); - } + /// Validate this transaction by checking the following: + /// 1. The sum of inputs, outputs and fees equal the (public excess value + offset) + /// 1. The signature signs the canonical message with the private excess + /// 1. Range proofs of the outputs are valid + /// + /// This function does NOT check that inputs come from the UTXO set + pub fn validate_internal_consistency( + &mut self, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result<(), TransactionError> + { + self.body + .validate_internal_consistency(&self.offset, MicroTari::from(0), prover, factory) + } - Ok(()) + pub fn get_body(&self) -> &AggregateBody { + &self.body } - /// Validate this transaction - pub fn validate(&self) -> Result<(), TransactionError> { - self.body.verify_kernel_signatures()?; - self.validate_kernel_sum()?; - Ok(()) + /// Returns the byte size or weight of a transaction + pub fn calculate_weight(&self) -> u64 { + Fee::calculate_weight(self.body.inputs.len(), self.body.outputs.len()) } -} -/// This struct holds the result of calculating the sum of the kernels in a Transaction -/// and returns the summed commitments and the total fees -pub struct KernelSum { - pub sum: Commitment, - pub fees: u64, + /// Returns the total fee allocated to each byte of the transaction + pub fn calculate_ave_fee_per_gram(&self) -> f64 { + (self.body.get_total_fee().0 as f64) / self.calculate_weight() as f64 + } } +//---------------------------------------- Transaction Builder ----------------------------------------------------// pub struct TransactionBuilder { body: AggregateBody, offset: Option, @@ -337,182 +525,128 @@ pub struct TransactionBuilder { impl TransactionBuilder { /// Create an new empty TransactionBuilder pub fn new() -> Self { - Self { - offset: None, - body: AggregateBody::empty(), - } + Self::default() } /// Update the offset of an existing transaction - pub fn add_offset(mut self, offset: BlindingFactor) -> Self { + pub fn add_offset(&mut self, offset: BlindingFactor) -> &mut Self { self.offset = Some(offset); self } /// Add an input to an existing transaction - pub fn add_input(mut self, input: TransactionInput) -> Self { - self.body = self.body.add_input(input); + pub fn add_input(&mut self, input: TransactionInput) -> &mut Self { + self.body.add_input(input); self } /// Add an output to an existing transaction - pub fn add_output(mut self, output: TransactionOutput) -> Self { - self.body = self.body.add_output(output); + pub fn add_output(&mut self, output: TransactionOutput) -> &mut Self { + self.body.add_output(output); self } - /// Add a series of inputs to an existing transaction - pub fn add_inputs(mut self, inputs: Vec) -> Self { - self.body = self.body.add_inputs(inputs); + /// Moves a series of inputs to an existing transaction, leaving `inputs` empty + pub fn add_inputs(&mut self, inputs: &mut Vec) -> &mut Self { + self.body.add_inputs(inputs); self } - /// Add a series of outputs to an existing transaction - pub fn add_outputs(mut self, outputs: Vec) -> Self { - self.body = self.body.add_outputs(outputs); + /// Moves a series of outputs to an existing transaction, leaving `outputs` empty + pub fn add_outputs(&mut self, outputs: &mut Vec) -> &mut Self { + self.body.add_outputs(outputs); self } /// Set the kernel of a transaction. Currently only one kernel is allowed per transaction - pub fn with_kernel(mut self, kernel: TransactionKernel) -> Self { - self.body = self.body.set_kernel(kernel); + pub fn with_kernel(&mut self, kernel: TransactionKernel) -> &mut Self { + self.body.set_kernel(kernel); self } - pub fn build(&self) -> Result { + pub fn build( + self, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result + { if let Some(offset) = self.offset { - let tx = Transaction::new( - self.body.inputs.clone(), - self.body.outputs.clone(), - self.body.kernels.clone(), - offset, - ); - tx.validate()?; + let mut tx = Transaction::new(self.body.inputs, self.body.outputs, self.body.kernels, offset); + tx.validate_internal_consistency(prover, factory)?; Ok(tx) } else { - return Err(TransactionError::ValidationError); + return Err(TransactionError::ValidationError( + "Transaction validation failed".into(), + )); } } } +impl Default for TransactionBuilder { + fn default() -> Self { + Self { + offset: None, + body: AggregateBody::empty(), + } + } +} + +//----------------------------------------- Tests ----------------------------------------------------// + #[cfg(test)] mod test { use super::*; use crate::{ - range_proof::RangeProof, - transaction::{KernelFeatures, OutputFeatures, TransactionInput, TransactionKernel, TransactionOutput}, - types::{BlindingFactor, PublicKey}, + transaction::OutputFeatures, + types::{BlindingFactor, PrivateKey, RangeProof}, }; use rand; use tari_crypto::{ - challenge::Challenge, - common::Blake256, - keys::{PublicKey as PublicKeyTrait, SecretKey}, + keys::SecretKey as SecretKeyTrait, + ristretto::{dalek_range_proof::DalekRangeProofService, pedersen::PedersenCommitmentFactory}, }; - use tari_utilities::ByteArray; #[test] - fn build_transaction_test_and_validation() { + fn unblinded_input() { let mut rng = rand::OsRng::new().unwrap(); + let k = BlindingFactor::random(&mut rng); + let factory = PedersenCommitmentFactory::default(); + let i = UnblindedOutput::new(10.into(), k, None); + let input = i.as_transaction_input(&factory, OutputFeatures::default()); + assert_eq!(input.features, OutputFeatures::default()); + assert!(input.opened_by(&i, &factory)); + } - let input_secret_key = BlindingFactor::random(&mut rng); - let input_secret_key2 = BlindingFactor::random(&mut rng); - let change_secret_key = BlindingFactor::random(&mut rng); - let receiver_secret_key = BlindingFactor::random(&mut rng); - let receiver_secret_key2 = BlindingFactor::random(&mut rng); - let receiver_full_secret_key = &receiver_secret_key + &receiver_secret_key2; - - let input = TransactionInput::new( - OutputFeatures::empty(), - CommitmentFactory::create(&input_secret_key, &RistrettoSecretKey::from(12u64)), - ); - - let change_output = TransactionOutput::new( - OutputFeatures::empty(), - CommitmentFactory::create(&change_secret_key, &RistrettoSecretKey::from(4u64)), - RangeProof([0; 1]), - ); - - let output = TransactionOutput::new( - OutputFeatures::empty(), - CommitmentFactory::create(&receiver_secret_key, &RistrettoSecretKey::from(7u64)), - RangeProof([0; 1]), - ); - - let offset: BlindingFactor = BlindingFactor::random(&mut rng).into(); - let sender_private_nonce = BlindingFactor::random(&mut rng); - let sender_public_nonce = PublicKey::from_secret_key(&sender_private_nonce); - let fee = 1u64; - let lock_height = 0u64; - - // Create a transaction - let tx_builder = TransactionBuilder::new() - .add_input(input.clone()) - .add_output(output) - .add_output(change_output) - .add_offset(offset.clone()); - - // Test adding inputs and outputs in vector form - let input2 = TransactionInput::new( - OutputFeatures::empty(), - CommitmentFactory::create(&input_secret_key2, &RistrettoSecretKey::from(2u64)), - ); - let output2 = TransactionOutput::new( - OutputFeatures::empty(), - CommitmentFactory::create(&receiver_secret_key2, &RistrettoSecretKey::from(2u64)), - RangeProof([0; 1]), - ); - - let tx_builder = tx_builder - .add_inputs(vec![input2.clone()]) - .add_outputs(vec![output2.clone()]); - - // Should fail the validation because there is no kernel yet. - let tx = tx_builder.build(); - assert!(tx.is_err()); - - // Calculate Excess - let mut sender_excess_key = &change_secret_key - &input_secret_key; - sender_excess_key = &sender_excess_key - &input_secret_key2; - sender_excess_key = &sender_excess_key - &offset; - - let sender_public_excess = PublicKey::from_secret_key(&sender_excess_key); - // Receiver generate partial signatures - - let mut final_excess = &output.commitment + &change_output.commitment; - let zero = RistrettoSecretKey::default(); - final_excess = &final_excess + &output2.commitment; - final_excess = &final_excess - &input.commitment; - final_excess = &final_excess - &input2.commitment; - final_excess = &final_excess + &CommitmentFactory::create(&zero, &RistrettoSecretKey::from(fee)); // add fee - final_excess = &final_excess - &CommitmentFactory::create(&offset, &zero); // subtract Offset - - let receiver_private_nonce = BlindingFactor::random(&mut rng); - let receiver_public_nonce = PublicKey::from_secret_key(&receiver_private_nonce); - let receiver_public_key = PublicKey::from_secret_key(&receiver_full_secret_key); - - let challenge = Challenge::::new() - .concat((&sender_public_nonce + &receiver_public_nonce).as_bytes()) - .concat((&sender_public_excess + &receiver_public_key).as_bytes()) - .concat(&fee.to_le_bytes()) - .concat(&lock_height.to_le_bytes()); - - let receiver_partial_sig = - Signature::sign(receiver_full_secret_key, receiver_private_nonce, challenge.clone()).unwrap(); - let sender_partial_sig = Signature::sign(sender_excess_key, sender_private_nonce, challenge.clone()).unwrap(); - - let s_agg = &sender_partial_sig + &receiver_partial_sig; - - // Create a kernel with a fee (taken into account in the creation of the inputs and outputs - let kernel = TransactionKernel { - features: KernelFeatures::empty(), - fee, - lock_height, - excess: Some(final_excess), - excess_sig: Some(s_agg), - }; - - let tx = tx_builder.with_kernel(kernel).build().unwrap(); - tx.validate().unwrap(); + #[test] + fn range_proof_verification() { + let mut rng = rand::OsRng::new().unwrap(); + let factory = PedersenCommitmentFactory::default(); + let prover = DalekRangeProofService::new(32, &factory).unwrap(); + // Directly test the tx_output verification + let k1 = BlindingFactor::random(&mut rng); + let k2 = BlindingFactor::random(&mut rng); + + // For testing the max range has been limited to 2^32 so this value is too large. + let unblinded_output1 = UnblindedOutput::new((2u64.pow(32) - 1u64).into(), k1, None); + let tx_output1 = unblinded_output1 + .as_transaction_output(&prover, &factory, OutputFeatures::default()) + .unwrap(); + assert!(tx_output1.verify_range_proof(&prover).unwrap()); + + let unblinded_output2 = UnblindedOutput::new((2u64.pow(32) + 1u64).into(), k2.clone(), None); + let tx_output2 = unblinded_output2.as_transaction_output(&prover, &factory, OutputFeatures::default()); + + match tx_output2 { + Ok(_) => panic!("Range proof should have failed to verify"), + Err(e) => assert_eq!( + e, + TransactionError::ValidationError("Range proof could not be verified".to_string()) + ), + } + let v = PrivateKey::from(2u64.pow(32) + 1); + let c = factory.commit(&k2, &v); + let proof = prover.construct_proof(&k2, 2u64.pow(32) + 1).unwrap(); + let tx_output3 = TransactionOutput::new(OutputFeatures::default(), c, RangeProof::from_bytes(&proof).unwrap()); + assert_eq!(tx_output3.verify_range_proof(&prover).unwrap(), false); } } diff --git a/base_layer/core/src/transaction_protocol/mod.rs b/base_layer/core/src/transaction_protocol/mod.rs new file mode 100644 index 0000000000..4d96f6d61f --- /dev/null +++ b/base_layer/core/src/transaction_protocol/mod.rs @@ -0,0 +1,114 @@ +//! Transaction Protocol Manager facilitates the process of constructing a Mimblewimble transaction between two parties. +//! +//! The Transaction Protocol Manager implements a protocol to construct a Mimwblewimble transaction between two parties +//! , a Sender and a Receiver. In this transaction the Sender is paying the Receiver from their inputs and also paying +//! to as many change outputs as they like. The Receiver will receive a single output from this transaction. +//! The module consists of three main components: +//! - A Builder for the initial Sender state data +//! - A SenderTransactionProtocolManager which manages the Sender's state machine +//! - A ReceiverTransactionProtocolManager which manages the Receiver's state machine. +//! +//! The two state machines run in parallel and will be managed by each respective party. Each state machine has methods +//! to construct and accept the public data messages that needs to be transmitted between the parties. The diagram below +//! illustrates the progression of the two state machines and shows where the public data messages are constructed and +//! accepted in each state machine +//! +//!
+//! sequenceDiagram +//! participant Sender +//! participant Receivers +//! # +//! activate Sender +//! Sender-->>Sender: initialize +//! deactivate Sender +//! # +//! activate Sender +//! Sender-->>+Receivers: [tx_id, amount_i] +//! note left of Sender: CollectingPubKeys +//! note right of Receivers: Initialization +//! Receivers-->>-Sender: [tx_id, Pi, Ri] +//! deactivate Sender +//! # +//! alt invalid +//! Sender--XSender: failed +//! end +//! # +//! activate Sender +//! Sender-->>+Receivers: [tx_id, ΣR, ΣP] +//! note left of Sender: CollectingSignatures +//! note right of Receivers: Signing +//! Receivers-->>Receivers: create output and sign +//! Receivers-->>-Sender: [tx_id, Output_i, s_i] +//! deactivate Sender +//! # +//! note left of Sender: Finalizing +//! alt is_valid() +//! Sender-->>Sender: Finalized +//! else invalid +//! Sender--XSender: Failed +//! end +//!
+ +#[cfg(test)] +pub mod test_common; + +pub mod recipient; +pub mod sender; +pub mod single_receiver; +pub mod transaction_initializer; + +use crate::{ + tari_amount::*, + transaction::TransactionError, + types::{Challenge, MessageHash, PublicKey}, +}; +use derive_error::Error; +use digest::Digest; +use serde::{Deserialize, Serialize}; +use tari_crypto::{range_proof::RangeProofError, signatures::SchnorrSignatureError}; +use tari_utilities::byte_array::ByteArray; + +#[derive(Clone, Debug, PartialEq, Error)] +pub enum TransactionProtocolError { + // The current state is not yet completed, cannot transition to next state + #[error(msg_embedded, no_from, non_std)] + IncompleteStateError(String), + #[error(msg_embedded, no_from, non_std)] + ValidationError(String), + // Invalid state transition + InvalidTransitionError, + // Invalid state + InvalidStateError, + // An error occurred while performing a signature + SigningError(SchnorrSignatureError), + // A signature verification failed + InvalidSignatureError, + // An error occurred while building the final transaction + TransactionBuildError(TransactionError), + // The transaction construction broke down due to communication failure + TimeoutError, + // An error was produced while constructing a rangeproof + RangeProofError(RangeProofError), + // This set of parameters is currently not supported + #[error(msg_embedded, no_from, non_std)] + UnsupportedError(String), +} + +/// Transaction metadata, including the fee and lock height +#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)] +pub struct TransactionMetadata { + /// The absolute fee for the transaction + pub fee: MicroTari, + /// The earliest block this transaction can be mined + pub lock_height: u64, +} + +/// Convenience function that calculates the challenge for the Schnorr signatures +pub fn build_challenge(sum_public_nonces: &PublicKey, metadata: &TransactionMetadata) -> MessageHash { + Challenge::new() + .chain(sum_public_nonces.as_bytes()) + .chain(&u64::from(metadata.fee).to_le_bytes()) + .chain(&metadata.lock_height.to_le_bytes()) + .result() + .to_vec() +} diff --git a/base_layer/core/src/transaction_protocol/recipient.rs b/base_layer/core/src/transaction_protocol/recipient.rs new file mode 100644 index 0000000000..9ae1031bdd --- /dev/null +++ b/base_layer/core/src/transaction_protocol/recipient.rs @@ -0,0 +1,219 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + transaction::{OutputFeatures, TransactionOutput}, + transaction_protocol::{ + sender::{SingleRoundSenderData as SD, TransactionSenderMessage}, + single_receiver::SingleReceiverTransactionProtocol, + TransactionProtocolError, + }, + types::{CommitmentFactory, MessageHash, PrivateKey, PublicKey, RangeProofService, Signature}, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, convert::TryInto}; +use tari_comms::message::{Message, MessageError}; +use tari_p2p::tari_message::{BlockchainMessage, TariMessageType}; + +#[derive(Clone, Debug)] +pub enum RecipientState { + Finalized(Box), + Failed(TransactionProtocolError), +} + +/// An enum describing the types of information that a recipient can send back to the receiver +#[derive(Debug, Clone)] +pub(super) enum RecipientInfo { + None, + Single(Option>), + Multiple(HashMap), +} + +#[derive(Debug, Clone)] +pub(super) struct MultiRecipientInfo { + pub commitment: MessageHash, + pub data: RecipientSignedMessage, +} + +/// This is the message containing the public data that the Receiver will send back to the Sender +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RecipientSignedMessage { + pub tx_id: u64, + pub output: TransactionOutput, + pub public_spend_key: PublicKey, + pub partial_signature: Signature, +} + +/// Convert `RecipientSignedMessage` into a Tari Message that can be sent via the tari comms stack +impl TryInto for RecipientSignedMessage { + type Error = MessageError; + + fn try_into(self) -> Result { + Ok((TariMessageType::new(BlockchainMessage::TransactionReply), self).try_into()?) + } +} + +/// The generalised transaction recipient protocol. A different state transition network is followed depending on +/// whether this is a single recipient or one of many. +#[derive(Clone, Debug)] +pub struct ReceiverTransactionProtocol { + pub state: RecipientState, +} + +/// Initiate a new recipient protocol state. +/// +/// It takes as input the transaction message from the sender (which will indicate how many rounds the transaction +/// protocol will undergo, the recipient's nonce and spend key, as well as the output features for this recipient's +/// transaction output. +/// +/// The function returns the protocol in the relevant state. If this is a single-round protocol, the state will +/// already be finalised, and the return message will be accessible from the `get_signed_data` method. +impl ReceiverTransactionProtocol { + pub fn new( + info: TransactionSenderMessage, + nonce: PrivateKey, + spending_key: PrivateKey, + features: OutputFeatures, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> ReceiverTransactionProtocol + { + let state = match info { + TransactionSenderMessage::None => RecipientState::Failed(TransactionProtocolError::InvalidStateError), + TransactionSenderMessage::Single(v) => { + ReceiverTransactionProtocol::single_round(nonce, spending_key, features, &v, prover, factory) + }, + TransactionSenderMessage::Multiple => Self::multi_round(), + }; + ReceiverTransactionProtocol { state } + } + + /// Returns true if the recipient protocol is finalised, and the signature data is ready to be sent to the sender. + pub fn is_finalized(&self) -> bool { + match self.state { + RecipientState::Finalized(_) => true, + _ => false, + } + } + + /// Method to determine if the transaction protocol has failed + pub fn is_failed(&self) -> bool { + match &self.state { + RecipientState::Failed(_) => true, + _ => false, + } + } + + /// Method to return the error behind a failure, if one has occurred + pub fn failure_reason(&self) -> Option { + match &self.state { + RecipientState::Failed(e) => Some(e.clone()), + _ => None, + } + } + + /// Retrieve the final signature data to be returned to the sender to complete the transaction. + pub fn get_signed_data(&self) -> Result<&RecipientSignedMessage, TransactionProtocolError> { + match &self.state { + RecipientState::Finalized(data) => Ok(data), + _ => Err(TransactionProtocolError::InvalidStateError), + } + } + + /// Run the single-round recipient protocol, which can immediately construct an output and sign the data + fn single_round( + nonce: PrivateKey, + key: PrivateKey, + features: OutputFeatures, + data: &SD, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> RecipientState + { + let signer = SingleReceiverTransactionProtocol::create(data, nonce, key, features, prover, factory); + match signer { + Ok(signed_data) => RecipientState::Finalized(Box::new(signed_data)), + Err(e) => RecipientState::Failed(e), + } + } + + fn multi_round() -> RecipientState { + RecipientState::Failed(TransactionProtocolError::UnsupportedError( + "Multiple recipients aren't supported yet".into(), + )) + } +} + +#[cfg(test)] +mod test { + use crate::{ + tari_amount::*, + transaction::OutputFeatures, + transaction_protocol::{ + build_challenge, + sender::{SingleRoundSenderData, TransactionSenderMessage}, + test_common::TestParams, + TransactionMetadata, + }, + types::{PublicKey, Signature, COMMITMENT_FACTORY, PROVER}, + ReceiverTransactionProtocol, + }; + use rand::OsRng; + use tari_crypto::{commitment::HomomorphicCommitmentFactory, keys::PublicKey as PK}; + + #[test] + fn single_round_recipient() { + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let m = TransactionMetadata { + fee: MicroTari(125), + lock_height: 0, + }; + let msg = SingleRoundSenderData { + tx_id: 15, + amount: MicroTari(500), + public_excess: PublicKey::from_secret_key(&p.spend_key), // any random key will do + public_nonce: PublicKey::from_secret_key(&p.change_key), // any random key will do + metadata: m.clone(), + }; + let sender_info = TransactionSenderMessage::Single(Box::new(msg.clone())); + let pubkey = PublicKey::from_secret_key(&p.spend_key); + let receiver = ReceiverTransactionProtocol::new( + sender_info, + p.nonce.clone(), + p.spend_key.clone(), + OutputFeatures::default(), + &PROVER, + &COMMITMENT_FACTORY, + ); + assert!(receiver.is_finalized()); + let data = receiver.get_signed_data().unwrap(); + assert_eq!(data.tx_id, 15); + assert_eq!(data.public_spend_key, pubkey); + assert!(COMMITMENT_FACTORY.open_value(&p.spend_key, 500, &data.output.commitment)); + assert!(data.output.verify_range_proof(&PROVER).unwrap()); + let r_sum = &msg.public_nonce + &p.public_nonce; + let e = build_challenge(&r_sum, &m); + let s = Signature::sign(p.spend_key.clone(), p.nonce.clone(), &e).unwrap(); + assert_eq!(data.partial_signature, s); + } +} diff --git a/base_layer/core/src/transaction_protocol/sender.rs b/base_layer/core/src/transaction_protocol/sender.rs new file mode 100644 index 0000000000..13148e50bd --- /dev/null +++ b/base_layer/core/src/transaction_protocol/sender.rs @@ -0,0 +1,657 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + tari_amount::*, + transaction::{KernelFeatures, Transaction, TransactionBuilder, TransactionInput, TransactionOutput}, + types::{BlindingFactor, CommitmentFactory, PrivateKey, PublicKey, RangeProofService, Signature}, +}; + +use crate::{ + transaction::{KernelBuilder, MAX_TRANSACTION_INPUTS, MAX_TRANSACTION_OUTPUTS, MINIMUM_TRANSACTION_FEE}, + transaction_protocol::{ + build_challenge, + recipient::{RecipientInfo, RecipientSignedMessage}, + transaction_initializer::SenderTransactionInitializer, + TransactionMetadata, + TransactionProtocolError as TPE, + }, +}; +use digest::Digest; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use tari_comms::message::{Message, MessageError}; +use tari_crypto::ristretto::pedersen::PedersenCommitment; +use tari_p2p::tari_message::{BlockchainMessage, TariMessageType}; +use tari_utilities::ByteArray; + +//---------------------------------------- Local Data types ----------------------------------------------------// + +/// This struct contains all the information that a transaction initiator (the sender) will manage throughout the +/// Transaction construction process. +#[derive(Clone, Debug)] +pub(super) struct RawTransactionInfo { + pub num_recipients: usize, + // The sum of self-created outputs plus change + pub amount_to_self: MicroTari, + pub ids: Vec, + pub amounts: Vec, + pub metadata: TransactionMetadata, + pub inputs: Vec, + pub outputs: Vec, + pub offset: BlindingFactor, + // The sender's blinding factor shifted by the sender-selected offset + pub offset_blinding_factor: BlindingFactor, + pub public_excess: PublicKey, + // The sender's private nonce + pub private_nonce: PrivateKey, + // The sender's public nonce + pub public_nonce: PublicKey, + // The sum of all public nonces + pub public_nonce_sum: PublicKey, + pub recipient_info: RecipientInfo, + pub signatures: Vec, +} + +impl RawTransactionInfo { + pub fn calculate_total_amount(&self) -> MicroTari { + let to_others: MicroTari = self.amounts.iter().sum(); + to_others + self.amount_to_self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct SingleRoundSenderData { + /// The transaction id for the recipient + pub tx_id: u64, + /// The amount, in µT, being sent to the recipient + pub amount: MicroTari, + /// The offset public excess for this transaction + pub public_excess: PublicKey, + /// The sender's public nonce + pub public_nonce: PublicKey, + /// The transaction metadata + pub metadata: TransactionMetadata, +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum TransactionSenderMessage { + None, + Single(Box), + // TODO: Three round types + Multiple, +} + +/// Convert `SenderMessage` into a Tari Message that can be sent via the tari comms stack +impl TryInto for TransactionSenderMessage { + type Error = MessageError; + + fn try_into(self) -> Result { + Ok((TariMessageType::new(BlockchainMessage::Transaction), self).try_into()?) + } +} + +//---------------------------------------- Sender State Protocol ----------------------------------------------------// +#[derive(Clone, Debug)] +pub struct SenderTransactionProtocol { + pub(super) state: SenderState, +} + +impl SenderTransactionProtocol { + /// Begin constructing a new transaction. All the up-front data is collected via the `SenderTransactionInitializer` + /// builder function + pub fn builder(num_recipients: usize) -> SenderTransactionInitializer { + SenderTransactionInitializer::new(num_recipients) + } + + /// Convenience method to check whether we're receiving recipient data + pub fn is_collecting_single_signature(&self) -> bool { + match &self.state { + SenderState::CollectingSingleSignature(_) => true, + _ => false, + } + } + + /// Convenience method to check whether we're ready to send a message to a single recipient + pub fn is_single_round_message_ready(&self) -> bool { + match &self.state { + SenderState::SingleRoundMessageReady(_) => true, + _ => false, + } + } + + /// Method to determine if we are in the SenderState::Finalizing state + pub fn is_finalizing(&self) -> bool { + match &self.state { + SenderState::Finalizing(_) => true, + _ => false, + } + } + + /// Method to determine if we are in the SenderState::FinalizedTransaction state + pub fn is_finalized(&self) -> bool { + match &self.state { + SenderState::FinalizedTransaction(_) => true, + _ => false, + } + } + + pub fn get_transaction(&self) -> Result<&Transaction, TPE> { + match &self.state { + SenderState::FinalizedTransaction(tx) => Ok(tx), + _ => Err(TPE::InvalidStateError), + } + } + + /// Method to determine if the transaction protocol has failed + pub fn is_failed(&self) -> bool { + match &self.state { + SenderState::Failed(_) => true, + _ => false, + } + } + + /// Method to return the error behind a failure, if one has occurred + pub fn failure_reason(&self) -> Option { + match &self.state { + SenderState::Failed(e) => Some(e.clone()), + _ => None, + } + } + + /// Method to check if the provided tx_id matches this transaction + pub fn check_tx_id(&self, tx_id: u64) -> bool { + match &self.state { + SenderState::Finalizing(info) | + SenderState::SingleRoundMessageReady(info) | + SenderState::CollectingSingleSignature(info) => info.ids[0] == tx_id, + _ => false, + } + } + + pub fn get_tx_id(&self) -> Result { + match &self.state { + SenderState::Finalizing(info) | + SenderState::SingleRoundMessageReady(info) | + SenderState::CollectingSingleSignature(info) => Ok(info.ids[0]), + _ => Err(TPE::InvalidStateError), + } + } + + pub fn get_total_amount(&self) -> Result { + match &self.state { + SenderState::Initializing(info) | + SenderState::Finalizing(info) | + SenderState::SingleRoundMessageReady(info) | + SenderState::CollectingSingleSignature(info) => Ok(info.amounts.iter().sum()), + SenderState::FinalizedTransaction(_) => Err(TPE::InvalidStateError), + SenderState::Failed(_) => Err(TPE::InvalidStateError), + } + } + + /// This function will return the total value of outputs being sent to yourself in the transaction + pub fn get_amount_to_self(&self) -> Result { + match &self.state { + SenderState::Initializing(info) | + SenderState::Finalizing(info) | + SenderState::SingleRoundMessageReady(info) | + SenderState::CollectingSingleSignature(info) => Ok(info.amount_to_self), + SenderState::FinalizedTransaction(_) => Err(TPE::InvalidStateError), + SenderState::Failed(_) => Err(TPE::InvalidStateError), + } + } + + /// Build the sender's message for the single-round protocol (one recipient) and move to next State + pub fn build_single_round_message(&mut self) -> Result { + match &self.state { + SenderState::SingleRoundMessageReady(info) => { + let result = SingleRoundSenderData { + tx_id: info.ids[0], + amount: self.get_total_amount().unwrap(), + public_nonce: info.public_nonce.clone(), + public_excess: info.public_excess.clone(), + metadata: info.metadata.clone(), + }; + self.state = SenderState::CollectingSingleSignature(info.clone()); + Ok(result) + }, + _ => Err(TPE::InvalidStateError), + } + } + + /// Add the signed transaction from the recipient and move to the next state + pub fn add_single_recipient_info( + &mut self, + rec: RecipientSignedMessage, + prover: &RangeProofService, + ) -> Result<(), TPE> + { + match &mut self.state { + SenderState::CollectingSingleSignature(info) => { + if !rec.output.verify_range_proof(prover)? { + return Err(TPE::ValidationError( + "Recipient output range proof failed to verify".into(), + )); + } + // Consolidate transaction info + info.outputs.push(rec.output); + // nonce is in the signature, so we'll add those together later + info.public_excess = &info.public_excess + &rec.public_spend_key; + info.public_nonce_sum = &info.public_nonce_sum + rec.partial_signature.get_public_nonce(); + info.signatures.push(rec.partial_signature); + self.state = SenderState::Finalizing(info.clone()); + Ok(()) + }, + _ => Err(TPE::InvalidStateError), + } + } + + /// Attempts to build the final transaction. + fn build_transaction( + info: &RawTransactionInfo, + features: KernelFeatures, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result + { + let mut tx_builder = TransactionBuilder::new(); + for i in &info.inputs { + tx_builder.add_input(i.clone()); + } + + for o in &info.outputs { + tx_builder.add_output(o.clone()); + } + tx_builder.add_offset(info.offset.clone()); + let mut s_agg = info.signatures[0].clone(); + info.signatures.iter().skip(1).for_each(|s| s_agg = &s_agg + s); + let excess = PedersenCommitment::from_public_key(&info.public_excess); + let kernel = KernelBuilder::new() + .with_fee(info.metadata.fee) + .with_features(features) + .with_lock_height(info.metadata.lock_height) + .with_excess(&excess) + .with_signature(&s_agg) + .build()?; + tx_builder.with_kernel(kernel); + tx_builder.build(prover, factory).map_err(TPE::from) + } + + /// Performs sanitary checks on the collected transaction pieces prior to building the final Transaction instance + fn validate(&self) -> Result<(), TPE> { + if let SenderState::Finalizing(info) = &self.state { + let total_amount = info.calculate_total_amount(); + let fee = info.metadata.fee; + // The fee should be less than the amount. This isn't a protocol requirement, but it's what you want 99.999% + // of the time, and our users will thank us if we reject a tx where they put the amount in the fee field by + // mistake! + if fee > total_amount { + return Err(TPE::ValidationError("Fee is greater than amount".into())); + } + // The fee must be greater than MIN_FEE to prevent spam attacks + if fee < MINIMUM_TRANSACTION_FEE { + return Err(TPE::ValidationError("Fee is less than the minimum".into())); + } + // Prevent overflow attacks by imposing sane limits on some key parameters + if info.inputs.len() > MAX_TRANSACTION_INPUTS { + return Err(TPE::ValidationError("Too many inputs in transaction".into())); + } + if info.outputs.len() > MAX_TRANSACTION_OUTPUTS { + return Err(TPE::ValidationError("Too many outputs in transaction".into())); + } + if info.inputs.is_empty() { + return Err(TPE::ValidationError("A transaction cannot have zero inputs".into())); + } + if info.signatures.len() != 1 + info.num_recipients { + return Err(TPE::ValidationError(format!( + "Incorrect number of signatures ({})", + info.signatures.len() + ))); + } + Ok(()) + } else { + Err(TPE::InvalidStateError) + } + } + + /// Produce the sender's partial signature + fn sign(&mut self) -> Result<(), TPE> { + match &mut self.state { + SenderState::Finalizing(info) => { + let e = build_challenge(&info.public_nonce_sum, &info.metadata); + let k = info.offset_blinding_factor.clone(); + let r = info.private_nonce.clone(); + let s = Signature::sign(k, r, &e).map_err(TPE::SigningError)?; + info.signatures.push(s); + Ok(()) + }, + _ => Err(TPE::InvalidStateError), + } + } + + /// Try and finalise the transaction. If the current state is Finalizing, the result will be whether the + /// transaction was valid or not. If the result is false, the transaction will be in a Failed state. Calling + /// finalize while in any other state will result in an error. + /// + /// First we validate against internal sanity checks, then try build the transaction, and then + /// formally validate the transaction terms (no inflation, signature matches etc). If any step fails, + /// the transaction protocol moves to Failed state and we are done; you can't rescue the situation. The function + /// returns `Ok(false)` in this instance. + pub fn finalize( + &mut self, + features: KernelFeatures, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result + { + // Create the final aggregated signature, moving to the Failed state if anything goes wrong + match &mut self.state { + SenderState::Finalizing(_) => { + if let Err(e) = self.sign() { + self.state = SenderState::Failed(e); + return Ok(false); + } + }, + _ => return Err(TPE::InvalidStateError), + } + // Validate the inputs we have, and then construct the final transaction + match &self.state { + SenderState::Finalizing(info) => { + let result = self + .validate() + .and_then(|_| Self::build_transaction(info, features, prover, factory)); + if let Err(e) = result { + self.state = SenderState::Failed(e); + return Ok(false); + } + let mut transaction = result.unwrap(); + let result = transaction + .validate_internal_consistency(prover, factory) + .map_err(TPE::TransactionBuildError); + if let Err(e) = result { + self.state = SenderState::Failed(e); + return Ok(false); + } + self.state = SenderState::FinalizedTransaction(transaction); + Ok(true) + }, + _ => Err(TPE::InvalidStateError), + } + } +} + +pub fn calculate_tx_id(pub_nonce: &PublicKey, index: usize) -> u64 { + let hash = D::new().chain(pub_nonce.as_bytes()).chain(index.to_le_bytes()).result(); + let mut bytes: [u8; 8] = [0u8; 8]; + bytes.copy_from_slice(&hash[..8]); + u64::from_le_bytes(bytes) +} + +//---------------------------------------- Sender State ----------------------------------------------------// + +/// This enum contains all the states of the Sender state machine +#[derive(Clone, Debug)] +pub(super) enum SenderState { + /// Transitional state that kicks of the relevant transaction protocol + Initializing(Box), + /// The message for the recipient in a single-round scheme is ready + SingleRoundMessageReady(Box), + /// Waiting for the signed transaction data in the single-round protocol + CollectingSingleSignature(Box), + /// The final transaction state is being validated - it will automatically transition to Failed or Finalized from + /// here + Finalizing(Box), + /// The final transaction is ready to be broadcast + FinalizedTransaction(Transaction), + /// An unrecoverable failure has occurred and the transaction must be abandoned + Failed(TPE), +} + +impl SenderState { + /// Puts the Sender FSM into the appropriate initial state, based on the number of recipients. Don't call this + /// function directly. It is called by the `TransactionInitializer` builder + pub(super) fn initialize(self) -> Result { + match self { + SenderState::Initializing(info) => match info.num_recipients { + 0 => Ok(SenderState::Finalizing(info)), + 1 => Ok(SenderState::SingleRoundMessageReady(info)), + _ => Ok(SenderState::Failed(TPE::UnsupportedError( + "Multiple recipients are not supported yet".into(), + ))), + }, + _ => Err(TPE::InvalidTransitionError), + } + } +} + +//---------------------------------------- Tests ----------------------------------------------------// + +#[cfg(test)] +mod test { + use crate::{ + fee::Fee, + tari_amount::*, + transaction::{KernelFeatures, OutputFeatures, UnblindedOutput}, + transaction_protocol::{ + sender::SenderTransactionProtocol, + single_receiver::SingleReceiverTransactionProtocol, + test_common::{make_input, TestParams}, + TransactionProtocolError, + }, + types::{COMMITMENT_FACTORY, PROVER}, + }; + use rand::OsRng; + use tari_crypto::common::Blake256; + use tari_utilities::hex::Hex; + + #[test] + fn zero_recipients() { + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(1200)); + let mut builder = SenderTransactionProtocol::builder(0); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari(10)) + .with_offset(p.offset.clone()) + .with_private_nonce(p.nonce.clone()) + .with_change_secret(p.change_key.clone()) + .with_input(utxo, input) + .with_output(UnblindedOutput::new(MicroTari(500), p.spend_key.clone(), None)) + .with_output(UnblindedOutput::new(MicroTari(400), p.spend_key.clone(), None)); + let mut sender = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + assert_eq!(sender.is_failed(), false); + assert!(sender.is_finalizing()); + match sender.finalize(KernelFeatures::empty(), &PROVER, &COMMITMENT_FACTORY) { + Ok(true) => (), + Ok(false) => panic!("{:?}", sender.failure_reason()), + Err(e) => panic!("{:?}", e), + } + let tx = sender.get_transaction().unwrap(); + assert_eq!(tx.offset, p.offset); + } + + #[test] + fn single_recipient_no_change() { + let mut rng = OsRng::new().unwrap(); + // Alice's parameters + let a = TestParams::new(&mut rng); + // Bob's parameters + let b = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(1200)); + let mut builder = SenderTransactionProtocol::builder(1); + let fee = Fee::calculate(MicroTari(20), 1, 1); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a.offset.clone()) + .with_private_nonce(a.nonce.clone()) + .with_input(utxo.clone(), input) + // A little twist: Check the case where the change is less than the cost of another output + .with_amount(0, MicroTari(1200) - fee - MicroTari(10)); + let mut alice = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + assert!(alice.is_single_round_message_ready()); + let msg = alice.build_single_round_message().unwrap(); + // Send message down the wire....and wait for response + assert!(alice.is_collecting_single_signature()); + // Receiver gets message, deserializes it etc, and creates his response + let bob_info = SingleReceiverTransactionProtocol::create( + &msg, + b.nonce, + b.spend_key, + OutputFeatures::default(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + // Alice gets message back, deserializes it, etc + alice.add_single_recipient_info(bob_info.clone(), &PROVER).unwrap(); + // Transaction should be complete + assert!(alice.is_finalizing()); + match alice.finalize(KernelFeatures::empty(), &PROVER, &COMMITMENT_FACTORY) { + Ok(true) => (), + Ok(false) => panic!("{:?}", alice.failure_reason()), + Err(e) => panic!("{:?}", e), + }; + assert!(alice.is_finalized()); + let tx = alice.get_transaction().unwrap(); + assert_eq!(tx.offset, a.offset); + assert_eq!(tx.body.kernels[0].fee, fee + MicroTari(10)); // Check the twist above + assert_eq!(tx.body.inputs.len(), 1); + assert_eq!(tx.body.inputs[0], utxo); + assert_eq!(tx.body.outputs.len(), 1); + assert_eq!(tx.body.outputs[0], bob_info.output); + } + + #[test] + fn single_recipient_with_change() { + let mut rng = OsRng::new().unwrap(); + // Alice's parameters + let a = TestParams::new(&mut rng); + // Bob's parameters + let b = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(2500)); + let mut builder = SenderTransactionProtocol::builder(1); + let fee = Fee::calculate(MicroTari(20), 1, 2); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a.offset.clone()) + .with_private_nonce(a.nonce.clone()) + .with_change_secret(a.change_key.clone()) + .with_input(utxo.clone(), input) + .with_amount(0, MicroTari(500)); + let mut alice = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + assert!(alice.is_single_round_message_ready()); + let msg = alice.build_single_round_message().unwrap(); + println!( + "amount: {}, fee: {}, Public Excess: {}, Nonce: {}", + msg.amount, + msg.metadata.fee, + msg.public_excess.to_hex(), + msg.public_nonce.to_hex() + ); + // Send message down the wire....and wait for response + assert!(alice.is_collecting_single_signature()); + // Receiver gets message, deserializes it etc, and creates his response + let bob_info = SingleReceiverTransactionProtocol::create( + &msg, + b.nonce, + b.spend_key, + OutputFeatures::default(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + println!( + "Bob's key: {}, Nonce: {}, Signature: {}, Commitment: {}", + bob_info.public_spend_key.to_hex(), + bob_info.partial_signature.get_public_nonce().to_hex(), + bob_info.partial_signature.get_signature().to_hex(), + bob_info.output.commitment.as_public_key().to_hex() + ); + // Alice gets message back, deserializes it, etc + alice.add_single_recipient_info(bob_info.clone(), &PROVER).unwrap(); + // Transaction should be complete + assert!(alice.is_finalizing()); + match alice.finalize(KernelFeatures::empty(), &PROVER, &COMMITMENT_FACTORY) { + Ok(true) => (), + Ok(false) => panic!("{:?}", alice.failure_reason()), + Err(e) => panic!("{:?}", e), + }; + + assert!(alice.is_finalized()); + let tx = alice.get_transaction().unwrap(); + assert_eq!(tx.offset, a.offset); + assert_eq!(tx.body.kernels[0].fee, fee); + assert_eq!(tx.body.inputs.len(), 1); + assert_eq!(tx.body.inputs[0], utxo); + assert_eq!(tx.body.outputs.len(), 2); + assert!(tx + .clone() + .validate_internal_consistency(&PROVER, &COMMITMENT_FACTORY) + .is_ok()); + } + + #[test] + fn single_recipient_range_proof_fail() { + let mut rng = OsRng::new().unwrap(); + // Alice's parameters + let a = TestParams::new(&mut rng); + // Bob's parameters + let b = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, (2u64.pow(32) + 2001).into()); + let mut builder = SenderTransactionProtocol::builder(1); + + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a.offset.clone()) + .with_private_nonce(a.nonce.clone()) + .with_change_secret(a.change_key.clone()) + .with_input(utxo.clone(), input) + .with_amount(0, (2u64.pow(32) + 1).into()); + let mut alice = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + assert!(alice.is_single_round_message_ready()); + let msg = alice.build_single_round_message().unwrap(); + // Send message down the wire....and wait for response + assert!(alice.is_collecting_single_signature()); + // Receiver gets message, deserializes it etc, and creates his response + let bob_info = SingleReceiverTransactionProtocol::create( + &msg, + b.nonce, + b.spend_key, + OutputFeatures::default(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + // Alice gets message back, deserializes it, etc + match alice.add_single_recipient_info(bob_info.clone(), &PROVER) { + Ok(_) => panic!("Range proof should have failed to verify"), + Err(e) => assert_eq!( + e, + TransactionProtocolError::ValidationError("Recipient output range proof failed to verify".into()) + ), + } + } +} diff --git a/base_layer/core/src/transaction_protocol/single_receiver.rs b/base_layer/core/src/transaction_protocol/single_receiver.rs new file mode 100644 index 0000000000..f8866cfe1d --- /dev/null +++ b/base_layer/core/src/transaction_protocol/single_receiver.rs @@ -0,0 +1,178 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + transaction::{OutputFeatures, TransactionOutput}, + transaction_protocol::{ + build_challenge, + recipient::RecipientSignedMessage as RD, + sender::SingleRoundSenderData as SD, + TransactionProtocolError as TPE, + }, + types::{CommitmentFactory, PrivateKey as SK, PublicKey, RangeProof, RangeProofService, Signature}, +}; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::PublicKey as PK, + range_proof::{RangeProofError, RangeProofService as RPS}, +}; +use tari_utilities::byte_array::ByteArray; + +/// SingleReceiverTransactionProtocol represents the actions taken by the single receiver in the one-round Tari +/// transaction protocol. The procedure is straightforward. Upon receiving the sender's information, the receiver: +/// * Checks the input for validity +/// * Constructs his output, range proof and signature +/// * Constructs the reply +/// If any step fails, an error is returned. +pub struct SingleReceiverTransactionProtocol {} + +impl SingleReceiverTransactionProtocol { + pub fn create( + sender_info: &SD, + nonce: SK, + spending_key: SK, + features: OutputFeatures, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result + { + SingleReceiverTransactionProtocol::validate_sender_data(sender_info)?; + let output = + SingleReceiverTransactionProtocol::build_output(sender_info, &spending_key, features, prover, factory)?; + let public_nonce = PublicKey::from_secret_key(&nonce); + let public_spending_key = PublicKey::from_secret_key(&spending_key); + let e = build_challenge(&(&sender_info.public_nonce + &public_nonce), &sender_info.metadata); + let signature = Signature::sign(spending_key, nonce, &e).map_err(TPE::SigningError)?; + let data = RD { + tx_id: sender_info.tx_id, + output, + public_spend_key: public_spending_key, + partial_signature: signature, + }; + Ok(data) + } + + /// Validates the sender info + fn validate_sender_data(sender_info: &SD) -> Result<(), TPE> { + if sender_info.amount == 0.into() { + return Err(TPE::ValidationError("Cannot send zero microTari".into())); + } + Ok(()) + } + + fn build_output( + sender_info: &SD, + spending_key: &SK, + features: OutputFeatures, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result + { + let commitment = factory.commit_value(&spending_key, sender_info.amount.into()); + let proof = prover.construct_proof(&spending_key, sender_info.amount.into())?; + Ok(TransactionOutput::new( + features, + commitment, + RangeProof::from_bytes(&proof) + .map_err(|_| TPE::RangeProofError(RangeProofError::ProofConstructionError))?, + )) + } +} + +#[cfg(test)] +mod test { + use crate::{ + tari_amount::*, + transaction::OutputFeatures, + transaction_protocol::{ + build_challenge, + sender::SingleRoundSenderData, + single_receiver::SingleReceiverTransactionProtocol, + TransactionMetadata, + TransactionProtocolError, + }, + types::{PrivateKey, PublicKey, COMMITMENT_FACTORY, PROVER}, + }; + use rand::OsRng; + use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::{PublicKey as PK, SecretKey as SK}, + }; + + fn generate_output_parms() -> (PrivateKey, PrivateKey, OutputFeatures) { + let mut rng = OsRng::new().unwrap(); + let r = PrivateKey::random(&mut rng); + let k = PrivateKey::random(&mut rng); + let of = OutputFeatures::default(); + (r, k, of) + } + + #[test] + fn zero_amount_fails() { + let info = SingleRoundSenderData::default(); + let (r, k, of) = generate_output_parms(); + match SingleReceiverTransactionProtocol::create(&info, r, k, of, &PROVER, &COMMITMENT_FACTORY) { + Ok(_) => panic!("Zero amounts should fail"), + Err(TransactionProtocolError::ValidationError(s)) => assert_eq!(s, "Cannot send zero microTari"), + Err(_) => panic!("Protocol fails for the wrong reason"), + }; + } + + #[test] + fn valid_request() { + let mut rng = OsRng::new().unwrap(); + let (_xs, pub_xs) = PublicKey::random_keypair(&mut rng); + let (_rs, pub_rs) = PublicKey::random_keypair(&mut rng); + let (r, k, of) = generate_output_parms(); + let pubkey = PublicKey::from_secret_key(&k); + let pubnonce = PublicKey::from_secret_key(&r); + let m = TransactionMetadata { + fee: MicroTari(100), + lock_height: 0, + }; + let info = SingleRoundSenderData { + tx_id: 500, + amount: MicroTari(1500), + public_excess: pub_xs.clone(), + public_nonce: pub_rs.clone(), + metadata: m.clone(), + }; + let prot = + SingleReceiverTransactionProtocol::create(&info, r, k.clone(), of, &PROVER, &COMMITMENT_FACTORY).unwrap(); + assert_eq!(prot.tx_id, 500, "tx_id is incorrect"); + // Check the signature + assert_eq!(prot.public_spend_key, pubkey, "Public key is incorrect"); + let e = build_challenge(&(&pub_rs + &pubnonce), &m); + assert!( + prot.partial_signature.verify_challenge(&pubkey, &e), + "Partial signature is incorrect" + ); + let out = &prot.output; + // Check the output that was constructed + assert!( + COMMITMENT_FACTORY.open_value(&k, info.amount.into(), &out.commitment), + "Output commitment is invalid" + ); + assert!(out.verify_range_proof(&PROVER).unwrap(), "Range proof is invalid"); + assert!(out.features.flags.is_empty(), "Output features flags have changed"); + } +} diff --git a/base_layer/core/src/transaction_protocol/test_common.rs b/base_layer/core/src/transaction_protocol/test_common.rs new file mode 100644 index 0000000000..27c30dc258 --- /dev/null +++ b/base_layer/core/src/transaction_protocol/test_common.rs @@ -0,0 +1,63 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Used in tests only + +use crate::{ + tari_amount::*, + transaction::{OutputFeatures, TransactionInput, UnblindedOutput}, + types::{PrivateKey, PublicKey, COMMITMENT_FACTORY}, +}; +use rand::{CryptoRng, Rng}; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::{PublicKey as PK, SecretKey}, +}; + +pub struct TestParams { + pub spend_key: PrivateKey, + pub change_key: PrivateKey, + pub offset: PrivateKey, + pub nonce: PrivateKey, + pub public_nonce: PublicKey, +} + +impl TestParams { + pub fn new(rng: &mut R) -> TestParams { + let r = PrivateKey::random(rng); + TestParams { + spend_key: PrivateKey::random(rng), + change_key: PrivateKey::random(rng), + offset: PrivateKey::random(rng), + public_nonce: PublicKey::from_secret_key(&r), + nonce: r, + } + } +} + +pub fn make_input(rng: &mut R, val: MicroTari) -> (TransactionInput, UnblindedOutput) { + let key = PrivateKey::random(rng); + let v = PrivateKey::from(val); + let commitment = COMMITMENT_FACTORY.commit(&key, &v); + let input = TransactionInput::new(OutputFeatures::default(), commitment); + (input, UnblindedOutput::new(val, key, None)) +} diff --git a/base_layer/core/src/transaction_protocol/transaction_initializer.rs b/base_layer/core/src/transaction_protocol/transaction_initializer.rs new file mode 100644 index 0000000000..56ed48e76c --- /dev/null +++ b/base_layer/core/src/transaction_protocol/transaction_initializer.rs @@ -0,0 +1,596 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + fee::Fee, + tari_amount::*, + transaction::{ + OutputFeatures, + TransactionInput, + TransactionOutput, + UnblindedOutput, + MAX_TRANSACTION_INPUTS, + MINIMUM_TRANSACTION_FEE, + }, + transaction_protocol::{ + recipient::RecipientInfo, + sender::{calculate_tx_id, RawTransactionInfo, SenderState, SenderTransactionProtocol}, + TransactionMetadata, + }, + types::{BlindingFactor, CommitmentFactory, PrivateKey, PublicKey, RangeProofService}, +}; +use digest::Digest; +use std::{ + collections::HashMap, + error::Error as ErrorTrait, + fmt::{Debug, Error, Formatter}, +}; +use tari_crypto::keys::PublicKey as PublicKeyTrait; +use tari_utilities::fixed_set::FixedSet; + +/// The SenderTransactionInitializer is a Builder that helps set up the initial state for the Sender party of a new +/// transaction Typically you don't instantiate this object directly. Rather use +/// ```ignore +/// # use tari_core::SenderTransactionProtocol; +/// SenderTransactionProtocol::new(1); +/// ``` +/// which returns an instance of this builder. Once all the sender's information has been added via the builder +/// methods, you can call `build()` which will return a +#[derive(Debug)] +pub struct SenderTransactionInitializer { + num_recipients: usize, + amounts: FixedSet, + lock_height: Option, + fee_per_gram: Option, + inputs: Vec, + unblinded_inputs: Vec, + outputs: Vec, + change_secret: Option, + offset: Option, + excess_blinding_factor: BlindingFactor, + private_nonce: Option, +} + +pub struct BuildError { + pub builder: SenderTransactionInitializer, + pub message: String, +} + +impl Debug for BuildError { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + f.write_str(&self.message) + } +} + +impl SenderTransactionInitializer { + pub fn new(num_recipients: usize) -> Self { + Self { + num_recipients, + amounts: FixedSet::new(num_recipients), + lock_height: None, + fee_per_gram: None, + inputs: Vec::new(), + unblinded_inputs: Vec::new(), + outputs: Vec::new(), + change_secret: None, + offset: None, + private_nonce: None, + excess_blinding_factor: BlindingFactor::default(), + } + } + + /// Set the fee per weight for the transaction. See (Fee::calculate)[Struct.Fee.html#calculate] for how the + /// absolute fee is calculated from the fee-per-gram value. + pub fn with_fee_per_gram(&mut self, fee_per_gram: MicroTari) -> &mut Self { + self.fee_per_gram = Some(fee_per_gram); + self + } + + /// Set the amount to pay to the ith recipient. This method will silently fail if `receiver_index` >= num_receivers. + pub fn with_amount(&mut self, receiver_index: usize, amount: MicroTari) -> &mut Self { + self.amounts.set_item(receiver_index, amount); + self + } + + /// Sets the minimum block height that this transaction will be mined. + pub fn with_lock_height(&mut self, lock_height: u64) -> &mut Self { + self.lock_height = Some(lock_height); + self + } + + /// Manually sets the offset value. If this is not called, a random offset will be used when `build()` is called. + pub fn with_offset(&mut self, offset: BlindingFactor) -> &mut Self { + self.offset = Some(offset); + self + } + + /// Adds an input to the transaction. The sender must provide the blinding factor that was used when the input + /// was first set as an output. We don't check that the input and commitments match at this point. + pub fn with_input(&mut self, utxo: TransactionInput, input: UnblindedOutput) -> &mut Self { + self.inputs.push(utxo); + self.excess_blinding_factor = &self.excess_blinding_factor - &input.spending_key; + self.unblinded_inputs.push(input); + self + } + + /// Adds an output to the transaction. This can be called multiple times + pub fn with_output(&mut self, output: UnblindedOutput) -> &mut Self { + self.excess_blinding_factor = &self.excess_blinding_factor + &output.spending_key; + self.outputs.push(output); + self + } + + /// Provide a blinding factor for the change output. The amount of change will automatically be calculated when + /// the transaction is built. + pub fn with_change_secret(&mut self, blinding_factor: BlindingFactor) -> &mut Self { + self.change_secret = Some(blinding_factor); + self + } + + /// Provide the private nonce that will be used for the sender's partial signature for the transaction. + pub fn with_private_nonce(&mut self, nonce: PrivateKey) -> &mut Self { + self.private_nonce = Some(nonce); + self + } + + /// Tries to make a change output with the given transaction parameters and add it to the set of outputs. The total + /// fee, including the additional change output (if any) is returned + fn add_change_if_required(&mut self) -> Result { + // The number of outputs excluding a possible residual change output + let num_outputs = self.outputs.len() + self.num_recipients; + let num_inputs = self.inputs.len(); + let total_being_spent = self.unblinded_inputs.iter().map(|i| i.value).sum::(); + let total_to_self = self.outputs.iter().map(|o| o.value).sum::(); + + let total_amount = self.amounts.sum().ok_or("Not all amounts have been provided")?; + let fee_per_gram = self.fee_per_gram.ok_or("Fee per gram was not provided")?; + let fee_without_change = Fee::calculate(fee_per_gram, num_inputs, num_outputs); + let fee_with_change = Fee::calculate(fee_per_gram, num_inputs, num_outputs + 1); + let extra_fee = fee_with_change - fee_without_change; + // Subtract with a check on going negative + let change_amount = total_being_spent.checked_sub(total_to_self + total_amount + fee_without_change); + match change_amount { + None => Err("You are spending more than you're providing".into()), + Some(MicroTari(0)) => Ok(fee_without_change), + Some(v) => { + let change_amount = v.checked_sub(extra_fee); + match change_amount { + // You can't win. Just add the change to the fee (which is less than the cost of adding another + // output and go without a change output + None => Ok(fee_without_change + v), + Some(MicroTari(0)) => Ok(fee_without_change + v), + Some(v) => { + let change_key = self + .change_secret + .as_ref() + .ok_or("Change spending key was not provided")?; + let change_key = change_key.clone(); + self.with_output(UnblindedOutput::new(v, change_key, None)); + Ok(fee_with_change) + }, + } + }, + } + } + + fn check_value(name: &str, val: &Option, vec: &mut Vec) { + if val.is_none() { + vec.push(name.to_string()); + } + } + + fn build_err(self, msg: &str) -> Result { + Err(BuildError { + builder: self, + message: msg.to_string(), + }) + } + + /// Construct a `SenderTransactionProtocol` instance in and appropriate state. The data stored + /// in the struct is _moved_ into the new struct. If any data is missing, the `self` instance is returned in the + /// error (so that you can continue building) along with a string listing the missing fields. + /// If all the input data is present, but one or more fields are invalid, the function will return a + /// `SenderTransactionProtocol` instance in the Failed state. + pub fn build( + mut self, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result + { + // Compile a list of all data that is missing + let mut message = Vec::new(); + Self::check_value("Missing Lock Height", &self.lock_height, &mut message); + Self::check_value("Missing Fee per gram", &self.fee_per_gram, &mut message); + Self::check_value("Missing Offset", &self.offset, &mut message); + Self::check_value("Missing Private nonce", &self.private_nonce, &mut message); + if !self.amounts.is_full() { + message.push(format!("Missing all {} amounts", self.amounts.size())); + } + if self.inputs.is_empty() { + message.push("Missing Input".to_string()); + } + // Prevent overflow attacks by imposing sane limits on some key parameters + if self.inputs.len() > MAX_TRANSACTION_INPUTS { + message.push("Too many inputs".into()); + } + if !message.is_empty() { + return self.build_err(&message.join(",")); + } + // Everything is here. Let's send some Tari! + // Calculate the fee based on whether we need to add a residual change output or not + let total_fee = match self.add_change_if_required() { + Ok(fee) => fee, + Err(e) => return self.build_err(&e), + }; + // Some checks on the fee + if total_fee < MINIMUM_TRANSACTION_FEE { + return self.build_err("Fee is less than the minimum"); + } + + let outputs = match self + .outputs + .iter() + .map(|o| o.as_transaction_output(prover, factory, OutputFeatures::default())) + .collect::, _>>() + { + Ok(o) => o, + Err(e) => { + return self.build_err(e.description()); + }, + }; + + let nonce = self.private_nonce.unwrap(); + let public_nonce = PublicKey::from_secret_key(&nonce); + let offset = self.offset.unwrap(); + let excess_blinding_factor = self.excess_blinding_factor; + let offset_blinding_factor = &excess_blinding_factor - &offset; + let excess = PublicKey::from_secret_key(&offset_blinding_factor); + let amount_to_self = self.outputs.iter().fold(MicroTari::from(0), |sum, o| sum + o.value); + + let recipient_info = match self.num_recipients { + 0 => RecipientInfo::None, + 1 => RecipientInfo::Single(None), + _ => RecipientInfo::Multiple(HashMap::new()), + }; + let mut ids = Vec::with_capacity(self.num_recipients); + for i in 0..self.num_recipients { + ids.push(calculate_tx_id::(&public_nonce, i)); + } + let sender_info = RawTransactionInfo { + num_recipients: self.num_recipients, + amount_to_self, + ids, + amounts: self.amounts.into_vec(), + metadata: TransactionMetadata { + fee: total_fee, + lock_height: self.lock_height.unwrap(), + }, + inputs: self.inputs, + outputs, + offset, + offset_blinding_factor, + public_excess: excess, + private_nonce: nonce, + public_nonce: public_nonce.clone(), + public_nonce_sum: public_nonce, + recipient_info, + signatures: Vec::new(), + }; + let state = SenderState::Initializing(Box::new(sender_info)); + let state = state + .initialize() + .expect("It should be possible to call initialize from Initializing state"); + Ok(SenderTransactionProtocol { state }) + } +} + +//---------------------------------------- Tests ----------------------------------------------------// + +#[cfg(test)] +mod test { + use crate::{ + fee::{Fee, BASE_COST, WEIGHT_PER_INPUT, WEIGHT_PER_OUTPUT}, + tari_amount::*, + transaction::{UnblindedOutput, MAX_TRANSACTION_INPUTS}, + transaction_protocol::{ + sender::SenderState, + test_common::{make_input, TestParams}, + transaction_initializer::SenderTransactionInitializer, + TransactionProtocolError, + }, + types::{COMMITMENT_FACTORY, PROVER}, + }; + use rand::OsRng; + use tari_crypto::common::Blake256; + + /// One input, 2 outputs + #[test] + fn no_receivers() { + // Create some inputs + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + // Start the builder + let builder = SenderTransactionInitializer::new(0); + let err = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap_err(); + // We should have a bunch of fields missing still, but we can recover and continue + assert_eq!( + err.message, + "Missing Lock Height,Missing Fee per gram,Missing Offset,Missing Private nonce,Missing Input" + ); + let mut builder = err.builder; + builder + .with_lock_height(100) + .with_offset(p.offset) + .with_private_nonce(p.nonce); + builder.with_output(UnblindedOutput::new(MicroTari(100), p.spend_key, None)); + let (utxo, input) = make_input(&mut rng, MicroTari(500)); + builder.with_input(utxo, input); + builder.with_fee_per_gram(MicroTari(20)); + let expected_fee = Fee::calculate(MicroTari(20), 1, 2); + // We needed a change input, so this should fail + let err = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap_err(); + assert_eq!(err.message, "Change spending key was not provided"); + // Ok, give them a change output + let mut builder = err.builder; + builder.with_change_secret(p.change_key.clone()); + let result = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + // Peek inside and check the results + if let SenderState::Finalizing(info) = result.state { + assert_eq!(info.num_recipients, 0, "Number of receivers"); + assert_eq!(info.signatures.len(), 0, "Number of signatures"); + assert_eq!(info.ids.len(), 0, "Number of tx_ids"); + assert_eq!(info.amounts.len(), 0, "Number of external payment amounts"); + assert_eq!(info.metadata.lock_height, 100, "Lock height"); + assert_eq!(info.metadata.fee, expected_fee, "Fee"); + assert_eq!(info.outputs.len(), 2, "There should be 2 outputs"); + assert_eq!(info.inputs.len(), 1, "There should be 1 input"); + } else { + panic!("There were no recipients, so we should be finalizing"); + } + } + + /// One output, one input + #[test] + fn no_change_or_receivers() { + // Create some inputs + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(500)); + let expected_fee = Fee::calculate(MicroTari(20), 1, 1); + let output = UnblindedOutput::new(MicroTari(500) - expected_fee, p.spend_key, None); + // Start the builder + let mut builder = SenderTransactionInitializer::new(0); + builder + .with_lock_height(0) + .with_offset(p.offset) + .with_private_nonce(p.nonce) + .with_output(output) + .with_input(utxo, input) + .with_fee_per_gram(MicroTari(20)); + let result = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + // Peek inside and check the results + if let SenderState::Finalizing(info) = result.state { + assert_eq!(info.num_recipients, 0, "Number of receivers"); + assert_eq!(info.signatures.len(), 0, "Number of signatures"); + assert_eq!(info.ids.len(), 0, "Number of tx_ids"); + assert_eq!(info.amounts.len(), 0, "Number of external payment amounts"); + assert_eq!(info.metadata.lock_height, 0, "Lock height"); + assert_eq!(info.metadata.fee, expected_fee, "Fee"); + assert_eq!(info.outputs.len(), 1, "There should be 1 output"); + assert_eq!(info.inputs.len(), 1, "There should be 1 input"); + } else { + panic!("There were no recipients, so we should be finalizing"); + } + } + + /// Hit the edge case where our change isn't enough to cover the cost of an extra output + #[test] + fn change_edge_case() { + // Create some inputs + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(500)); + let expected_fee = MicroTari::from(BASE_COST + (WEIGHT_PER_INPUT + 1 * WEIGHT_PER_OUTPUT) * 20); // 101, output = 80 + // Pay out so that I should get change, but not enough to pay for the output + let output = UnblindedOutput::new(MicroTari(500) - expected_fee - MicroTari(50), p.spend_key, None); + // Start the builder + let mut builder = SenderTransactionInitializer::new(0); + builder + .with_lock_height(0) + .with_offset(p.offset) + .with_private_nonce(p.nonce) + .with_output(output) + .with_input(utxo, input) + .with_fee_per_gram(MicroTari(20)); + let result = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + // Peek inside and check the results + if let SenderState::Finalizing(info) = result.state { + assert_eq!(info.num_recipients, 0, "Number of receivers"); + assert_eq!(info.signatures.len(), 0, "Number of signatures"); + assert_eq!(info.ids.len(), 0, "Number of tx_ids"); + assert_eq!(info.amounts.len(), 0, "Number of external payment amounts"); + assert_eq!(info.metadata.lock_height, 0, "Lock height"); + assert_eq!(info.metadata.fee, expected_fee + MicroTari(50), "Fee"); + assert_eq!(info.outputs.len(), 1, "There should be 1 output"); + assert_eq!(info.inputs.len(), 1, "There should be 1 input"); + } else { + panic!("There were no recipients, so we should be finalizing"); + } + } + + #[test] + fn too_many_inputs() { + // Create some inputs + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let output = UnblindedOutput::new(MicroTari(500), p.spend_key, None); + // Start the builder + let mut builder = SenderTransactionInitializer::new(0); + builder + .with_lock_height(0) + .with_offset(p.offset) + .with_private_nonce(p.nonce) + .with_output(output) + .with_fee_per_gram(MicroTari(2)); + for _ in 0..MAX_TRANSACTION_INPUTS + 1 { + let (utxo, input) = make_input(&mut rng, MicroTari(50)); + builder.with_input(utxo, input); + } + let err = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap_err(); + assert_eq!(err.message, "Too many inputs"); + } + + #[test] + fn fee_too_low() { + // Create some inputs + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(500)); + let output = UnblindedOutput::new(MicroTari(400), p.spend_key, None); + // Start the builder + let mut builder = SenderTransactionInitializer::new(0); + builder + .with_lock_height(0) + .with_offset(p.offset) + .with_private_nonce(p.nonce) + .with_input(utxo, input) + .with_output(output) + .with_change_secret(p.change_key) + .with_fee_per_gram(MicroTari(1)); + let err = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap_err(); + assert_eq!(err.message, "Fee is less than the minimum"); + } + + #[test] + fn not_enough_funds() { + // Create some inputs + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(400)); + let output = UnblindedOutput::new(MicroTari(400), p.spend_key, None); + // Start the builder + let mut builder = SenderTransactionInitializer::new(0); + builder + .with_lock_height(0) + .with_offset(p.offset) + .with_private_nonce(p.nonce) + .with_input(utxo, input) + .with_output(output) + .with_change_secret(p.change_key) + .with_fee_per_gram(MicroTari(1)); + let err = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap_err(); + assert_eq!(err.message, "You are spending more than you're providing"); + } + + #[test] + fn multi_recipients() { + // Create some inputs + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(1000)); + let output = UnblindedOutput::new(MicroTari(150), p.spend_key, None); + // Start the builder + let mut builder = SenderTransactionInitializer::new(2); + builder + .with_lock_height(0) + .with_offset(p.offset) + .with_amount(0, MicroTari(120)) + .with_amount(1, MicroTari(110)) + .with_private_nonce(p.nonce) + .with_input(utxo, input) + .with_output(output) + .with_change_secret(p.change_key) + .with_fee_per_gram(MicroTari(20)); + let result = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + // Peek inside and check the results + if let SenderState::Failed(TransactionProtocolError::UnsupportedError(s)) = result.state { + assert_eq!(s, "Multiple recipients are not supported yet") + } else { + panic!("We should not allow multiple recipients at this time"); + } + } + + #[test] + fn single_recipient() { + // Create some inputs + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let (utxo1, input1) = make_input(&mut rng, MicroTari(2000)); + let (utxo2, input2) = make_input(&mut rng, MicroTari(3000)); + let weight = MicroTari(30); + let expected_fee = Fee::calculate(weight, 2, 3); + let output = UnblindedOutput::new(MicroTari(1500) - expected_fee, p.spend_key, None); + // Start the builder + let mut builder = SenderTransactionInitializer::new(1); + builder + .with_lock_height(1234) + .with_offset(p.offset) + .with_private_nonce(p.nonce) + .with_output(output) + .with_input(utxo1, input1) + .with_input(utxo2, input2) + .with_amount(0, MicroTari(2500)) + .with_change_secret(p.change_key) + .with_fee_per_gram(weight); + let result = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + // Peek inside and check the results + if let SenderState::SingleRoundMessageReady(info) = result.state { + assert_eq!(info.num_recipients, 1, "Number of receivers"); + assert_eq!(info.signatures.len(), 0, "Number of signatures"); + assert_eq!(info.ids.len(), 1, "Number of tx_ids"); + assert_eq!(info.amounts.len(), 1, "Number of external payment amounts"); + assert_eq!(info.metadata.lock_height, 1234, "Lock height"); + assert_eq!(info.metadata.fee, expected_fee, "Fee"); + assert_eq!(info.outputs.len(), 2, "There should be 2 outputs"); + assert_eq!(info.inputs.len(), 2, "There should be 2 input"); + } else { + panic!("There was a recipient, we should be ready to send a message"); + } + } + + #[test] + fn fail_range_proof() { + // Create some inputs + let mut rng = OsRng::new().unwrap(); + let p = TestParams::new(&mut rng); + let (utxo1, input1) = make_input(&mut rng, (2u64.pow(32) + 10000u64).into()); + let weight = MicroTari(30); + let output = UnblindedOutput::new((1u64.pow(32) + 1u64).into(), p.spend_key, None); + // Start the builder + let mut builder = SenderTransactionInitializer::new(1); + builder + .with_lock_height(1234) + .with_offset(p.offset) + .with_private_nonce(p.nonce) + .with_output(output) + .with_input(utxo1, input1) + .with_amount(0, MicroTari(100)) + .with_change_secret(p.change_key) + .with_fee_per_gram(weight); + let result = builder.build::(&PROVER, &COMMITMENT_FACTORY); + + match result { + Ok(_) => panic!("Range proof should have failed to verify"), + Err(e) => assert_eq!(e.message, "Range proof could not be verified".to_string()), + } + } +} diff --git a/base_layer/core/src/types.rs b/base_layer/core/src/types.rs index d0a89a5e87..4b99c419a1 100644 --- a/base_layer/core/src/types.rs +++ b/base_layer/core/src/types.rs @@ -23,10 +23,12 @@ // Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, // Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. +use crate::{bullet_rangeproofs::BulletRangeProof, proof_of_work::BlakePow}; use tari_crypto::{ common::Blake256, ristretto::{ - pedersen::{PedersenBaseOnRistretto255, PedersenOnRistretto255}, + dalek_range_proof::DalekRangeProofService, + pedersen::{PedersenCommitment, PedersenCommitmentFactory}, RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey, @@ -38,14 +40,47 @@ use tari_crypto::{ pub type Signature = RistrettoSchnorr; /// Define the explicit Commitment implementation for the Tari base layer. -pub type Commitment = PedersenOnRistretto255; -pub type CommitmentFactory = PedersenBaseOnRistretto255; +pub type Commitment = PedersenCommitment; +pub type CommitmentFactory = PedersenCommitmentFactory; /// Define the explicit Secret key implementation for the Tari base layer. +pub type PrivateKey = RistrettoSecretKey; pub type BlindingFactor = RistrettoSecretKey; /// Define the explicit Public key implementation for the Tari base layer pub type PublicKey = RistrettoPublicKey; /// Define the hash function that will be used to produce a signature challenge -pub type SignatureHash = Blake256; +pub type SignatureHasher = Blake256; + +/// Specify the Hash function for general hashing +pub type HashDigest = Blake256; + +/// Define the data type that is used to store results of `HashDigest` +pub type HashOutput = Vec; + +/// Specify the digest type for signature challenges +pub type Challenge = Blake256; + +/// The type of output that `Challenge` produces +pub type MessageHash = Vec; + +/// Specify the range proof type +pub type RangeProofService = DalekRangeProofService; + +/// Specify the range proof +pub type RangeProof = BulletRangeProof; + +/// Select the Proof of work algorithm used +pub type TariProofOfWork = BlakePow; + +#[cfg(test)] +pub const MAX_RANGE_PROOF_RANGE: usize = 32; // 2^32 This is the only way to produce failing range proofs for the tests +#[cfg(not(test))] +pub const MAX_RANGE_PROOF_RANGE: usize = 64; // 2^64 + +lazy_static! { + pub static ref COMMITMENT_FACTORY: CommitmentFactory = CommitmentFactory::default(); + pub static ref PROVER: RangeProofService = + RangeProofService::new(MAX_RANGE_PROOF_RANGE, &COMMITMENT_FACTORY).unwrap(); +} diff --git a/base_layer/core/tests/chain/chain.json b/base_layer/core/tests/chain/chain.json new file mode 100644 index 0000000000..0322df1213 --- /dev/null +++ b/base_layer/core/tests/chain/chain.json @@ -0,0 +1,17497 @@ +{ + "blocks": [ + { + "header": { + "version": 0, + "height": 0, + "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "2000-01-01T01:01:01Z", + "output_mr": "0000000000000000000000000000000000000000000000000000000000000000", + "range_proof_mr": "0000000000000000000000000000000000000000000000000000000000000000", + "kernel_mr": "0000000000000000000000000000000000000000000000000000000000000000", + "total_kernel_offset": "0000000000000000000000000000000000000000000000000000000000000000", + "pow": { + "work": 0 + } + }, + "body": { + "sorted": true, + "inputs": [], + "outputs": [ + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 1 + }, + "commitment": "723ac7fbd0fc86d252334e018bdbdcfab51f60c095d2ecbba2abcc7bded63b32", + "proof": "d4e57c90a9f0d7c3bba2a786b6a4238553c35a1bc071972e6c33386a27146f204c9cfb8ebae8c80f9d84f22e7c37a4c811b6fe0aa4f8b0cc40cd36a2aba1254b0238fa7d1602442aacb5de316eea13c17bba8bf7cb337a28e3c9b782e5bf9218fc2efd00eed4b4edef9786c118b002f994465faf6e41414701d96de95a1549557dfb3f40b99cba30dfc61dfb4fb6920c2a9ec5bab2313ccf1fca73a8a563bb09ed29be18f9d392f83dd59c9763cc06a6a7b4e88a67b8ace47d984c72b78279094e6f5f5e89fe88754ff9668f87a4bc30c91d92e46a9316bef8201eb15f35770f5a5ac11be848646b42b17b9eb6e744b87573cd7778c9a8ee1999f898e18e786cb47cc1b0fa0b6cde3d22a7c41ff560de13598ad7712c248ce2f380cfcf03361114203857166bc3c8015e1af9a5971a34091a6a79221805a9175c1f9c06a36a5fc2746dab1b8682e8e7173eeaea3833bdc50f57883b493c30d1af5bfc8219145c24c12e887dd92c403270fc0ed40ef6ead313f2197f6b2da6b6c2e1abed0bfb1308adb8efdd20c9a4ffd784bb01a9d1010ca050532c0d955479aabfd9fa5afc054429d7835e658c1c600d28d1d790a8f29ca861ce3fe9464d6801496440427f346884b190b1ffa9d6cf037648f186044349791c842c01d48b2fc54ce55ece31161c39639e2d78a88827b07746a867a5b875859012924e2f695f8c331cfabdce123498fc114472a90d5a944a706988cf553f5139b7337ae5bc135b12730f78e15a6a8d93697e8a0688f55d21f4dd40e78d3702c0601aa6240fddb520ee9eda19098e2d18b9d47ab028a970085ed2cb63a18f6a44d9cb0edceec5d5ecd7b934bf139a4d27aa042b4a3383e5747c77b1ca658d8345559f41dd8ddd29be73dd34090a6e9f03cd43cf346bf6b9385f239859965cbcb7323c6ea00c70e9babbe933b800" + } + ], + "kernels": [ + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "a8aef7122e83a82a6b8ffd6729860ad594d278a5651f77be007af054fb2c687a", + "excess_sig": { + "public_nonce": "fab511490f09850da21561405e7160ed0fdde6bf04fffb587808387f02ece23a", + "signature": "0576f44c8011c3b6372bd3dbbf6f215987c193429084106f6f25485196ee8b07" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 1, + "prev_hash": "83458471965451ae7eae37109928771512a363cc01c539b7c0e6313c0de4656e", + "timestamp": "2000-01-01T01:02:01Z", + "output_mr": "b877b3f82b4db018e3304b39db70a1e794578c90a2fe3224d52f87d3cfe8894a", + "range_proof_mr": "e28c669a06bf169d76dc414fac67690495962c8c5ba879e863d085c2f9219aa4", + "kernel_mr": "ef58a77c3d2b2d605165e3cc6af2d5deae6ee04f695ebe878bde23a31ff4cd4e", + "total_kernel_offset": "dfa599695fe03954619aeaf241fdce02b7940da4d93298b3031f07b7c51cfc0c", + "pow": { + "work": 1 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 1 + }, + "commitment": "723ac7fbd0fc86d252334e018bdbdcfab51f60c095d2ecbba2abcc7bded63b32" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a24493361860e1e4f07e0594855cafad65374ee8d1732b4bb7f18817930ea615", + "proof": "c8d64fd97a11e469be6e963271ea8bfd09fec70952ec8413653c384f7b1b354a6ae5be4070c8911b0614708adec652373b1291a0a62fcfca9187ac36fcfb69249cbd4991a3dd8aa02f408e5c294406eedac4e917e83f47f2be18a7a7039f4245901edcadf48042b12bea6651c78dbfb22e089f26c83a8ee12f80bec55e19507172a5a917ba107fbfc7fb29dac4d824c721fb60b7e28d171ca57e15606dd25e0c0c6743d27f3cb64f6d3be4ec29d83e2d04da507c437673ffeac583c773829e0b5d3d51b91f1665e7ba71b0ce3c9f77b34e978c45aacc210499e12922f38d13052a96fd60641ecf133fd2d070dd3009e37656342d2ba65dbb6b9df1fe44e1ab261c0c44628bb4f9f1d51edbd52210d54537df750f20110c7fb4c11c7d79213201564f6b1f0337f8842a4dddb5ba0f7e8962a892827316e3e111d196a5328c371a681fc48743b4a7ead1e26ab0e2c7aa0592b515467f26d4117ea8b6c572f30405dcf13351c0db8bf29673f78ba518a9e4a46f90a7ae2660edeb2df82acbd9e9492802ca3a9c2a5e2e23306783111d247d939d78223a01d01c7abe81c5b29c7f609ee583a4e212c69390fa57f696f7e57b536f0a4cc7db71ca79228437ae424e24fcec70ca40b95e896ffdf9533822bf3c3e2609e3a2ad77aa96b7838541edce394c21c4288c447abd51e3187fe39aac3dac944c9cef38821a3741bfd1305d6322d645c735775d96c661c293281126ee03a40086ba3950860f2a36a689b519ae2c18c31139a5ae3e56c1063cc24b03c43fa4c46eecde8f274a79a02e16b4b6da51c2dd89fdcc27f30613247324e2e89e1f0594e7266245124586c06d59012f0f35ee4f4b633719ca725bae3c95d74d4d751f775ade16fc46ea3b688fd45be2520ea4bee7c6d2e71328eabe5cfaa082eacad775bedfa6ab5014f9544918e7aa8f07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bec1f1a8e990ef62d1f3d4d6ec62a9767c850b116807147d7c51d131bb83b439", + "proof": "fc4d500a3b5eed2870160bd444824ffa903acb7a170faa4b51937f33e4afd153ba2ec2291e009d0a8e62a118597b7f63aa301084c2d316a20738765829c0970b5a029664802d9fb5791e0bbf270249161f2afec6dc4c816fc354bf7c9ebfe71d5ae24ac181322236e37314ef98c9294cc1bfe9cc3e78259656af9eaaa1cc043c16add88ec6d56c2957ad5d28f9994c41e507a4910fbab36a3f303d4e89a23f083c64d72fb27d238918e0a8de90716eeee12f45f2390f39f3bcc5ba3786f38f024ddfbf10b36229b046c8e70d671c7a9c366ebfca195aa4f4b87e7459fd362f004897985e3e9db0653ef4f560b6f6fa8637e1ea5464a28db2ba4744b3b780035420acbe451a1edd651d56a22bd2fdaeff5037a9b74155ddd07986edfedc97ca7382a9516d8dcc5615607d46bd39b1f8867004d614b0bb1cbfba77ec4ffa81254cd0f1c792a4446125662b6d9281284b36ccd28fbde09a01fac85496a053a9b95352797e9245f4fde122678be84313c098c64b554fbfe786ff173e04704892424b72039e5a2e8d1ee91e879f335fddda5e552311e1e0dccdc4854691594a23524cbc3ec43306bea9d4d88a99a6b4478fa175aff1bd182dfca1415574327cfc8830c2893ac9d320972f92056c78a0e43a5b163acba22e31d180ad11ad41c8f15059e41714cbf567735f399cb62b03fec97de115ad9654e2e72acf299b0aeb8c0464564e6dfe522f0e5aa77c9822a6ede43f8ca983ea55833672f77d1eceebe73e4214735b3a9fb6e86513f0f547f00fe6c247409438b5e8a9e934d11db36a55e054b67ee6bc2a216c3d63c329a30565c7ee60b45e59be9d20e16f1d112ed016416eb7114b0563748564ed7afdf9bdf2ec90d27d5795adf4938c214428a1e314be09ce09a4cf0a421fc3a1d1fb31b76d62fc2eac9483c4abbc4a6fea0b3896a92601" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 2 + }, + "commitment": "fe96e82c83a455693c4af5a30947d56f9acc505c14c3a4e3cf7b48cfaca1f81d", + "proof": "b668d52ae93f59f0bebd3388b80860638d15f8d191e8ab79d644f0bee6d5574a5046fd7b262e471d49fae0e86d50144dca2087922426e8ebe4bc9f600480003bc6f2020a297d53ea6b66ba177a17635e7e23b6d6817ea25da27e1addffc47c3caa6654b5e3f6dd069e799898fa3b5508bc81153a659176dcadd0921e7e2d3374664cbffeae4c876470126eba538671fc889b973bfc360f04db615a85fbcb4a06f299cee86fc19ad400fb26379fc98eb837dbb3f015e68f6046e0999419c1aa019f065cbe68d72a2fd27c16f9b19bd53cf8f5a8aff90674d063c6b914b0c4b70482fc83d7b5023cbbdb8b19bbe1efcad8ec67005df2c4cf6f5e64d687716b855a24980fef56078280774ead921bb7be03227ea70f058223b0e104817e0e32e82196b245e39ec26f98fd3708622e011ed3f1f5d34aa197ecf615a148014c92023186693432c4799c756eabcdc6e6eec678fb9af80aa10a79d6e8e52641c4a57433ccda01adc75cfcf27a428218de2bf5dedd6136f69bf0f8d0d00963d9302962438ede9ee6979527a186dddcd88ead0fd1243ba7b7d6bbfca0db8eaaf2cdad3f60323fbbbcb754b564d9c60fb950a7761e55955fd88f145675e694ce0b3a78f653aa14e63926f83d91d6d4774abbb2dfd086a530df7eb94ed2ef6728f51bb8247ae8ca41249163dfcd80103c1828a1688d4c7516e844550a42482fc3e3fae90124a65bee616797a5efeed3443dc556c045a5eb6b9472c388061b01304283062d69a06c4f87596a09e3f85e440da0d8f823b47d77120f3f698bdfc7331a5f960b37465171edec813d757748f03ab4133cf5b30be6bd0edef73c3236feba4a9c777c94598d031d23682adc0786fd2b44075af3ec40e4d73c3ec9a323bd0a3550f107fe4f9782748efd953934eacb8394fb86beaadaffe0f0db7d8ab518fd57d9d504" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "842749b4190a5ecf9a9ebe24a4dac81e85c1288f25aed5cdf77d289b12da5340", + "excess_sig": { + "public_nonce": "088344c69f41af851528394fa1cdef493f43a50807aabfc08b9015b8bd9f2f56", + "signature": "2411f6298890b4a0056252ff27787a080b5137a0d556efaacf83abd20920e500" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "52bbafbb6ef01004cf708cadedc20253fa4b4f163c6381734b316fb90aa2bf72", + "excess_sig": { + "public_nonce": "54375fcd1234ff21b1292a40918957d33756542b4a45787570747a5141726849", + "signature": "c04373d3bc9c623bce422faea880a93aa9d781227473dd144447b61b1df6220c" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 2, + "prev_hash": "8ebfefc40560cc1fe6c282c1458ec3482efd19c448fab8519d28736ff4f26496", + "timestamp": "2000-01-01T01:03:01Z", + "output_mr": "35cefa2f5dd074f8cabdb1953b39fbaa8c6c5014f0ae3503835e0aed0d68c857", + "range_proof_mr": "df17a4112249cdb1f6ed3c21c6639e5bf5b1e02edb7f07210bf53a27192931d4", + "kernel_mr": "5010f4f3b3f7b14429ee1a3aef826ab82ceb80e145e3cd544ab805747d4ab909", + "total_kernel_offset": "23ecd2fb0c82a06e354d139ae340d05eda27fec662f2fd0917fe36c3c61a0602", + "pow": { + "work": 2 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a24493361860e1e4f07e0594855cafad65374ee8d1732b4bb7f18817930ea615" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bec1f1a8e990ef62d1f3d4d6ec62a9767c850b116807147d7c51d131bb83b439" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1852bc4169774957861763da779805844eee814d5f47f6a44470b7097bf4db50", + "proof": "e2c62f31c65ebf6f31c5b6eac2e45072d2daf3fe8fdd06cb745ad2434f4d770f8a07c0ffb803624319e843f235ec4b81a644097ad2d650123ec021b772235705f01629fe9e8cc53ea8fbb81385c1bf4e9c015f878e82587e20b4b92858b8725efc3b99cdf2b8c2c8b607360af5b43a668dae2f8eae378f235114c68f3e981d734d47077dce78bbcff4f3661b0dd84f28edc963785b0962852dd421a95c5f7b0f396b609f68be35fc75b6699f64e69122bae0fe9ac79f00760e69aa115d2d7d01f86414683b5a5e123d8be531b0b45c5ee7406b04cbb406cc570c7070eb0c9c0ef2fa78fe178f3124f7b8b1ac76322ccb11a188540c7e0b1c3d7e6e79bfce0a0bda2cd53a8b8f1655c37c59d7c9d98d9da2335ebe5d72b957d672a1b1429f0371703ede82f24e38fa9d273afa45e51d5bd8aee7dc0d3ce6b6fb8fd9771230560c3ad1bec9ceae1a0b0633df0629322b2e4bdb63da902ca952d37cf594f8199f403023fd73cebc5d95b93648a15d31e33e3aae40f5492d08d277c1d115e6687d099e0a1e7ae886047bb9c3f841d5d4ee35de8ca74b36b9e5f07c89dd24bf82560900600a0b35421c5e2bd35b05fabe09ab29da2a65f7560d51c4e9ad1f5c2df7373a274c7a9bf0a0b45f05e60aa855a7ff97c638b8adc0948efdcc1296f4bb666b14834acc254f4c0fd956a7c8ded5dfc492584cfa9607f332322ad33486b7970b124b7a9868c13c8648f4ecfccfd2f7fe89193260f0af06c41ec60d9f6c88cd635ad574b6bf4075b5763b5f8ecbaa1a4de24a974537af2acb0fe39b33da410a0c4252c8dc28cbf9edbbedbfd1bd12f31f0d6dfdb0ef4602b504bf9d1e2fdd99086c21b272af99ce9b5e139e0997465fa78c6e4c29da913816c108c0e879e3d30049b60b4657a54a717f1961ba136adf499abd8951f40cf379265e7a9ae23bcb07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68bb408d0457f3fa265424b9bc415deae64aef235b055666aa9608dc21d40869", + "proof": "e2b38a9533c9578b788d0d57d09601d34021d26ac54960c912f6b1ddb4f38611281ccf3da59c551e6e766d904d7268c42d9118dc4f3d6f93377bda8020b5bf71ea422e30b6d923aba3e45716fee5bb1aad0bda6317606fe15c4f3563adb1754a86558eee8738ed2418ce889bf3592d54ccf5cbee0183f42c52c8cad45eeffb656eeeba3931759b563bbe1e0b6a8ad289a2a2109b2680723e9848c0f498882505d9a0430c612105f95cac87a26b24c92811cd8659b310d9224fea49ff912fae0865694d927aa0cf384bb020068fb531637816ce6e32080124ae910019553aa103d89775dd14775338314b1cdcbf40129f29c94bcbfb5a87e8e50b08757f5dd450bca4416281b3d46141c10e4397001b87318d2ea63230aa6e8062278cd865981aa4ff6110ed165288565e2ff6f29f0be37dd387be61a2ea79b95e5de81be3b40d10e3f7b581adc23e9fffd9c172bda2aa30b008a0eb4125562e75f065c60f102cb65ef396dbef125b298d48ab283cddf1287becb80db137f6381e35f7b10cc123ae8f976a8c5de2df7c6b27219829c9b31b4f157b755ebd8de3eea1f55ce37b338a29466294a2da9abdf9c6f162227cf6ee70ab147c43a75fefd21112105cde06cac5b0bd2746fa8bca62ed77844ce2bdee4fa3bcc55a30c72b01fb45205fe606b673bd7dd236747a27d4f8bc22886badc7a62c072e43105de31c2471fe4e0c0ad86a80c211f58aca053bbef8e5f92bd1c8504cd31d4735dff6b4dcf66c2ca562d04b7fe7f0e3afff383d0d73e524ae9f981ba0f9cff5e7b7e9d06d91445231703c15b2a8b1f1e266b2edeef1a023ffce26b0f95777f34a99d5685930fbba790ca4dcc0f9b5c8614ad8bbdbaeef426ebe5bdd79e632855a25ce89c05f9e891b01350ad58dcb6e800853f7e49440a972cfccd9dcf1ce8735cdc0a1d6a2fd843103" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "848d2a82e660c82b7895793d1da94daac70243adc49bad561bb394c76c063322", + "proof": "18894b869c1cd159e646eb1176cc8fc442ea27af10b98d3c7320d89915bf071e4ed9f1d39f2242e86e0ff21e250348ebbb14b5cdad7d1d45a19281a32778483f8e538ebe643ce1390f58a777f99f07085e0f8a6ac09e7964327802454d20b36e482b325acf07edbce6acd8f0fa33838abff8120c16f8a796ed4937242e6e283c1297d24e80315d1d69331d9853558632761f4d69c438d3c4811a5bb2e9a9840b6bcffa9ac93fd901d3b0be6059f6961a5b4d33300d721834468dfbee9df12d0f4ec4d1136f023a55972e69ed49344912056711ee88650e0348c6b6decd54470f1e83f3627adec75e7a3c3137298fbb51b6305caa1f55b47e393a65e6da63de6656c2d780b9ee6c0863be989a6a7cffe9a6197d1098848ee2e546ec5337a77c0ec670c0a4ba61c76926f780dc0a641228b90c3d4323b0a45a6e7ca78d87463e5c2eabb50ef807fbdbf16355a071dccfa82daed9b7fb8ffb2bfe1bf79eab48250e1ec655411bbc67d2629dd38448d41c22bb5d587d9bb6b9d035a5fb8bda549435c8001378d5c78233616d0874a25507aaeec2094679d4cc18d582126a21bcaa1a86ea0c7fbd9b0bbbfdf234b561f56be0fe91374ecf1de5589e3e9f4fc7df6e2ae04abed24dd8cde860b4e855aebf0f1512c087da95cb24e6e530ffb8fc675c77883e53d14297803def70b31734c56fbd691281f8899862203a95f4de54f0836f5cdd5e5c63fedcf5a325202121d0a0b1bb24e0f7f81764166a3734ebc77d0721e6491b2e19e1d70e02368b62df5f38a32105bea2ae9ae6f36f821683e13a9230b0b272be8cf088c369915bd2d67ebe8f218a6a85c455edc273f1f689a89b4d5185cadc636cc6bd3096390cfcdb5a8d30a02cbebe3ea7c7b7e8102d2453361305b697b2b735549865d2d28dfab2b1d37d7d873c149877060740dd351fabcfa806" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8e1d2f1cd95031dc33b3b9e53faf0d54dd548250c5e48a8ae2c0ff9a113f0a4e", + "proof": "ca67a8f2bf1871f6f4816aedceae63ed7912aa42323ec39f790fc4b0fc5b9d29b8696ebe37b1f397823dc4fafd819b098a1f0694bdb489e6b72231abe1f73642665b111f115f6a752f2ba0d8f87173f516c93c1cd449ec9b734884228580ea3f860fb5b5a500a2df85134c0b649531f8822893498841d2947f853f754b324e7640a4dae71178e8b4b61062da233f9b4eeacc33f73bc909cc2fb34273ba736002b907e16d5c2b313a8fc79ca2162bba4eee0e890fe4aaa1506749be346ad457088d79d3faa1c6e7a3f4923d1351e5a9f8af3b80342da90b3e4fd0f85bfaef430b32e65caceebd64f4f0a26fde345191aba308b2bc72d0978c1db405a64d2401397ee7bdf1f75fda09d631ab430df30612b202d571c8a15b1d59aaf326ade3c164284470d0a61acd841709e4368d599bb34de9666cd817650981aba89b8236d65f7ecb911e7bb152b5bc250466fdee76d23070576290a12d6f8df7e1ac89a6ba4548a3b7b94e4d5cec68060e464a5c95533ff46da147e32c15d2adef4d8a98d62410793fa8de683067cbe1d14773ee896f6140f0ebff84f5e24ecf65279438e30a7457713fa3e3d748f68318e53f07441812d71453e7468f68e0e8c68721f546686ab193449c9f9f10dfdc64b1b3dd6189b3e4e8f077061685ddfb6657760e12552219b55643c3c62174d9eeb5ec82e8508cee8b1fb472dce09f3f1dd8e51eff518e04c212abc678888ea8ba4825e671bde1f1fc70d7ffd8948b28d94bc4ba34201cc5e8cd442eb43f9599f9db06b0ba242a3dcc2d7c6f779679a79b365603f91e201027a5fd4e9f5acce220eff31e39e943845dd70f36a6f93730fcfc6c8a217b6bbe7b4486a65f91973b2fdb552485e7c944eb31268c19376710d4b7f6f31906fea371d171c543ec6b46a9ea27f6c6e2ddec5eb4c6ac88e0530e4346d7b60803" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 3 + }, + "commitment": "1c74853689e34efe8ac094362c70ced414bf0ff072eaafb9cc8985aeba5e2809", + "proof": "0a5fff810276295981435e941957636b2481ff977df351344b0c5f6e024b123136480b74f636d7639bd035d17480e8fd74ed5a3e93478d2037679c6f7911c41fd42747e98a03be622e429b8bee865b63ff8f6aab5ad2353dd33cdb006d193170d6de20795d1d69cb989ea34c496386f3f245dd029b05d7676ab099fff30678372dadc1165b7d23118c900027acd643387f673b9974f3f1f8ce436cd8982dfc03542fdbf5bdb38257a0e4a0d19648e784bdd47f8e4e22a6b22cc0acc7b00f200ec5a44c14e6f5698dd66e01e75b50ae88fc9f9357ed5af7a18387848f328ca703c8e48f3915fc098a0eb466c1caba70b055d440883b10dbb3bf42c5dd7caaac46762bb38b67ed919fc03920de97beaa653f4cb7ff667a5c318055371c74bc27475cbd8c9939ee45e9f9db6b2f8819d00a9d644e417b64c3a41810b3dfa075d50820916f6b2acd2c8c23171dcc6b23ebba243c0d889dd19cbd2e7e20bd64e7e87a30620f46227d0df518d89d712d53c3996cd211c89e43abf0280359f3db7efb27fc5180d4b8db640ea8faf1287eabfe3f554f177c87616d6d700a75c47bf9215758ab1bd04c2e722585abec408992d7feffb0ae4a0be5c13432b74b53f06cf27792daec720230b4adf942e580a3f11b0182dea83081c2c67d9dd7399fe612613e1209a374e438741f010c19c85dd34ae36bbfccde4e1e9e755b8c8328f896a721ded79c2c55e96dc5bf68b73e6bdd17b82831e3d9c0674eba65f8fd0b5e43c97c64399ca46a69f7e5cc3d719ea5433847acdf0e03cfa224ce522456e60a0fc92dda15ebc8e88a27e41aa1927099add5d1a7cf0c5527d4d931404d2171b31fd019914f02558d9921e080f890aeacc24ee9fb11f48a6124f3beaad5724621b83d03e9592df104d4cacf4f837926f0bd404e31afb158a51e290938d481fba50c2704" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "5640acebb0d5891f97453a3df0ade330a4f824d34cb48627797760c9bcd5c41d", + "excess_sig": { + "public_nonce": "eaeb0b0919e335051525ed3e6d13aab4a4a0b3e3a5e5e2d510f68c239575fb0e", + "signature": "e3e31714c0f924e906ad26c8fcc576df5c82105b2eb1dd496a38ead2e2922d08" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ec0f4fff46817d280a24cc8ba554801452c5a887b90a1c5ae20c27f2a6ef1c4a", + "excess_sig": { + "public_nonce": "309e80eeebe26d001f840bb10d79d2f32f7d36d9fc1e578a94f96cc592f4564d", + "signature": "de5a0ac920ef22080c97322c46f5b0cc5df666afe45f6d872b69f46ddc2f7b08" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "36486999f9aea343778505a15e99267c1766792f63fa5673e57bd7e195db4b6d", + "excess_sig": { + "public_nonce": "641eb569feab8b55a85ad9d402c45c134ba149b70c0974147108143bbc90bd6d", + "signature": "9f8f376614865708bfdfa6a64b0282f4260e0ef2c2ec6e7e91bb003d09ac8802" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 3, + "prev_hash": "035747f1ea39518796f2275a7b759f524c66e61734550c0b933ff7f171e8bc08", + "timestamp": "2000-01-01T01:04:01Z", + "output_mr": "93f1c24445f0746c2d2177fecacb080200f72ecfe309bf0d79f8e308e52b4a2c", + "range_proof_mr": "bce3b6a9778ea8e5ff6a8cd705ea7107534b00ff516a04ead40f7e29f6a5fd12", + "kernel_mr": "f0bef3e3683983ff41f885dd59c2a08c3a66033e013c03b42f7999e3567eebd1", + "total_kernel_offset": "e427904642d1c911e5d3b699bf883ecc58585fbdc4d5b2e3c403284665c6270f", + "pow": { + "work": 3 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1852bc4169774957861763da779805844eee814d5f47f6a44470b7097bf4db50" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68bb408d0457f3fa265424b9bc415deae64aef235b055666aa9608dc21d40869" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "848d2a82e660c82b7895793d1da94daac70243adc49bad561bb394c76c063322" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8e1d2f1cd95031dc33b3b9e53faf0d54dd548250c5e48a8ae2c0ff9a113f0a4e" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 2 + }, + "commitment": "fe96e82c83a455693c4af5a30947d56f9acc505c14c3a4e3cf7b48cfaca1f81d" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "10819ab258636ef2c9d4a8792998de985ea5370b8f818a817be9dd4387251b12", + "proof": "c4d088d622b16c97e84c5122ab885c30b444b5aedd8111da61e5f871c3a22c0244bf3a2d9b8a8337acdbe85af4a95355407801ca8926bc72d6bd6225d6866b166c8aadc47273c38b4c9852d87c69eb046969684d9639f62f110b730bb1d5517f1c6394c1132de005bcb1c528e23da8bc121a3f38eeec030a44acce4b3a5fd665ccde779ccbac83cf1038be90070d7a5d8e158efdcc91a061b36141dfbab9db0a68d82c8067f2e23f8840a0bebba5c90b2c545fb0187e2d95bc5cd4aa5c65940c72117caf389b5c48cab4dcc60734ee37ebd5335c8e0ea2f83aab8e9e7d16850a1e5c8e04b0585372008eb418295bb2d8852672c7fb620d00f91f8c793ca48d57347fd9c7f256d9b5f1b0844612c1aaa25229fadae217217fc4b4a77942c89e636254a7841fc58a559c8656ff7283814ededb725d8e0805b402d79612e5b9e3204cd7ebecc586eb21b70d4e7a7dca8c5f856b05bcb68088f22a6fe6eef01f23752c8e1bbd3a7b0cc47814df62c36b0bff2c42f82f6f5fe221b21af7ad4770175ddce20eca0c1d101a4e3fd947ca3a418ba79a3c3f7047aa52562b123d05103b7470daac8364736f477664e645c959482f4e13c1353245ce79b86bb8c7cf703a5f5058322d27779463cb0cdd3ae92e53f1dc7d1ac199a0be068fd3e2bedc8570006e7141cb768678bfda806f8156815669ccd212ae2ccd67c28f4d6b047aaf38729c7e1af639fbe0bf211e1dacbafa8eb0749625b508e2002c246886baf9213e25ead342275aa8224f0ffb5ee8a502788d50062457c960792aa08ca2900ac20f6494158371c988dd4f9bce48f2931a6dfa388744a9b99b3b62a80c8f212d36724b632332636662e965297a1bc0576db4411f55eb81c9de3939825e8a64484f3c023cff07b45152f341e40e4585febb5ce1d0c1b9093cd92c4674ecd3fa9989ba04" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2a5cc9885cab5f1998534534f18e2717be1fff00da99e1a770cc04d995268c0d", + "proof": "dcfd86c5fd9fe9b1f116c902bb59191ad2e874088ddae52327d2cdd10020967ce67a0e15f0d5898f560897957a96f66a7a3ad14cac174ada35d067cbc5675b204c63c4c9363d05092f7bb76e0974226f2327d74146e602bcfad46a92bb94c26ca611e3ffa10029d41af5e3f3ce41f7ae562ace2c54a1aea8923f6ffe809f852f2b552bb90f020366dd4937e4e42a4108acc2e86b99d794cd20c77089a40aa701e4dcbaaa06efe44b55dfdec7eeab82ded11a7698eff513428bfc75ca5fa1400a5394790a9e29674dd222e328dcd6ddc2d1e6c5dc7c34812a820152a2d185b8023a568d77f90db84fac29484df7cb29c0178e16c842a78b4fc5ef4f0bdc030c27840c0fa11c2d8ab8b5b0956c8b523100f7d77c7168416fc2cc52218f6cbc6c184c7f0537dc8597d541dbd4af655d12382df6b53c0a4432f7cfb1aa00abfe0e3800e8cc50900efde0fe81628c135622c4bff40a08beed49f1014e6fac1028dc0714c7d9e4faf6ea5e4e04dfbc2e542aa78511272a63c3cac4f6d3f3abdafc9a5d745091a61b961363a9f44175ae871583d7f707c050cdcf3cdd8d46452d8b260fd2d9a89d7def97750762857fc944323a8e66cf8331606401b446551e37b38e3b207bd57e5849641738bc803879070b93874e129361928bd70e8825a1f7fe3c517eb84623e00715b7a37abc3d3cf7092b0c3361246fa823cbc7b00e4af9f3b10d901c6a7e25fb3382ea7a14979ff7517862e5bce4c4240e3852aecda6e8e2995f2c843fe7e70b55e88decc79748a4ad19380428045b36fd0dab2152d3c67a25462670244703d8e093b6dd35f6bc07597f1740d44ab552611cfaaede536f8f2c69756e0186d78652fed9725abf67864931438ec3a2a1d236651b0124496b89740d952a2a5c1556d70f04af7b28bbabf07ccfdb52ecb746a68f97b3b8dd388aa706" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2e3f9e7bae6fd6ff9c3a749d0d78904028d66d075acf11099213dbfc151f8b73", + "proof": "901e376287269f7b9419c020e683d7cbfadd02e7eec2626864112bc018d8d278a0f392844c3970402dad603d2e2bde4cc6b5876bb43cd4990ff6b65bc7fbfa4e1adf8743e8365412401b86798da2723ebda43b37c26a13dc76b9cac3a1bb036496d185e87e77b34f8ab843f3354a6396d6ba204a523875c33f98f8eab91b516b7b9fd63b94a47743bc487d70576f9489d2243a1aee33e3f2ddaf5d0fca4ef70929cc8501c5decf33fd4b5b33feb9230f09484d2bf79efe549950e3f4e36a0e0680c00dd1eedb5a62fe94e9eba904ebb97d84cd46c2263a932e857a24cceb070f1e23470e67f1ec822134e2ac81b569a33e0dadf749329d21085b60c0ea057e26ec9b61091fc71688f41d928bc7aef13856a4e0561c0d41db2642567852b3ba0c32b1518df8f3b838dcc0f68f25cbbc70c17ee67affdcacf3388d8621c8fd4348acf1299dcf48649fb46aed585d1fd7e6c1c0d460fc1830610df40590dcee482a26405592070f7432a7582a0489f51da645351bf54bb6038e3a09d8b1d1b5e33ad8e45e97aee51bc6ba059b223024fa7a905ce2adda47875dd4bbe0e6ee8b8d19c60389e0ac29f0f630b9bccb2fbda7ca2b1fb300df7b26cafc795103d91c744eb897d665cca2884cfbddcc2f791dbaaf776b3e9ebb4bc948c914984ffffb25519299dc93a0a17b571f07e36a73257d72c988b233f1cc15fbf15ac97d007a3f778c1622014a867d75a0381d83c36131f7f6ca7a1002f277b72ada688a556bff7c1c823220f52d759a7cb8affc93499710fd565685da0961fbc4d7e005e19d407a500abad82f055b15c7bb94511a3627a892dcf924c3e0d77b525d1adaea8a8972072099ba01c6c1f92e38573ca2fd1c84c15772a368fd670c75b23c90e8a17f06f921a3ac1e4fdb41bf117e5654ffb59a2710c506d38fa01b268dbff46a46850e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4c181345492db49f5ddd592099793694cc8e9e7783c18295e2e5ba726488e95e", + "proof": "bee98659c7f35d913312258dae5573ec9f7436bd3d5c81d7b382b6b225458c70a61e75d24c5f091632c272dc7454f953ad822536b8c0a3aa0a40e8e552a16a1956b17c2b5203852a7c6cbc0c93eece51d7b47cf7913f6cea179b76734a6aaa6cda988b0a75e15cb3eeacf2641fb0e6bc29b6b3ac506429ba6f5d8564e19c4e3a3d70b91045281f09cb254dc8e9e567253e86b36dfe8c2a01ce7f53d0ed6c9a0e99fa543a282b1ec7691467c956641086fe59fbb5605efcca7b050fa6f631fe0a729dc09a30aa48eb355eead205c91005cd920c9ccc2dc195d9b4e1b8fde99d01b6f9f6471ad975832317aef8209b588720da65ccdce5bf49b9a32fc7252d705bd259a1f9202df15086d961bde390161b7937e1ff8e819c84454989e3bec18c10d8f8d9994ec8fe0f0db71826cdb6ad38b7cb2a4580d268caa7956ad2016ddb09b00c7c2cc13f0f2fbd359b8aa97582f29349e85b53bd1956b152a8c4eb14e17c564140f57e50c5473fe3dcb27a170f4f249e20fa6a4e641add85ff2713274167fc15126bc568ff12e0c94dbeae8902de726ae362db7ffa4eaf70cfb3158da669d60ff8379c0185a5ff823022e0353907010edc8818328b1ebff6a5213d08c5406eed8929f81ce6518071052cf56684cae8542ac1ed060e16339440349291cd26c84c5c9611b841ddfd312710ae3837c7e45a02c87988a94af2039a8ad5d1435db422dab27761d0c4e8aeac75eac7736d93042615169aa714f92421dfd3d4216d72128c8ee8cc89fca36489cd830a7fb6a74c2cac050f05c3e3f48a6ccccb245f24534df345227d123269ea1930c317b8e955e0d7ee8013ca1f05eec3028984166e6eb6848aa0e11c1e7f62785f8467f4d6d99c5aa9bb79283abff3ba6c47070b1feea3f6326bc71fb12d2f5fea44ef1040bb2c65899cea5dd8380059a8efba0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "92eb113d298ae9adc51a4e1ab84c08aef2328a471a1235d437838faf86393157", + "proof": "9e9dc4a9f233a2dfd9d5b73196907b8a377d1971b5eeb6797d53b787ea2636416c6994099993488b01c1a25c6deb43279ba506e478314ee3af4f42662138fa4ee2f51462694cd4337aa5912576b3536585ee2418ea34d3daf217e14c81cebb089e8405737001b29fb091c8b976888788fe5f8846926596edfaab3ba88366cf47bc5eac9a08cd8990502d5fe6fd3f1d3e26c5298b88cf3bb22ae8bbd578b0350e26a66eeacdbfd754e96b474d0582b9037955cf12464664a4fcc8da4ef7831d0116521e31432ed0caebb0dc8203398e96182875b966bde751346803117f62d30b3062c8ec3d4ac6365e03cb81859ab7bc982c83340d6e80582774ef65a0840816f05b4d605784712c1d267f15cfa77e822005b9ab4d747bca5c7306a74d5a6a5a62b1a3f22ed162427c0d0e2c11afad20c4ad509d2373d8b4b27740780930bc391296270b264b660352569b4a66c4bd6218ebaffb92958edf6ae8bba8aa319a1b14cd1e3694238e2c45429b662b4bcaa6ac328850b4682e8f8143b9e5378e3f7584d4a9519f4152d9f88596c0b8608eaa15b72e8aad8dcd9cf4af45452ea17b2e4aa440379300d95e1eb8d9bfdd017126e10eacf30a5df9f8f6aa028695c2ee6cce1b8e063795de3968f933d565b7d578ce7026b7e9a333b3bbf1b171b1695a426e1be7b5685470c72496c605a0f402b3c6047b3b6a436d806e2070dbdee3690bf000500f78cae28e31385305dbcdf24febd687d01d0b70c2b1e62f742cb32623a62c0f147102a7b41f11a428b206daadbb7a2af5645c8e0f95a013a59f32807c905f7bb73d3a3f8518055102492fa7d8881b9ab8c92032bf1f68438e00975240641b918d03faf61da36fb4b2531024d09647999c88eec83835f4c6af0767480a7b4eabbefa105d90e09a2e1917e8a6ddef6e382d73e0e3fddf78220d89fb340c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9ef08ee542586ddb8fd9788d261d9db4e32c13de733a33a53b04d730a0897939", + "proof": "1c19702939a86be9c122d7059f5dfbf36f0f4a5f56ef25ab96f4ec8902014e4f8a8b24af4f84ecd5a7203159aba8bb96236b85094ae0de3beb046a5c5c1acd30b608d7964d76a97eb69e6d9a3fccc33358258a2cd329d9c8dc5b651df1be426ce2986a26bf5572d7d9cec7e263a067bd0e385b4d2e40fd7423c08ff17d3082356c8a48790ff8647388b4340ad86f951f2bd65e823100f1e06f2f9641fde3540fe7da65280f46d381ab79062ad004ffeeedccfa3bca5f59134badaa5d9171680a4f31d0c470196970677fccd38f5630f4716cc5a47ee392210bd88355bae4ad0b422689e7eb75ecde3895bded9c6df8d63a69d5c28c286b66cf1b00db1b46496622af6f4637de035db9cc4bbf368f55872460c648e4c13a4330691acd5220c4442a171f1f5e07e4e4c9f0a81186184bc43eeeec31121155fd096ea30a85d4da57c8585aa9cb488c8559c6bc638f9e9e4c0a4e123d37688b2f91d350e80e46863990e5df4133a19bb9b11115990c7474e2f8d50876459dd8c889cf2328bd87925118a4898b19a4d1e12e68967da12e1bbacaa835e2e724b4d060adb6cfe7970226462b3bcf5da84592be51406dd59fec35dc2993875e3365d8586038ce5aa8e96d8a5809a6c9900d3d70a3001cda60aaed3f477b8acf5a76746409286ef349b13946babf263a328768a0e07c96c15416a01d103b98031d7f0c590585b31860f3421282bb4dcd7cb794b6ab086f7de63e2c019fa3e9adea019913883db79c40ec7ce6166a45400c5fc464dc0e47116a8e99370bfa7543a80761ac31703cbded897a666a53386a08bcc526febb692906dcd461ac8f9b383db6336caa37026a7d78194969c073b09289d10ee27db7c9f41eb2012fa954495611b7b52847f51a9e3007593491fa247158d26c663f50bbb707860215d40e141f6c11d8e92af9b2ad7104" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "aed9066358d21b658689b6c751f3be49e1aab81cd7f410962aa6ce17e493082f", + "proof": "74477895d572d13abdc8c37f3e4f066c5e61d1b713521496499b3208ab57dc12328b4293e04c0d3db01070629baf0967c484562ab9b5dcea7d48a980386f924b224ff9297dc9ddc86b90dadb0c843c8d19befa061e2fabc667dc5f04b852cc2ab69b020608b94bf0a103d58df433741efd61c1b1311bbfcd476f9fa9fa176641fcccd055e9a9aae5a13cd19695afc64f1e1ccd2302fe6abb6a0ed167dabd330d2c392794193a851fd17110fcc4d14b0dc8fb117fcfd449d90df5f04202d0b1050524bbfdcd35dee4c5b1ec8debe01490be3c894746b456ce6dd300c9e88d9e0b12c44cac1803b8f0ba64cf99d6dc7e47849ba93eccbc0a25e5a8a742f8b12c368e80f80aa27463c8d1a73e41e9c1d3e3723e88afb01c29587d7c667c1d560f36a8bad080953f51d4e9f770852982f0c49ce54479015775ebaff26188f6aae35492d952f70456b27d19cef4b4824ef804ee96add42b6543cb1fcb9c9833588221d8e3582b1d15958c33dcd2e877af8871529a48e89bc0af888629a3ae6bdcf129420d5dac4dc293c5d289c319680e7a6babb01847f762fcdaa6ad4be34e60cf18f291f2c39d6c17181c9bb0afac3890f506113577d14993398dcec12a4def970978daa45a78627408e1b503c76ec40a313490db53ba3de301086cbe4252729d62de63c082cf3f3dbe8e3fbd3575452b113204588141f535c2c54fc4f4ae8f1c1a668d9440d54d1ef2cef792d8263b708e5694f7992669c5d975c22258685ed01fec73237b06d94fd8c1ccd59f3ea3a33009a2670cae7abd16d59ca135ad9b0f702a59a3c9ee8686aac51e23cfbfbd7fd56b10f0d5fc7525fe11d1cd578e00c444782f5fe9dd581920f992c62ffdd6f4aa2e23a7bfaad734000c108ba77f205d0e8125759c91d4e7aed9b7b38b8b54ece0140b4e174a5e2d7ed7d19a4b63a2d601" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c4fb6e488d26b799aa118ee7255e7281279ba2facaba1d46c24fe1b9f377de16", + "proof": "cc1367ddd246d66e74dc22b933191c171925b4e32df45c6b816350ab8377bf46cc165cce310ff301bf8447a816d6b4005a51d839c0648849ba62945f1389b63a8ea61cd5bf4445c5d797b733fb87810a7414a5792d2bf150e78178cf8f94b015d8ef9591c4410c0e1133e09c2cca359b72662663ae29cc69159912d11abd053ecadefe35da79207662542c88cd8889a7cebf9420b69c6263268d8ad49f0d2509d6086fc2662401f19716719e14af456329c30b537db780a490255bab1b212208c88b290b687aba78db1cda89c19b148bf7fc81d303838f9d36241860053fe906feb6f09bc29a6dffb63c6c216c3d9f09e7b51eb7f6e066bc805558ec4cd5da3ed2bfa251d51c6cd06fdc3ef890da8cbe6e9faef52292f21d6aa72438ce3e7801a866cacec2279c71afd8f526334abddb3b99a1fb2b46876a30ed049aabcdc3355045436e0eac7a28e9bb33a63fd1429132db593e4a08daacce286611b4e62f401261ee9e045c8b0b3f06c22a02792db540c744f679b3826afd3d8f427e5d795e8415dbd3a74df2d44dfea005ba31ebf9f6bda8d78010cdf8613ced94b6d11b69c8ca015041777f5c5611e68e6024eb81b071798da5fc6a136627b9eb1cbf8a4018ba684d35d84099824f2c25bfce1e4d735d2ffe4b576afe00f63616bb6571710a95f29b0def498dbb193a6f82ae3f02708cf39335657db6f995af03f390f34d6ed612fc17976168d3e56b0f317d40c2cbf8e99b921c0afa928a67f72b169c27f227ad000bc177d55dd415bf998c21dea37997ec17795eb9e45c70167a5c4965941651d63824ad289c445f4385ccb32be2a3dc731a9402b91d42eed1053d0d27bda46fde5167135f262ac52a1bf0eb3af2d541e6c098476058f84440d342090e004749a42ee3190c194d98ae7dcc6e90e11a090ef15d822fcc0acf01e85ee80f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e6bb327c4d62bb172780a6ce3c557afd8fe6b4159fac302ebd237f4204458647", + "proof": "8e6096103abc762ea9b13720b973b279dfc7ea4700c67c8ce96208793ca89f6c4248c74790e187f1109d0191e38b7a9ef61429b165f21d7603f1ac2857ab77724aaf2059b3d8318c0c5fd00be303d16786907f395f8233cbcee9ab299969aa08d88ddf73243c207053c1a759cb62036461e947ac12a20d8aa9012b1be983da2093e5bbdb521743084bd9a8414a0c0569e181343f0e7a0a546912c3f5cf25510eea2b40d641bc4efcef366c87f9260f2e25c3e9119463711820f944cc12f36e0b841750ca873d588db1274c63d31c944fee6acad01dd4cb972c0f9288798d3d02aa627a499095606f9342fa33f053f19b6f40976daf19d87b9ebe47db444ebd0ae44b277aa9a7f33efafb8f0f50df9c279503831de9db15b5a3d30466c2101f35f62492163f6cf8fa36e0cf6bc6c4c18df21a7bd8b026e184aea019aee07f7c46a85a0d2fb8a13b95eddb12544e9aa705497b372749adbf12ad41f6437823c742586e8d705866471201425fa06edf213b15b796a675f910039a4729b66d9fc13192f263174521dbf5e333e1dd256524495f4fcc9c31be198e8b04b29388beb630b41adc9aa9c518a2c1e10a780c638044e981145ae28b84bc69ae898e7398ec3f705f2cb0ab4a267f0bfbe4cf0967386a98ce4ea26d2cfa3fd6615bce6ef81860f2999272fbc7cc662fae017d4e1197f9933bb15e001873cfd44d529d07754f26500a76945161ab55618da17e3757bdb18e570a5a0c45bf53da51067cc0625754a0394759b4182eeb2b8348242ea3a5bdbad8b542fbfad21193db5d127838117db294f699086d36fb27d2961600b2ca23c1c17a0b8423eb7db363b6ce1f2c09355237be2de3fbf814d5e39341e6ec652aa7943cc182581e9ee6a2fe478844d708929b2e5a050a2db653c283e7a75649d77121e86533d82446b979ed976e83810e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fecb2af9b231a5d15fa7b4c05fa6aa2aa1636770c623af3c80a9a803d1ae5c63", + "proof": "9c63d41b4aef053781fb8a5b1af7148b11a368a39935b65ae2e1d87652868f1db0127feab6766119237e36f1fb8b05bbd19eced44d173b998383da19b183747850b49891b25a234cfda494c9901ab872ae3878727793a0c90ca9346671fbfa5fd40972d78407c92e01e2871333bab642816f191ac2f4fbacb1c82c3bce01ad3e26d1d1c6afd441e5bf6b7fada081ed1e283f8c1a4b2987f5e9883afe0ee71d0d68efa7715ea08e0e84bd6a6d91ee3adcfde6289c33d932b4d93abb1faefdad0365457ab4fa4276c969eefa055eaecad213b82505bbdbaf323b28bc29651a710dc4a09e515439a5bdce40eb870076dfb3b8edfbb9ea06000ddba69d3c7c00b42e94ba904edff29c85c7bdad70177fec40518c53b1df7d376e22131239e1b9d556589342b8a958236a829c814760222b0630811335b0ad4ae2671081bc7a350843aac2f5bbfa8f8cd4fe71e3cf9ea12cf1550f2a919d0f60dbb87db9f757424c448235afb226011a0460bb3e78dda99f2921e7e4b04e11761ea4ad0980cae5422bf0a260e1de95faa3226386094e3e2121adb89e1c5f8193b980a9840a94b3f35720ce1ba29ec57f882b0cd3a71db9fb58d2952a37a71da98aead27f4e96ea56282493683e53eaefe1ec0c8976a0e25a76603e2e4d0c89f2472eaf417f7ac2b16c2c145f673aa8db7f8cbca52df63588d4d94044ce4abcfd716a0a65a698cb3b13b248918f8debbc7cc54da6b66b1ab9a4851038529ca9b6596757a5a18f2c5c78646396070b9420d8ae6413db8aa13756324b486a1f3ebc802137c7220b63fc666073deb8f68de01fc86389c083b1d8718414d24bd6401b7156217dec57bf2d5135f69433ff4c39ad48ae2c58d187eb21e1f7aaf7aa6dc37e7ffe2f6ca73cc304b740e021de78ccdf4800c5f0205d4c9c6ed56713b752087d95fcf1514b4ba20c" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 4 + }, + "commitment": "70ea1ed62cad4bba017ac73b24959403753e264567cc5e26c0c4618c7172d347", + "proof": "4aa2b0a73f96095b8ff4517f099f3ee494af86051aa684b3ae4c61fca9cef80ae48716d8133b66ccb1db39dec79edf64c4d1d0af17f1b34207c1401a578d20732ceac2a3892717affe659f25e53a34d32cc04fac9f1884f9c95567d956d747040e198df8144f933307e737ce85eccd944a4d3c4260d766459ab2b3f9f7f4e5701dcc5083771726bacfbf8a427d2e25560e60b57abd04042d642d5a7b023d9808ee9798a3aa1eeedb16703fa6a4144057a9caf5f8b29114d2a4ead835a3400f01fd46e26b67e4fcca6be88f098cf97ea5756b2573d2e441fef069337b0388970c48ec0d5345f2f9bda5f1b4d8a17860ac804163348f81f3791b5c6e684da593262a7155c5f028bcacecbc3f029702166139b0b3ee5cee01ef2b42a2d584adc06e5448423b90d2765b445c5e880fd0c5b6da9a9228fc0c261f2dbf710d1bfbc57a8a1857b968d3e816bfa849f13ded569b90f6b71187e8245e5509f900ff9fc7587cd5beebb9beda7a0e4773a2e38b6f3b2d7c94d2ce6c5af0c68c78df063020352608920243e506e7e53afd7fee32acd5f0c3bacf3ced55aed78230e5953ecf43e8798ba23719f374a06da2957430fbe04612f08bf7f08607af94da8625e05729583b7ec99d5ca86eec7a24775a0ecb8448d67f80f4285cd572ba8ee80717f41a7a44fe8532a45491f23a89cccfd53626e088462a6297b6edc2565ae7f99d902588dac198da6a9529044ef8892d23b3422f5e851f5974b5b54b93b7c203964b76f6644fa92a7f3017ed7d50bd6dd736f39bb48b99535f30ec86b079fec22f195ea079e6b72157896e709bb53ca034b9880acab6f5df7a200424f8ee02d81e425a7e9db1bc94b91ebe78c30e6b1f909816acda9d21e6348327501d1d49cbe43d093704bfbad4b5d9da5390b691450de3c77377a4081219bb89f3e1cfea4da65708" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "02b6900c6220bcf8613873abdf06907294d7827ccfb043aedd90662f55dd8f19", + "excess_sig": { + "public_nonce": "140bbdb75c9d8cd16cab3aecbd7e43b3d6165f3a7bd21091824f9ea83514c70e", + "signature": "94ee117fac82a122a7a6fe405e5e5c07dca3d903bacc00f5a9cc11e51bd3b202" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "14e012cef80006995c15953ffc92b8055fa923d0f3b50acb31b19947763a542e", + "excess_sig": { + "public_nonce": "18dc89bd101d3f599dc770125d941c7f35f2a71dfbf59f16deaef6bcbec7a378", + "signature": "01153b18840ef8bc0badae8618fc68d1651cbc6d6b30587a6de74bfd27015d05" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "2cbfdaf1766df51c47f5f29c5a5edb919cc927be01c30f099f2ca7ad28c8445b", + "excess_sig": { + "public_nonce": "20571a435300060a52a3bfa3896516b780aeaa9d28cc557024a1d69273c9a173", + "signature": "15fc45f056190eb243338903f433fcc05d944fa48634b553900cb6e4673f9c0d" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "e08875d2fbeb6a297cc33dc156d81915100af4130e9b60a85bc4fe54cb55ea6d", + "excess_sig": { + "public_nonce": "466f9251a1abbb639a6190653a59d3420e3c0009f9ff65370539927fdf9ad22c", + "signature": "31c223de87c5eeb54fa7ad098c58e8c74f7b68367bdf5ec0b78296de4b73650e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ea04a888f203bda1fda8729989c942ad9abfe37b31daa6ac806be23fd910dd24", + "excess_sig": { + "public_nonce": "1ab9e8051eaca6c89720ece12deec0d71d98789ad57f03d6fb5883bdaf31a267", + "signature": "e8bc2713ad02b7bf2ec7b568143e6578fb1ab5172ca53188c80dbcf27b8b670f" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "f8cd0e18073cc43a01ab42e3691bfb0de26d9fbc6d274c353d7d378016ec9115", + "excess_sig": { + "public_nonce": "26cf90956ea8b9be25c7cad32d57f42b0f5400f1c8354ca30ff94dc5f2499969", + "signature": "8741969f65443715145452dd3d81ae254f294f2fcf1c1a734017e1202afb5603" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 4, + "prev_hash": "a5d795d8f8027b63995ef6b5fb1ed787ea6454c3f1bec7fad93a52660f1a5434", + "timestamp": "2000-01-01T01:05:01Z", + "output_mr": "f47eec3c7234a9f953bc12dd31522cd95060ee0f2d0a0faff55d79015d58fa8b", + "range_proof_mr": "3b0b18cdfdc6fd38afcfc0b68a8cd076afb0a95c3dd9c0e33f5f326c1051bf88", + "kernel_mr": "2c1d0afbe37017b60a92eec22c6f60843156d47832e5fdf0d548ee7afd1090fa", + "total_kernel_offset": "b96a29945df6c0035addaa071f08112aeb3f1309007f9ce3705eed03c5b29e09", + "pow": { + "work": 4 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "10819ab258636ef2c9d4a8792998de985ea5370b8f818a817be9dd4387251b12" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2a5cc9885cab5f1998534534f18e2717be1fff00da99e1a770cc04d995268c0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9ef08ee542586ddb8fd9788d261d9db4e32c13de733a33a53b04d730a0897939" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "aed9066358d21b658689b6c751f3be49e1aab81cd7f410962aa6ce17e493082f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c4fb6e488d26b799aa118ee7255e7281279ba2facaba1d46c24fe1b9f377de16" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "06c9c1f6845de8c029c26a819f12fc5439a68529eeda35fd48a990627f1dc553", + "proof": "b470b2787ddc9e75f4f56f3f6366cfc53e9a5f0d14f324a82e54f8866718cc2fe84b1ca0855a3ad2f521e13de29c43367aa62ed150a173397df30fdc3de86168246d5bb121ce39912e634d27c91bc8ca24689bea063d71535eff3c51385012613e0a5cbb261e4a4dfdedece908ee1b915a4cebb3e7586c1e55ac0429762f81763e8fbd1bf6a1c375719abd6ba612ab904a5e320021e25183110fc50e5347f90e33250658439818104e307397a4cb89b6892d620cbd21b44e0f31b65fdc7f9008e978e154b989e92dcbb37ba727e065988035ce35ee192b2951ad69798ba2330e20a882f0ca0b255ffd33e18db9e0f72e54dd2f92e9c3d6201ad119cf5c7e447764fc9ddea4edc5272228e3a62daa6c32deea7b70e10c8d1b3706b0fe3ded3a726eb136caa1935ce52c0cb806fd0950843e405ea385ef6e26a037743f3a69a72828adf67e6809168ae9d64fc1eadc13a23f7dc33a095435790ba16f6322180e07aa6ec8f35fe3a609dab324cee90b5c44c44945cdf1aa0332d2348257dc6869599e6eda7cc05206505065fc59f7ef549193a3e232ae5d9f9972ac9b0bdd60137b707c0e01d23a16194c922c3295a24c4958d4afbdf933fe964ed9b5580865f8336e9fde2dd52da929f75b0b6caa59b3f387a735e276c913fd002e04d47c09192006ce856ef7ef5e01edfb70a0142a7af3503cfdca49bf58e225a0d99cd7c7a64df053e3ca8f1ae341111bd71429500b982d7717d0bc8d210660e04363c535ef6c226101f115762d576ac97f128acaaecfa758054af226f462570e7771a07ecf625a9ff1c7e3629867476594e80869bcc088e06bdb88f223643e70ac096da3d15bf849c05b53d3916c4d4f577ce41b161d0bac06972c8e3f8c20f1c83adf441d02e3ae1305cc43c46db18b8c81ac46a01dc663204a99c69461b52880065bc6d10e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0c468efb77a4ce8a4d1d9fbd1f01dd42227504fac1197449123038a2d9bf6903", + "proof": "16e0de0bc4fac2b10a05b0698492c64a39dead1daed5c411fa7ac4f462a70c31d69676753b5d8acb0f59adfbf4cf4310bec79175598a06f83930b258209eb615e23d7926b284402e4600ee938a65e4cfff3ed11fae975f474a28a321aabee331720d09bce8acea2970e8f17aa6230bfa7d2c7fe401b73d5726ee9b50ef16c854ad1f0245c26bc2a66fa8a828e3ec37c3af6120dba75c2f9fa6467be977008401ce36d00d33e7d305ae372f7c8514b6858f62b56509eec84f0d04fe82888b8e0bc14012c22f09ec139b93ac5a107c1c36ff7a257962fc4d3b8f36e6ee952d2b06acb3b6de5fa121de0132f524fe057007ed0d8546af66820c26f2251c8dd4d03388e92d983a96bc5c146e223e3ada875bc9745ffabaac62fd6bb8045c6274870608597d12984a1f0a6592c141f5acad8fee18907c6a63292fbe516eb97702721156120031f356190d05982f216fe59f87ddf474bba16e0d545ace3098b0df104810cb4292f3cbe672858b4888487eba90da81a135e6f6fa9bfb2fc52c506bc65f30891d831eff69441b50e84b7f1d6df82f75627dd71b75aab1d173f32790f81ec6dc1d5c27a4261dcb7188aa0fffb84f3877478df49fea32bbd2f43286bd94515a359f4ff0193a5a2de69be9d23a7184fcef2a511b94dfde205be86c3e9fe22366c5c104de58b00f8c2650be690f9e00af52c8e4f354f3a0b7893f6748702b06eeeab5d270e28c088c515abc650b3b95bdcacdb3c759b25357b7daf4878a587f44b797502d16d79a74c07a257f95a5f4d233ff515889293817c488ae9ab0831bf8594aa58bdbd62b7766e1732a1d4d5d681964e6939c800bbe42a46662de4d476b0df389322b485c47c4f4c3e07912a93ac31f3e851f7cf766ec1fff2af5f80d02d9b4dfcad320690082f4c5bdef4012e6b69028ddf8c684b14543ff5c6bb70a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1e96e35cccece3cf198cbaa9f1c9091e12b4da708ac675def85684cafda81554", + "proof": "d878f435aeec1b183fdb2870bb007985e5aaea4a3bfd6e17edf4203f4c5a856dfcefda708041cd190b2b40a8e554934e0748a61a8adbdecf7b94b50abcfec93de2544513c3020435346e9284b8adb7ada64be910efe136a7636c14284e8bff6e9436f3d9ba448de9d95f76b275d190eaf505a829230a20133f43d7efca9b043c97ca1bbddeb3a797870f5e977f91e576bbc75523e3e0920c0fb542a64338270b2f4df1669ae8d551a40b3999a1db2f0f4a9df897dc38cce1d082d864694e0101239e76ae76b461473362ef1f747dbf9e823e2d93d711fd193f701899cf46ed028aca5aa21e2173aada7172a567924972941356733bcca3450fea95aace57231600498ef9c2a8bb47016573c0f2a55af0d026348eb81f6608d979aef2c54a05567282e44a0acc2682373a774b87ddd9f18007f012fce502b5ebe105a49f197f271c246edad85a3419a0bbbabeac17e69c2454f117039f95b47a3da3d6330411094a60b21f95a1c40705792704f6acf6612f7609483d1195dbb9fc3ac09ae30d4f908ad6b2263adfad77b10da69a8f4a914ced1682615b8c476b93c295d78adf29e4ca2ad886665db2a5cd3da217fc278821f62f5d7ca18df0fe261b150ba5d515443031c3014f7e97bb794548abd3f72497e45a29ac0c9c66445df85643f28052a207e5c3e8807ed9d3bee7eec2ddbdb8fc05e8d1b3c7dfd1b0c1ef84f13a731778ed0e629194a7b8cb16a963dd5bcbe965b6333c2297129bf23da84a7745990ca002487855a2f770e8b8ecf2ef8f1644992920b0e36f956899ff426ebaee9f734645fef217bade32cdb0ca8ccdaebac63baaed89d071e7288ac8b82eb842bf31484eaeecb15437913732927a8899a1b75e18c01f4d80072d8cdfa85c8f4aed0a3a6d9df55fa8b4d462e957c9f19f715a16f751d2c9ad7c5e00aa0cb73f63e106" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "201492b78ecfe229dc938ebb0b09d1fc3a8852add23e4748e3dd768f16f86b6f", + "proof": "42c2122dace0b7ac4e8f3200ca63cc7656ba3ed8501d891e5def1adee64aa32c4845e9387ad8903bd36f4525063e23fabd62aff9a2129cd7869191d912a576648456b6b62bb80f6f6e976fcbe40e25f39512a50181068c8fde619df355ae1d58528b1de1f61cbd3c5f0f3de89117dda0c3d895d87bc7ba4ef7d15c6d9a12b140a99cb200a536b3e58450b51075e34bae5da53d2e91f1238443e0396a9f46f20462ccc67907f6e4011db1112cbc6eab5fb8d462fb4c21d38d698b2c3d0e69190fdce75b402ce82ac1facb589b66863d3bbdfbd83f62c3a368bb38fdac26964e02bc37a24d668c0e5dfeff55c4e83db71ac1c6684b2f0361bfffb365e4fcba404eeae8b19290c375aa0adcb30d15547aef2a829c005456ab884a32287e978cb875e860db5711326c429e513780e7522546fb5e73d8960a9dc146519a44f0d6817e3ca0f4e18ceabf4bef6fad0eeab5f3099e2df7ead3d436fc5f5effd562fee3092e4f88449ca1545751401a91c4d81954324ba3d53aba1384b9b0df37951cf85778d2f1e910a0472758d93688a07cf403ed41b93204880374416caebe650f4d6136f677907c1bee5a9142c8596d0cb8768ac33a5cdcb2a6c6e428238a430d961d2086d8c11ad30b6f941f863f966045b5bfd77e8dfaae206b18c3f6df92a02b782493c588589e098b036143253d040802b58f05cd89ee5cdc5a840fa948b8af031627f00145ca90521ea1a81f6caff89371bf54e26f72640051578ebe58a088345294a69d7e1e6b0e15fa92f6c8273d2c753c99502459f9a327fd1a5e34ab792f9e1ab2ca88ef4204fdedbfcdb9311fd2bf7847268970ac5ce55f6fd68e09257ff0f833139819fc809c65cdd96a2bd959674706bf60a1056f9c4b15331460bd033b32d8a5d7169ed1087b68abe8da8e1eddda49884f1c54399c0028fff5159c01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "62e9e96b7be33240625a8325a39149dbdf5504c2446bcf8b7cdf7d6be0f45359", + "proof": "b626d82273ec22a42440b313f9d403563079339819bcbc47bb843cdb8d15b968d0a5f9cbea58d9914b84102f097009984e36dd26e4b36b2525ee69f581a0e41a5410563ec9951dfceb3fbebefc8411106815cbd614a561096ca480ca15c8ed07a457667956c1b3bfafab1f2427351c654a9791d9c48b92375861c4a461c82b3428ac1c540f4a943be894a9d590e76202602597c3cfde5615ea6571e6d6f42601ad2713ddb696499fe03566f347ce4f6a42c6d325c15c095e17d55b093b05b10030bcd613322738437a7ed8853881540d276f034b3865233e7d5d7f4de37991023e92d8ace29d69c6ada7bb1e6c54ba477342df6e819be6c6b3c2302df97d416a3c94532b281dfb27804e152e548fecac0ab3e43967c188bc687c683f6a1077184ee108458d392d874b540c9c3543f07ddb16674ab5f529c1923560d5f4016f0ff2ed9ff3890c49b5279bb1b63f6e6ef17573a8d3cd7ca7be9b5cd353b95e216ad2d218f13aa5a5be316de474c4292cbeeec8bb3b71fc992b17e43f8325c8e771baf3ed803033a03f8563f16aa53d852da352b4b8b0982cb9751fbae93b6af86dfa11f445a23b147f7e3fc7393c15a0dbdc8488779cea7f700b1edd5d54381a145adff81c5168b538da42dc24622f226a0266779c36edb108ae9e57dc49169608ee7594111a88db1f5651582940a0dc458fe9c8c8f3faa040fd55b6cacd8eda3fca5cbd7f57b66e5c4c6fe6bad212ceff6ece8cc1362a6bebd3861208c6a0015ed2cf44a161d044ec2a1a9201245bd84695a7ff88ef607520f13a2a101099cd0a36d9235be2ccba9ac0e03c4eb931987dca155a4af87fd83ba9b99d3f36dff76015e15d19c48ebee868d9a67caeedc7c025bb5af66904096c6f671c8b09c8e004242c45829ef4cddc80ffe275eb4eca0e5a9c1cf9b3a9e3b130ad445981647003" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6cb2cab44b25d2e3d12a2c729131457f8d63ad17080367f7127562fad5c34866", + "proof": "2a831739c3b2ff0938e00b10be7dda346fab0c3d1dd27a23af7b377e73cd251b98db9726e4c40f5b6f110a311d0a6472af52820c36937c0f1f9e8ee8e3e9b344b6c4cba4f967a1f7b509271baef14fc5110829c132f9db4eae8f2fa7dae7601ed87cfd971ec1e301baa09bdab559de438a58fa1d4e74e65da92d7e6f7ec3790ebc54934d26232794dcdfdf5c80ff28ac4e831624157c60d9582078caeebd890f217336b990acd5474d8d44d6ea6112fd5f0464ed8763e1b47444df242d7c5606dc0f850a0c17565b344edbd8da2f8b210eecfd44baba491e5624058b1a08b903d8fbd7d01d2d0ec9b97fc00e59793c5fb26f735a1d1568ff54c426a6fa7782001266b305d685e326bfa9628f978ce2e6c111bbf769b5d5747ade1424b8305e7a6ac0e198b8998059504f31ac4cfd22999e66f3dbc97864189dea187c2e8dcb1950a48ac0628d1515ea5ee848739444c4d341e7a60547424338ac0b4789c5d266124980a234b7db02ecd7ad1d668ab2a69d01f7215897114fd312f0b64d037d037cc0473fc89947d64bdcf0a55371a9bad97cf20ded273c1f81b9cbbdd962dc27a2258338d25311559948953c09b5797b5016c5593d8d7f3875c950c654e68e4356ba5f957c16231d107285698c11e06e76391654080619eda2560f43833ac14f763a3da562eaef875fcb91c85eaa0c068a02498b24ac60484c34467684e1bf40441a5a8ee30963f08d4c78f5ccd1a29064faeea1b67ec533e6b4c136742cdb0a5aba466c4eef54fe5d365ac9c899e337ae1a2a5003081c6a1f15cc3ce1908d4f3e5a4a60edcd4d832660f6d35fa985fcfefcfedbf3f98dae10c36df58d41394eddf9f04d819de3f12c0599401c71eb98b320fe80b73587a1c4577c597325860ae44598c5aee9d418a7fcdb1403e4a3e37a0578cf1d398c68fe701a62d0073909" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9a9c64263382a8213f750ecf4642606f1ff52de9d23082de24b79aa6cd96e92b", + "proof": "fab5592b9bd8c18de48e141e68fb130a06bf55658e311c67a376b36b6cc8a54b168dcdb4c05f3539d8c1856eef54270df76fff5c0ed95613bc0efaef134ab2176ebe069f7eb77d8924dda5cadaf1711cc0b861c22750e319e77ede95ab090f5f2e1c7c82f73e47886669055244a1b8007e9c35d4f09653ef72daf1e07cb2f3209ee161b16cffac195d6533a796b6660e72a34b28c53c798a85c408186df5c506a47b5db3f59b82336f6192d87540f473d9c097c711ab4a8202d375e76d3c770e3461a7fd46c688c34391f5881d73467148045f4b1b747e7d7f6cf552028b8c0b18bfd15eb96a7f6bd0e1947b3f58eb368967c99bb77e30fb093c6b13082e324a4651ba3f7b916b5dfa4db34a918c976e26b1c20198ddcce7f6f7c71db3a72417cad55b7e6f40853e03821591a4a40e9d59d3f652f6a57a33533d1db57fe3a14b268f82c5a13e79dcdc3c799de7cbf752b1efd31710dc0f1fcce2be187f43fb0446582b937b503fd147c008c7148b575a69aa2d0733f4056d55aeca1e7a458d1d404eefd4329241df8a82ac4a277b579835c8bb28cf578625e677127f2d0a54231c674c94a29a2d4668f41cd973e9173ee1f262f17b41c3cd21164ac999e49b22220a1109a926d7240641b9e8a818248e58f4dda30b4ae62a4dd5faae346c995cba80f506273170001a418c25e58a53f8c39376a7b7e1911b892e7ad459358306ac974e6acc00f11373120c1315ead607e6f82c332fcccb2120d9642e1e2a604d204ced44bf61e0130e60a20ab5c43260f85364fc77a3dd724219bab32b536572be01ccff3206916ae4e01141c3664effbcd7756d609fea417e9fd2ebb447584fbc0bf6c990fbc0b35bd3f2b602a607f71793d002887688b4416b43090ff4e70d120b4beec16587c88d681235332bef1d5341695fa90d735016f8d9f90f5bbc0a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cc591c27d565e22d405c05d8242a902a8bc3d67792f9ed18911594525c748118", + "proof": "5294d1bbcce3d099e2e37bea0380b34c7f99dc17630552f67c8bf5c5a2b61e319255e585e687098952ee6398fd339e7d4d48b4851c90a3460d47c0557b7f7a6948c4acfeed4e079b5e014ed66cd75fe9a21f750b44c44fe2300fe0966807f87a66cd9e725a3675f5289859698a2df458038a6c9f1e1bb7693bb127dfc8e8377fb8a0ec06404fbfa181a887d5c88acc7fd0c46c080ea92fd142f1ab4b3340680d7b81b7951e7a3ca1ddc9b448b9646750f81642391a0480a48dca023abae9ca04e7912b5c8eae8dc02e421448454421b0a76824b15f0ebd91f46a0c88cd50c90a8e37485d6fdb10c78a08832db7cce1027d0fa129da90634ed3454cffc4e0d51ccef91a23bb444fcd9c77a355714928b8c5f39cd94310bc1557bde803d7d3111abcc95b2819ee45e5f4c67c60b6b13129dc2c0f1d4e653bdf4ff71b43100a06120229d33d033ae2b33b305c805e7fee197d1b48760848a6121c4afa0674e16b71682220f8a3c93a62f798849b76a4a87754919cc4e26cffad355b25d40ad2a1290e17b7bef746c09f4e4d08f2c7c38ef266ff052f531632e6e5bdf5ec87d60866304485b825c89e25fecb42d27643f50a2ff9b1d64f2fba3f6426e0587c76d54bea42fcef7b9577578c1a2f2a37a5b176bae75a1837b208cc3f2565e12c6b090bf23c1ea0fb0c871993d33870e05e78d2d34ee4afb2e7173415c16084ddcdd63ceeb48cefa156493544eb8265360402b5138b5bd5410bd9f61d28528423e9546ffeabb87b78c45460fab452ca60d373c3a745ac64ce55ae03c6400daa9c72261b0aad27711599fc5a84647c457ef310b0a3ee61f415829c86cfda396d18fc5744893ff7e5f4abe419c4ed82d46e129028d7a4deaaa1e6f00f38cd707faef9770d67b1dabcd9d3d934736c95d87421d8daab3ec41fe07b0daae342780e5d763a07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fa7e9702199863ac347ce758e3f7be4efb624545a28aff7552f6eede76ccfb55", + "proof": "b2dad299f8c9a58153b2c995c12d822ebeb50e11e6db503abf82be0422d88522deb83382b88ad6eaa3fbcc634359b910de0a17f127595b637442fe1d2908275bd6c61836d2451d8878b4da6c34ab45f1c61655e78031289c0c34c4f6f12b9944cc912b9f9fcce5340066357667ae468677c9568e603684afb24d0778e6994249a00878607f12abbf8365a181fc061caa0b1cbaf4d03902007df7be3f27ae960f2dc5b2c95f8084595c9a51f59465c1b129f5a4241eae207c3eb3950839157808c93494a43f368090084241f0698f1488768109ecbc7abbf3605b08afdeca38086a0ff3c31db685b1f3f2ecc795f8297db94ef930791ac1669768aaf9cc1e4b586c3e2a96905fac68e9c3ae2c32a732edc4fd539fa9cae7f8f58087548a3adb055ef18db6e26f1f7efb939310f5fe22d61144364c7aafc408b7149d02165ff11044f740358bb1939e99697d986382d03af4db07455f6ae2cd3ff9343c10c87859a2a71ce55207ae97384f8d80822fd392f8d3abca3fd0ed58dd1081c7c5e5e30524fae05c85ad7bc9373a75aa45751f636e538b8383d80629b3baf6a8212a8a50b0b29c60ded985225e3c5a5d55f5f720b115bb868a06f62e5a41aef148d8e338b2566d9206151983ff7af1275983d66e6ee608dc39f46399f2e4e189a29abf4d7885b766e5550823afc7833aedb6a19b030392fd3e56ef2eb01274111c2d487982acc60a6853047bad5e86831efaf2bb4cdbbc61623fbf06e7865610b016082bb8c861b6111947d8d1fe3a8fd694c4d67f85dcfcaca9071d450aebb887739167a2abb6c4adcf295677cea696146c3990bb5348d8ec20ea7fedd23f7ab54bd861559308cacde4817aced42a1708d7bd9ee5411d880a38d668730dc2e2bc589100ef945a4bc2c96fa73dbad409b3c0fe5337b348a4ce6e4bcce3620f83a859a40b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fa9bdc4045add5a66548ee1d22ff46f2b584d7a508f85325f68a4ae576a97629", + "proof": "8c186d07072bac15a9e5b66fdbf09aafb30f82c9c8f862e382c51f72808bb76584d0f039fb4bc631def629d318eeebf9026afeae6055491f7e077f38a2a8d64714da13e8662a4b1da2a4e8c456a457a27750303c8074e605b57e79cd72ae1a3c50ba755a03348bf64f2b3e576496c8132bb79c986d70733182e25fcba2d8576d69cc3154723e4983f0848c9a8b49b05050ec2905a0282e4c281fc453be7ee30c582329671748fc02ac11e8f41a9117164bbbb07d3464056587836395680f3305c0c7f0edc55b02709b0de8fabe65c5968f059f1fd87660baff4316b37542b908e6d4913ef8c1b7c5a7f5231724406abe05277088a3e0e59ec3d0f672f1617f5cc0b08a67729c904c00a73040baebd8ab6c23bafda160fe08975cd0b393869e3792f20b6a69a9caeb148a85c91d974d720929145cae4dcf9302a2ea0cfc4aa32088a62ac1d57e5ae6a2e6eeedbb74aca7bdf77714807ffe4d15881b2830531870d0c63e3c8457bf7e43c99bc14922e491adedabcb60e3935c74095de377b48d77fa364966b94740390304c8236bb3aeae737c8abb05bd678e1bb1343d1412d92588baf807c682c8105e6a9103483ee9b8fad05349da9547b6607a666feb94be09406c182e97d5a38e7e8eb5b7670110e68916a0eabd238c229d94eec3d75cd9462c88ae4c0dffdd8aad11a43f1b32ab0d39f75071b60cda9e1816020f59fd876448ac9d3bc7ddcda466c345c53277fc1d7c3108c3839f60e8fad15e1a95c9704070c61376710f4e41c91a1bfd2afaa60e9aad9cdf8ca1935bf6c91a5696457775dec46bdfb373d21ca5871a2b1aa7c609375f668273213afa44d4d633cf638e6696d331a9555d4e03dca1e24420549b7bf5ded392a75f105539ee0bad6cc68107025fa6fcc06eaabb2721d6ad51a804211c635aa9b3081bdde4ffb80442eb2700" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 5 + }, + "commitment": "9a0b7f372fdb755963e5fce6f0f0bfe78c39594e7c5dc8d05b41bc42d5467946", + "proof": "fa8d034e910870479aec1427bb7069e072acfa326ff3820d7357c39d7ec9b32ff436a2f7e4ef7e8dde4ba228d4734dc61456372834b427239e788e3e0ffff96d0a8f55e7e8122fa4f83ed446ed2b67a779854916f34bfd40b32975b20bf91d67d0959c921f3e507352f84b109e707c27b1ac0fb7e0d7413632bbfe82a1d6b91d9b2b70d7b58e77b120b0cfdbbe33397c6fecc8f9561b6ce7ce77ccbb5dcfa00fa5e9860dddbb7516713a3f18d380051d38850d9da0124990bb06f0fda63f7206bcbf04b1c9c319fb493dc1e4b0184cdde0de04c03e13e464bbe06572c5213f00421fc0d1994539159e03c756ea7c6ef6b6e92719cde3e35c4c701efad8ce4b44f4ecc3c9f2f9f83c9b5bc34d166ab3ace4806d522ffae98b84e23b5a2113d8484473a8c0db7d824b7914c3eb3044d06f66587ee0bdb6a62b0119070a06947c7b1c4f08a521d5d4f0547eb01c8484640c00670074fa492e9266999299752328299e0e03c333476d946ad3e94e1155e0d7a2b3c76910a00d6eb61870c4d0459b3636d2e10a3688fb143adfd3068b1095c2470ec0c10acee91dfc44e225087365000ec447b986317f990d47ba2eaba9883d67b3637f565f62c12116b91d3e80b56af89ea95181c3486c357c4ef49efadc44f51b0332caab13cde9d66b1cf10ed21bca359bb73f5aec8697a2a774a1c0f217dcf86ca14979c0ea4400c8015c2a2c633c558a3f0ca58935cd6a31f014e216d2a4bdb04303fd3b742971bbaf0ca67e3068b1f367e25e16cd5c15a6b6ce048d1a417e6c6ea67893e162bbb2a76b1378220495ab9c7b58217142bcd77df893dd8b65f8ec18063e133c76833ab02344511d5544c4b11db3c13d24aa8ff611b5e5f6481bb2b7f09c3a67f1a0895247b2f70fe73861750a9d48e346edfbed7dca2beff386e2998b35b6b4e10f936add326202" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "389ccdb505867cb92ac983bd3662ed232664e62fd190a18f8132800268e1e036", + "excess_sig": { + "public_nonce": "782a5d3adb0f8174adfe1fd6bb16b66efafac608a1256f6a4c515511e8d3ce14", + "signature": "7f82cc87052eed9da15616ad2138eb6c37639fdc77499cfb1f582d6eabf40104" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "3a0609d592ee305dd76d2d4d4e4311c1759139a2fd730fb0ec1caebcf260c55c", + "excess_sig": { + "public_nonce": "56cbee9c749ac41d612bdc05cd4ac2d1eb68e7b0a9f4267934a4e2e2aa8e2b7e", + "signature": "6a4a7f8dc198bf850cb3b779ff386b03066a19ba52a1de3c10173fc544911a04" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ac9235aa95d94256fe9349be4d3443cc8c673a5f9f1abc78296477e073bf3946", + "excess_sig": { + "public_nonce": "86b538e2e327f65908d9508a9f6ad684a1d1ed742d819d07119cb0df7f314b01", + "signature": "1220795723fbacd3f8e72db0a09bf3c9adebb828078a026f7a0f750a0431b203" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "d6a12b95a7e54b565118e89323be3f984eb1304cd9f1dd463691b25a848e090d", + "excess_sig": { + "public_nonce": "64e7e4adccf02421bbbf37b100362706a59b1a6bcac69d426363b4e37b372a02", + "signature": "0ca5c0c2d737b58d36db805b0b177a34ec07a062c7aa6b26d3e058e925ed250e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "e696cc953cdc18fde0c2ab43deb54602851e69ce311a63acfa1d3ac1eb20576a", + "excess_sig": { + "public_nonce": "3000e10aeb74e1fcbd1dfcb04bb18ae4d63796990d44ee5954c51b2e35b5a403", + "signature": "9c898c5e6db9e1f73d429f8e9499c0450a54e2e639677017cbfd45f084484506" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "6c5d377b331798484f88a2f6178ff681ea1eb28de40ae238ba850a8c1f3c8242", + "excess_sig": { + "public_nonce": "6ae560076e14d2a285cbf859ac1944e3305a793c41f51c8a019752a2e530f35a", + "signature": "880602d922fe825bb512889d2d124e0ae7053cd5dc553341bdb9636fc74cfb0f" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 5, + "prev_hash": "be01fc8608a54cf3347397b55aca70477344c8b680f8eb9e55070aa23ef80911", + "timestamp": "2000-01-01T01:06:01Z", + "output_mr": "5c9d18924b94936d4bd6889cea114cf146b83eebad701962752d55242e0ea261", + "range_proof_mr": "a9820f3447f176123dff292848143f9911354a619e14018104af70cd201f2520", + "kernel_mr": "349c94308d2365506d598f82a37a7ef7b661171dc2fa36d3f113b842d75690e4", + "total_kernel_offset": "601e22fc46088cf84e48eac9f3c25e7e1191304fdbb9054dd5fe1080e8235c0f", + "pow": { + "work": 5 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0c468efb77a4ce8a4d1d9fbd1f01dd42227504fac1197449123038a2d9bf6903" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1e96e35cccece3cf198cbaa9f1c9091e12b4da708ac675def85684cafda81554" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "62e9e96b7be33240625a8325a39149dbdf5504c2446bcf8b7cdf7d6be0f45359" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fa9bdc4045add5a66548ee1d22ff46f2b584d7a508f85325f68a4ae576a97629" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 3 + }, + "commitment": "1c74853689e34efe8ac094362c70ced414bf0ff072eaafb9cc8985aeba5e2809" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "06b695a6bf4d2bd1ace456cec231e402e9b2ae329280a3e5da780b555ef72200", + "proof": "f0585237db69d6dac9482aed7ccc457779bf27c4684bbe33e61d8da9b3ab6d2552dadc76ffbca4ffea32cd607477469ad0825f4ef029740f5897cd023327a72bea5f525aa81688801e8d18ca059341b4cad34d3ad9f8f69a5d163a7cecf5ca509e4134c110f92d53959560b6e1ae3ca01290dd757af9a989e3f0f7cb40b4e04a6bcabbe27cf52a89491522b107bc491468e8f2dfe552e6c71025f7e9fcb50d0fd3896ed96fcc802d60bf247dcf72c7acd4b520f2b83c2927269a2a01d8d2400d0c5fd762b507739f2ca81b579fd7f5fa301dbc72ad9d6cc529935225536d080c60ff7ec17c9d0b909cb6b188e5e1878968c5849fd358323a8e832dcc6f7a2c715864ebefd9bd628b74a3790f17714c1e843d3777ac9d61655e2134383a5098079a80e3922b37dd7dc5f3f6c033358debc3b932de3e927456f319d1f067ce7a19980e2e68994bb4166ef625e26e05ea1ee5847a2c0d64767552084e77ba77f43e042bb7f43cd33045e826e2ced26ba84719e14a9692de0743b5b5fa3a051d375514119cba667bf687097402bc313487e0bfb2c04ccb300bdb96aee84fb81900270ea9e9850603c42a325df9de80b299bfe2d0cb21a4e66c8c1eee81ace74682271c651e8e6f60d5e9edc51b52781bb5f4818e5aa6161c191d2cd040260af80439a82b747935162882d0dcd8cca0b66cbe22b3f3df75fc28b601abb845593da81d747b742d3112c48818d721bac3509ea950233974db2d4b5c3991f34f95075e4594c08b6c50af0188dc68600ed345626d1e56f3d15de1faf756094084c002246378f827090a49cc53b096385009b38a10a297dd06775e9b4bcc5c8c5fbe434347f1be0cd86a3a48e14515d02949eb91d5ac6c05a56e93818733fafcdbdb29a800e64997620855cc1ad8aea840e23321585e5e0d66e2e4b537d84af03c181f890b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "12dacf3364c226f29a70649f734dca048d6da94cf76cd081b67b41d9a1a4e53f", + "proof": "1e3fc57b1f109b4f8d16481aba443524f237a342a498f3a2165cebc30eaba7197a756cf7c5e8966b52e93a2efb004c643214e04d437af6c3ed1d5db68e9a3b17c08297f8ea65cd2446399bc328cd448fd8c7c6b7e28db84cb5b29f7bc68f2e4e6c188c291aa5bf028c9ae46311f56d33042699b1655fc2eac143493508e1fc738b4a264400e15c4abf2769a607322c65073b221dfc4b418d21e1fa2ebe329601e623b75e619807b2df7eb53994966e13aa5e9f942b60c281624541c7d099840911016b5f9a0bd47991483af83d776773a3c3d830438bc7036dac608301474c02b2a18141965472349f686b7b03deb63afc9ce176c92e67189bd4a682dfff840c909a5ef628b3004e2f78d525b66dc34c800d2d809558f39d552f5ea3f90f0d7c720d49d34729282884d0b49478b4c799d71e9365af392b77d568e5c172c301225ecc4ad2becea315ccb37c53f14d6df3a8553b1aab030a43ad885b7cbf8aa507d659ee6987c4b713b8b8129dfda06ce8e19ed406b21ceeb292c73958cccee656fe4f378bc29be22532ed50f5bf160a1a612bebdbdc65aa9c2afa6bc16b5a5437485d4eae4aa319724a14c2e408487e3c57cf42f5b1865f1330c66a057588860eb075d2ce3b49a1aa68d231fa00ab129f768b19d33b8f2cea8cf9cfa1ea47406f8e8aa973ae243f2ded670c81209656600e76042cb91a80a7b278a346f8b6a1492ebc8bccace0ab8fb6e0c482099decc6e31a102a72d1f1de4d03a36b9a1a7d27d2f633afa41c3d3e4f99ce8fac30c51d1db7e5a47d66d9840a379074c017c02fcca8d465281a362770b61df5fb1a294e557f46c4866cfa5ce09b67c5ae46d67d3b5b13f73638f57e68c89b8d41ce9db2c971f97db44a27b955c5ebed30c0890554284bf3bca2cadb44d05b468b4e164e9f768270cd55778729e791f9d9d99a0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2483324eb3cf5cf7385a50bbf408855a65132e108b65fdee7dde51144479fe51", + "proof": "30bcac4fffe48e711b24d834b247f542b84e6b0c30341e7870919fe6ed536f760618855a8b0cfe95d4ff331c1e1f6b5291cea3a7265246712b6ff678ae49c472e2eba6097bfb4db91f18b72d5362876916db826cc1735e47f36dbc22d4f9ac468c17f153e190b134043d54c7b4aa349f5bbc1c7a84cee954cea47d42392b1770d5e46810d348d0141feae4125eb355588b048967fb7692c721f061fd4fb0700b24567da99b7b24150beced378bf287b66e37769dcf6c2e69dc26989734a0ae02d8834ee492641ab02e5121cfbe6c80a5c8aac84450a0298827e40745a489440850734d7fa6942744015b49a54c423c7a6bd548d44ef82e97a5c5a43a8e16226e007c2c72e875d25b10f86f8534f8f119b0869596885a33811e1b9d10c099054ca09f86d1ee4da0b4a436a0221ca4b44a60e3cb8377735808fddf94cc4cbc3a4dacdddb7865f7d465b1d1ec24b68928d7134ddd2108f56682cf553851eb7b8a7394e15f87b9dd170063f770b425f9e199934f8dcf29d2634db1d7f2b857f15f412cc6e44ab2b26dced969396b293121c405d3ee33bc3b62c8d7aafb0e103c7775c05f55e06faaf90a60df90cc967ffa5563fe7331469410d9effedfe13c17aa4420cf61c14864c10518d2200185d6698b153f1ef2a922efcf43be90ac40b3ba6d96d3a46f978ccf70d55fd41a530ef7e80c4569a96c65adbaf78d39ee63c2c5359cf627c050da30fe9dce10537438c39504ffef74e84070d582fd66b400ebe822b0d8c6604a4987a71d2b3174c57e196480c4a8d81f6e7228a6710d40ea19e06fe24e83ce669a8f44f823b365285969634de8230bb79299dd659338b5cd881c5c23491cf317dd4815c454e628a6e68b84a07e774d56b510d14d20baecb40e7907975beba3e4fffd72aab4a866c55eeede1f8075288322d558e2f86dac5078f90c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3054f4c914cd7bbb8da287fed8d5ae1f8ca4c0e0221e0ea6bc1ebbfef61f972e", + "proof": "5accba0f890373339ca829c0f30230460838e73130e5e5bbfc72af9d80932e539a4a41ab939ee4e210589f8a1fd4f893376086bd5f4c9678b016e11bd55ed07c869c05ea5e3a1fcfa4c8398c6b1e219e2fb3c155d918bc6c2c3971ab6d924b210a02fc6621305a1bd8d16bbbef17d63d44e7ff0094aa12e12c9495be7102f029669053dd1a89444aea1953af67b5ddfcbbad5d9c4a045ef5f3eba375a21ddc0750938e446670b055fe323829abd2fe855cc65798f3ab4a9e566f0bf886576f0dcd6bc881a047190928b35b92b961b656c94d6a7928da20323bf9705b1845380752733404f7006cfaa57f8c3b8703b524069170c7606965e813ccd0926c57e64206b08742f96cf9869ca66a74e431b524c37abdb0d996a2516ac28a8540cefa2e4e07b51f3141a99ce5a016f746450cf7a8987fd547670c63f2466cb6d43e87028480d85fada19a527af6146e45d4783c4a9035b112faa8aa7a26b290540d1a6d0a5adf8856039f44671fe972adc972d65cc646512d0001eea9c77136432cf700ca545925ca9d97b6ae946ffa1729a249f95487b07a812d41c68ef79a02cfc60786abad9d87e416aee9d29f9686ee44941deb46e74e506a7ca63a527b05e4271530a62e280ca2f16d156feac10b08db0b11a0f599e3cdb7aaf60b9c7359fe065e28a4173df0e2058787c8d37cb93cc6e9e25fcecaf91ad1d5247488889384206a7c35796608f8c5335d7f359c1787c398e147d555f0166b332027d6cb6e2fd603b669dfd715ea422aad8646b16cfc2c62afcfda27be6a7571412a68060820da07c2f13d1860e96e02ec683ba5df89b5475e8382a2bce9a3146c6015183e70ba6c4c53f07d93d5c5cb30d76b69ee4a07fc5ea698f7cbe5bcaa30896e8e97cb450bdd2ec70f79ea91e637a44f0552f4a2fb8a286b8f74cba0ca56d794e80a0bd700" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "306ba473485f68c11702bd3d52446141c38382c7359962cb0bae9e3c1c351227", + "proof": "e0538ed8e5d38746f129569b3d2b6f2e8a83d108ff5ae91200f20174f15a314ff60006c985fc15dd34bbc04c88583131d7657deddf38525e57af3e14c09e34354eb6dc7a17b9a9275da83871f7cc0c89aad770d6c8a7815711600f1d1aba3f18d0ab0fcef9b2908cf7b425828054debb956aa13152123df23f9562475bda9756bd37483ad08ccaec9596f42724c4f32e2e5d7cb487d20026822c83dd3156a105a437d65ef692e7e9f0536d9622d982a6f9a26901136469de6e48daa15454550272ee15a2e6247dec2bc273aee5675e42c57867b6774d6dc8e7ecc63f9357b90500ad64f77ee1f58f529e7dac7ce420d65a74aa22e70ea2ec85ce24d37ed2014f4c251454b1ef4ffe45bb2cb5c3d0f427560535af371508cf4832bc28171edf5f1e87e1101107af65c0cedfe6ef698d3223a69a8d3185ad5413d297bc1f17524dd21811e32bb3cbcd974c947b5dc3fca590c652ddd0062b50e6b7226460dd3769be954f05587962008d619ad95c516faaa08aff2de6ceb26bb1ef30d1a26eee1e7e67ef46f726815483764842849bc5f821cd2ce9607dc9e4921f785327cfbc0f7c5e952c3f81709ec195121e53e8fbef7115811f47506da6bddcd58b99e3bb41c414d0034f2fd6c1c0793c8c2630710feb02c46c87a97916271751d979ce957cb0ef991708267e252404175fa6988dc7901c54d3d73b03b196a18db806a96d37d2b63885eb2686e0c583393eacea3f159d21340c9e4165f32860851c6d2be803048ef243393c348f094834031bd7b40e5ac56e9c54ea7dff844d503c070f78737090a8ed96afc08d003ddd66261c07b80f83109ca327285a18c055bab4722505916a1fcdc59117073cfd5b61a01671d9a15a267f9efdd29eec88ee5af8182d09a39042a279f1133f13ffacc598a2c46e45cfbbdc066b822c6ad1bc28774edc09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "529bacf7db2eb62e198e3e0e5bf2f827171ae13a4146015a6c1b588318b66c66", + "proof": "60d37c5e4adcab9e7a9d8ae4e1c9c22bd0d1f7c279da9d1059907a89e1c262306ca75460152afd462db5fa1fe029ae0797e72a2b82f82414b1532031352ee003b2367893293e91e8c9701cbe28de568285da1d47737c094f17e34a42c1506140c8175af84b3817d93fd1a67b16eb3e87eb283d84b148d697fe501f45031c672140d236de171d2a20bb8b869fbc16f6e93a3d8c9d79829438daecbcef01d40d0c6dad31df948d7aff0d002d582fd1080d22df68154f3114d0c11c6029a21ca60ab44313f1b43c007d8534398ea8dc53cbe2087c9d3c1f445fdbed00a28e696e0b94913a37e521d06c1f8f47f3d0a9b52b2a37ed063f7bd6ed9fa130dc0bd429062e95b1688988b2ad318432bd49830aba543440b21a784ba334a76448bb08617fec6ffd5fcf4134be0646dca4641d2f2c3ca467a4012c6f3508377cc01ad0a64190d79e45a2006952d4a8af8bbf77d01c99178e7bba3baef71e637a563d3ab710bace3578aaf38ed25f28e689214923c9bb86f3b5f649afa993db26e2f1803d2a669553de3e9df2602b72174bee1aedb729c08fcd4480a642dd68f87eab19d0386e8cc02b10d93d03464d97033560d43dd7c987fc4729fd85f81d890148e8bd29c65c8a6c8097a13978f4784dc297d5baf49b5d4f76dac12c84288cce5727867854e831eb67be11f2ab5e61a45b4fb41518fb8d0e14112c39403d64bf17c06b167a59574b2806efa814c4960b0875873e76bc03bb3b48e8122062ca62fe2bc43732eb31fc9dcb19f8840c1db86005a359845fa06b7bb528d5ff1a6235a1165747e680f6832e52f1997f37c90aeb961c6646d5659623aea3f5315eca15e72c9474ee64b1f3875a66730b552f846aae963d454a6fd3216638c1270b61e67602590a5ba33bc207135b96ddb0509a00a3cbfd4dfada534ba7c5f410555b7c1fd8f50d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a6937e0068adba8bc4c9b8a8619a07f3a82ed681aaa496de70c3ef966741611f", + "proof": "4204d7fd411c7cc324b09169d9b92f77a4a10c4b3f02c1cb6d452a71675301792c26d3b51f767b557f4c8c6c8f9c14fa027ac4f7d2ceb8c7c4e9374aa76059286ad08aa57c04a226e3977bc8cd383f5f32ed56f099eb444a54a26282056ccf28be86b70ea952a1af441dfccb24b133e1e7decb5ba4b1ed00fbe493f3df21e8493a90eb79b78806b9fc5c820687a0b38a79539ec1c906be90d7cd7bddd64316056bbb2dcbbaf3ca12e12936e4e5d0a1941767c91fbc0e1702d93726db3e4c83076017d5545b01e4d3c1def826adb876808a8ce398028cda240ad86162eae8b6019e90af3dac3a32b399746f9535f27ba9d70ac12a6fd7141d9de1e99ec3f788148eb75d2ade67035da84beee7882f564fb9c7a9624ed978e7eb94f2ce7375822c926052c70f75dd229fe3a96c5cb7c96160160fe9b5bc936f38ae64d27401f25742022b17b6112367b473fc97931c8c33027294b66e1412c6960ed03d55f50a2a606ee475776f94790564cd5973dbadb0763be5301bab6d92a74ba0d1c098b062f8780c5730d48b085342526237f97e5d43e4a13efa6661ab23fb3229f82001680038a1e61949f2dc758ad270900a1ee14390cd5cdd6b159fedd5fb84daf4133722d4356c24ed789f7be4e1641f4c8e97a52772c6ff0f4f8e379dc5b2bb83fc765e6955d12fa35eefae4f081e5dd64704eeadbb8b1bd54d0f71200fe894b2423830d31618918220cf6ff8f46371042cb70cce38b15aac3e108c79abbbf1fa12348218252f00d3f36f5733490234cf3300de2386cfe86aba495feca28c1e36ba2fc08b78f317327da527bd7658c2ac889cebd6da6348acd0f8c0b7c22bb716db2c4d1bef02d2e3654ce466843277cec46807636027c2f2fb9099a9fc1379ae0503df9917be6aacdeab1376d14d1f66e9d08ff0a322349bbdaa273228a26238f70d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e6d1c904ba1802f778900edcb9fec176afa5628acce6a953ad2feb430bb38115", + "proof": "ba5811c7b4288e758667334f9fdac4317fd7f97d95d8027a6ce3d7d9f765c25138230316518ac3fa8ff23c5270ba590e4380701069d1f928441f983525b9791bf6af53f617f2233703b6f77b5ee0c4e65ca3bc21cb767bed779efb9eb5037216a82ca2b592fb54ac3839fe301fcd819aea35c656c740e2a2962a05953d5973707d7fa52ece6c9f23379cdcfb8ffc369a82bda1157e00c3967269ba0d5004eb06a2ac8447b96030a94c86f9dd5c27641aeeb83e99280af053bb8b0a6ce48f1c0787e5879b280cccd08f7445eee13dab446a5c24a399ba720e7b22ac405b10b208626a6b5961c86b2077645a0b640838e04c96620daaea8d913e374fd1a9965824924b1f527bd78c76cf2e852b7883b016be72af9e94289abb485b73f0253f0d47dc803ecd9e396ca3ba0d06087c0b50836ced184bd304325e5495e1e821105630dcee3603e33eda30dd9873d5778cf9d061689f8719a189e8b24a962146b5dc6dd6cbd9423b55e6b6c92efad91343e2cc1ef426127d5c3976aeabec9b0e685d375cb70e3f075d6056cd768d675eb689d5ba5c66931aa7f11dee193293838dc633a4c8a8b6c976f4ffc822b2da9a65ac24fd8c1cb36d8e63ee75ff83bbe337a4363ecb3bc59431d3356bf7f3bb0df82f1a81a3b0e373c34dd6f5b73dd2981f7c3fa0ae43811cbc6bfb2b9e148093bf56cd4d278f15beaa3e4ebcb6bc9117a54e4f3cf1244d0ed511d88dd7cfb1569c9f4895fa569cf25fe46ce87d84b714f74f5eba6dbf31408d70ac171d1b2f5ff3b16d9abc2031b10c926b7a059687c2e7ea462a48b7bfab4198db06bb3d178e7b631cd73d709a33958d6b1f11c3e84bb8c56e1b4e5aee9e38a4333583f35e191ed2b9649e2057a3152ddad449d610b0553e0d5af5a13bc5314f519c38780dc9b0142378a58887d820571d94640eb3e8915d0a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "eccf2eb87dc8ac5eedbde0db5609a6dfe673df56f906436c33899f878f63747e", + "proof": "2c351f6b85a80ec7583935ee392f540ddd9ad9f16df6d19cb2ae1a758eb1e4732e0e8a5282fff0b02237a7bf88ba55021a451a1b8aa7c0d8401e82f925e9de47d4d332ec7c9cd5820b90eb2ebe8a44e0fa773b6be223126add01e985f4b724293a8a63cc853e83844dccbccfab0cbba0f4042540c2249853ffb4670d1f107a3197564630ff7aae05a6db1c1eeeeb00a1f6f840ccaf9347d5fcf70a1aaaa7b0051cf90a8847efd5914b8d927786042f579080f4ff0c1fc841ebdb04ae772441007486ca49d4ded63ea4bb0c29cca3e302ae5187323609d76ecec4e04da31fd506aed1debd0eab9dea785fda27e367d167bd5ac69c03591e57ff87f8d6bd93156fe01f2c42baef1b840772fe913c09f4d66aaf5e606148ef3cca0771b5724d091ede76f3a6fd65f8bcd0eb7a8623a63cb9cb3d195e4f43d366843ef2c222b9dd695ca205f21e7f5d4ced9e9bd579291b74d4e474c3ec9bb453c30dc460cc1784326c08d119ea5eed17507e42f09644d931ec45b0fdfc9dc32c15989aba4c13a11f74a8fe2718d9aa2d081608136db7ee7bcfbc9b4e7a3cf992bb268b2911209468407f6af94bb609bfe557e86f822cd36a901f62a70d15032edf24102280099b766a839b393c118bec3925c8fea4ddd19ff4a85deac7f1e36771756e8d9e6cf46caa997273565dfd55efb98aa7f0f221a740a7ecdcef808e869e911ea22e643a22ae90f6ff5ce32c9c0c8d1e7a8c446795ccdc51f9486b0dece7cda47e9526cb031ca3f59fbd4d7a71b8c2672b60344bc6d074042af8bfcd722a9f5067dbbd7733b433c07808baef27ceddf48f466bba0f5330e69ba157f3c6af8f8fc97dbe597d0f7d027a4c2093d418ae7b696346abc196cb5c9d8c5d130ca6cff56f4b7e9f071ea063986e64afd47cf9a74efae555869e4391cd4554e3c830747479d0b48602" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ecd0228403c39840b710608770d893115655d514dd2a1bba0e78243f8dd3290f", + "proof": "f45b379878cb2e4db41f212c5fa80c2095ee117c15ac84bd34e6601cb8683750e692def868c5ad3bbece825d85f11dca62704938ba585beb316edd1720513d228c75f67c30464643024b41ca918792bff8493fb86de82e69bcf248253bbf8c338a2d08c59626ffd9ebf7af56adb5dd5d309b1d0feac7c766e2a9c92101cdfa7ad610febcda8504f4ffbd33e46a53fccbb9894681004cdd5a9f5ee9a5113ebf095bd430b37db878d31ca55570b26da3d75bd0b09a026942141c44e78774ffd9085c0bdb68c8634fe8b65a92d991e4cfb81cc0891ef1b01b10170301b2a3514b0200fc79ffce5204f5eaa7d8f26ca360c8d1a88f1c034a0bb34fa919c1a6f0a82972aa21969a84bc6fd0eaa846799ec63ace17c0c9923e26d4cc58b3b38aa73374325876a9e1685acb1e8e67138179fef368056c20571bba073a14453e841e2d6160a762907249c960b3875b892750eaeaef6a85deacc635f2c98ee68a41186629ea75b30bee3f14ad8793874cbd3ea4d8899422eb887e907737ef3f0b8d23df77f2c2bf8ca75ba233fb5a84c49c4bdf8067e6923b569219c7d10848e99839ee2212b2e1cef4b83939b559c71fc75ce289af7739016e2c56cd37593d0509fa7f58660740de8f69a4bdb9e43ea2b768d50933336a2eddac3aeb306ddd016880e634f465402a8c544b20c1f764d4770f15e35d1c27692307ba49bc9fa45d9ec9540ce6823e98c063272d95a34c5fd569131607053f1d56731498a73ff954117fc167f6fc109f8e0925a301aad1e14e15736d7f941a8ab2d9b70c45a0b3c089e1f26b787bfe3c4143562c0331e0433b9bc36952d83e37c9725c850b43584acab5c207b3beddf1e690a123f8512a17ade1e17b441d746736f65418efda51f76e7e83014f241103cd201c8f5d3a13c18ef9d9c34c64488c8d1d635bd38883d8b2a49109" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 6 + }, + "commitment": "a4822003e6a42970092bbd7f714b7b670c0b1039e403e2bd8d38b57d8ab2e546", + "proof": "1c9fa41b61cc56dbb965c7563e11e11a53a9ca00ee30f1b66bf29565a4605e694ccf883389fad575f3a3d320554bd89defa383b2312d459dad41c541e7dca071889343d2e9901c8319ceadf8a6f282aeeca3cb68b51092e94d158b1e0acf83366c4ac64b973fcdbcd1c4cde50710cdafd943ced450c1805f2b2627864105530aa3faab8141070c6e08aec1dc61176529d4ba336c483b3f5ae0eda1c49c605c07129a2426b2953de8f965e761aea4c8c042454f949f2db817b19354fe28b40f0f21b15016e907b9aa4f4d1735cf4c00f7638b7f732d448c2987e7d300bf445f07a409886dbc239be027cd7a81e3dde68f6f583d0ba3befbec1528165af0683e7fec2242daa1c69dc482dc0d74ec6cae80df7d9d7758f4f55d6afe1e7fe63f445d86d03a6022845e608ec0dfae934c0d0b492da59941982044b278c9447f602162d4564a4f7dbf0a84649c116df3f38743ab525895ddbe00a5799256e3514ef414b09e4cf1069157632466af75a95a7bff3df2487221ce378602631ca8ec48f22d46e3c6a7009ee83f05dfddac9b029a4db3c60cda0bdc04890f5f40477f57a92c34b4f77001295cffa94852f9c1f3b3a14ff70c10da605de3d818513e40132d6a5418ee71fb1d40233644c306b8669f82aacc9071b452ad28523f2b672317a05e98361fd7ea5a7189faa2ba7b623b00c35a7a40fb100a26b9d599f7c7e79e0f55fceaaebc6c22dd7a4fc90e94661e88c729734499d132b87949bc2a9eadc8b445886ed6dba5d87148533f7fc9ccad17ea348f1d4ffda431e9897b762e65294c3ab0808226400594568d775b1cc2d2f95970165fea9600021940741c19ee73407fa6170a6189f5836b9bdb71e452c218b2326198ee10fa174dca7bcae8deee7e0b2058813722346da23cd05ff00d5feaa2e4a68df6ab963cbd4401c0b07211a40b" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "0468c3f5dafa2317b5db3b149c107efb987af0bc078455fc3ce81a1982eae139", + "excess_sig": { + "public_nonce": "2a37de7a6de3fbac5c9c12bab13895e07159b69fa0eba012cec6773a8d921909", + "signature": "3c7a559049c5c51ba26c0639599e5d835fe290360f4649252fc6436909daaa0d" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "40a096dc3ac0b5d92e238e2f01105503f2a7c511bbd1f5646180dd1fe024600c", + "excess_sig": { + "public_nonce": "b4103a61035b823255d4b6bc3b91ead6a02f539ba501d88713e49d402ea40f4e", + "signature": "07ff2ef85081de5c04a930552b9b4798ecfbc106c400c177b9686b14bcd86309" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7e0de9dbd1abfd850c62c685db9e9634862a369ff3430ecab39471c3fca03d16", + "excess_sig": { + "public_nonce": "16f5435b59ad3a2ff09f865140bc8cded1bd449d25489e9dc5e8efee768bcf67", + "signature": "152ae52c14a435a22b8bf931c30b4efe47880530621f8a1171fad0ba0906130e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "926e8283313f3f0a11ae48d65b95e7573879df86335fbac052c4b2a4e68c5f78", + "excess_sig": { + "public_nonce": "324103fea0463cc9a0c18906b0111c09a8fa54674361f46ddce1fad801d43308", + "signature": "1930a3596a587b4f1dd2941daf90a51888613c5e83e7972fb669259f646a230f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "bc8c288d179dde69a29a16da46fdc0657915b73267f65375e72ea31b2cc7cd75", + "excess_sig": { + "public_nonce": "f272c8fe752092c2e10d7c7f9cb5502f7b546569dd82a8a6d8ff2f3211afad65", + "signature": "374a9c1921348fc8fd0583a0818867a46175f35c70a90cc144a24807235a2702" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "42edf6b866ce22c4d1c1a18987fa588fc297f0246ac3aef4dd6f3016a0fefb6a", + "excess_sig": { + "public_nonce": "887fca749e4c667752e8cc8f9d9856494e6054dc07836c6530dac8bb650edd6c", + "signature": "3de844788db04480ce1af67f10f05310b76616a1aa39e40e353b7c5c4e3a4e09" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 6, + "prev_hash": "15bf761e8b0ff4a722e0b3b339ba6cc28022bfd832e6ffd9a4803caee6ba57c9", + "timestamp": "2000-01-01T01:07:01Z", + "output_mr": "93ff3a6db3e65e82da1809d4fe80c6cc1b52c2e0c458447a378e906d0ef0a757", + "range_proof_mr": "12dd3e33396c00fa6a4465ab300bcb39864d043d3db5a6914f501436289d9f9b", + "kernel_mr": "28d5f23077af5b34ee225e073672be445c95903b23e818fa4c58120674924963", + "total_kernel_offset": "35f238fa0daabe9d38ab806602d7d58fc2b06e7411d00bb627fd273528d2a202", + "pow": { + "work": 6 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "06b695a6bf4d2bd1ace456cec231e402e9b2ae329280a3e5da780b555ef72200" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "12dacf3364c226f29a70649f734dca048d6da94cf76cd081b67b41d9a1a4e53f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2483324eb3cf5cf7385a50bbf408855a65132e108b65fdee7dde51144479fe51" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3054f4c914cd7bbb8da287fed8d5ae1f8ca4c0e0221e0ea6bc1ebbfef61f972e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "529bacf7db2eb62e198e3e0e5bf2f827171ae13a4146015a6c1b588318b66c66" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0a281b7f74df13fb093c19cd4c32bc6b21936739ac95a973dc480e3539cca272", + "proof": "387cb54d9411a72b261899a4ad48f8c7ff10b98a7be0c8a08117c768be20087f92b9bda55841562ea5c7efe04d8362dcdd8cf35d2e011d773e43904faf21d620fcdcf637251eacb0ad5f22af307082d6aeabfbcace57bf10d32fdb74c7178f7834047dc34c1b48d52afdf77c391ee0cbb07aa3d5f24db9ee1b555efe1471fa69ba814fca08809f2d0ddc2cfbcdbab945f1034d12b88dedf1ecf47ea955a8ec0dbf08382fe0174012c4d29b7709e8c2e485659f3257844c0642a6b4c1b2a29c02ae93c5938c0adae6edfe29efc5bc3bacb748447eeda6f4981d94edba6728060d5cb50a503f7a8953ee2b664371e1d8abd1ebd8b212c75910fdafdce53459c221721a76df02bfd67356c95519f5c9e18ec5a800df05769634b8c7708f9d280c37aec08291d1146173100661f430b11de560582ff8e96dafff64bd98977415795306fcabfb0c1f1ca4a1decd0adfe4eaf9b8adde1ee616b5b717ba25278ece6901767e99319b493bff0a1f89aec59d3450e4face1c39844d9289cb4af2d4a15a36040906ae0de9cc951415e5e5bf18e8776ee40ccf1e42e717e25bad387ca3fa26221e36b1f6e5550a1e80e3b3b0f398777f56e4ac7161ee8e9a877451c0facb724e82f13e7ba2e5affc5e3cc6f148cad5ae55066273fead9888d503226384523f52964cafb56188c42bcea05cffac0c8cdc1b31b799d7f4542d7df325db5c993a2e282a712b9443478e8c181e6adc6463ddd735b42634e0c8a2179b2c4bf36209c8803d5ac4b10c1c7f90eae435ab55192562a2cef03eabe720f0f4679554d00afcccbc44f0967e2fe20092b154504fa7d464065cec563c78f792bdf3dd86c83cc383c58f27154c062e738174d8071aed77da3772de9d28d1a4bf82f8289ef2010bf2f47cdc4ab5a3165ebde75be87398e7a8adabf193317274fa276490aad30d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2c9c84231d10cb8d09effec1efa12e807b7b5ec59cb96caab7a1b6c157322a4b", + "proof": "e0b3005c68a829d1ae649a117e48018a2d65fa0b06ac854579feb31d2afc9d1d3eb47b4a4abcce5a00b94c787cbf91a05f3661ce17980e08fd443dab4d6f746e7402bf3574471616b151c5ed83e1008631567d8a78a1b8435f90a25a1ba11927baafe4111eb49f805809f68f521cc076060f6a90fc1a7e42b3cb0be29cec155be5e37d150dce51a38264966a81a9941295a6f3a41199604f1b6b855641d9df0c878effe729688487f017f59f8e308f61124da872a5622522a9ddb6091a8378075382b898ebbcf884c6428c5c5ed59346187c1658b1465b634ea7fbab0ed8c20d82b51ff19c1960bfe8dafea8b441c540d434f11ef5e7f7dcfdfe35c30542813e7c3b1db8f7d60c7c06b43d7d4d780195cb1065b9a271b8f749e753e756a95b091c3798f3e1694bf7a804455568ed79877af1dd485f608f9f088f868f7745f431ce395e7778f46ce786b236c3579772a17b6e7fbb85e2b7035cd5b479ed5d335ff4affe1ed86183dd530467e6797adbe80a3a14e95f2238d23463776c2e95ad7c3e4dda44798e63bbaa8052d1315347ce2ba048e8b7c746716db56c28c4f9ab25e4c7844865767e5c77db5a271058d44efe1d0569e5161e3de02ecd38332497513281d3e3aca31b7bee5e3f63ca9569b86c728945815982066d4cc41f9ff5742340b0c9b5d3bd9fd214a6c4f4ab3cc482e3110a637e6c4ca1a988fdf07c20fa6384cc4cc3ed3da12d8faa3b66a0d77c3343905761c953517c1e7e1167cf3fbf7c523726f64c01123d38f391f9a476e844623cda151f0b30f4ce103c1ebfddf024b0b280c760cd86fb24a4c3744196956c41992153f24ea508b19ca2f030dc24413ce278783f7ebe876bb47586583779721c81a7102c1b9b232a62d3e715e08702fb9cb0dd001edec109eb45bfbc1c884d05b26d9347d4dd04ba3b6d5c0814b50a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4e97f1f76130e298c9ef877c250dcc2673b2ca6ccae84aeff3c5cc34f3018076", + "proof": "fae20f4831636c20b02d7549af36e71a5d060a627b3c62022f24a266e028fd511a2ee919ce9c5dfa7d07451efe568ae46131e2c7850cf4500e3fedc62e9fe22d9a0c754b08e258aaad2efb5d78fed280fffa6e875e6e42c595c8cdae16c3a56ea8d570eef3f50305b427d80a79c8370de29e31257d75a697d0e78c0fa6486160a1e6199b2f0c24fe766818b8e10c7ec892c86e1f8847c8187c3a8019460c6d052efe68bce5cd30f63f205a6b537e1156c60e0f6ec8fadd91009b42ae2d261f05f6f34af5783be00a469d3bf198ddf90c3535982428cd9dbfd633c28ad581010a346803529ee07a18df6c5adc4e002e8ae276f027e6b03ba40219ec6c7fd9167a9a7aa4ac0b52d0aa83548a464e60ab2085d26222bb41275c267d17afccaa026ad059253eae0da824fa29f5503509da92f179c59d9a14848f9909df66489c640c701870611aaf3f05efc6c3430bd001f5acf80a72b5c79d031214cbb2f51b3b34fe7e3a5ac1a3322c51dce6110ddeec32efcd898417782dd603614b15e11854465c1d94dcf922580ac6ea434283b4be402425be29e5c29dff94caa743214f67495e8893bcdea6c3237a2242a87fc974328c5bc536e5eed65f0ba8ce0e6638150b8817a844ba5b97a98c9ef0eceb4529a35dbfa62d9adb3814eed57fb3208f906030c5b58826b5dfba89abf596256f99a075280155398fe2414de644cb593f0217682fdf10fc65f3d05ce3488919bbd26a3f6e4d40e7dab92136e678edca284d405a4a98bcfb042c5b8f19a6c2a69f2142ad0d4c98b3cec4c24af1216afc6fb844a68f4b6617a41a455252cbb1f89cf69adf4fed2bddabef483c283662fecb0225f5aaeeff8798e8619b1031432062cc5ccb9d22d0ff4d747df1e3b8d7a643bb062cc3854e527ada578b7748ba67abacc5bc926449b2cb1c61ee31d58bcf9feb04" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "529b927f4f1801dbb81c762b36b35bfb24f32aab338e727b4c973dbff6311964", + "proof": "1e23e515ee1c176f96d273259263061db9355510cee58bf9a9ac310ab13d16688c40afeb3ec7612a1b2cc1154de27ef25352759e438aeaa189c35bc1cbbb5a724cf1061737c17e8a694dba359ff39c0d188e65fd83057bd44f0fcf84e83c2561305e74f4385e8cf14bfa565a6ef7a0177d479e5772c493b2447ae1673996ea6d2edde0c6beea2e7ce6cbcbe2258654642a084c80ce4f193982903e3f6f114000dce4939871c218c7a2cf1e0046bd7e451d962669dd7bc586e4c5f4cc471fba02911d1bdbadf9ff0e5fad312ca55647495466ac03743cbe2bb638b0b1cb99e90d4ad1d8efbaffdd3a7a8984475701c472c2b4571c1a27e345fab232897f169878c056586e0bab3b05ca98bd4ccef09c4139daab068cdbbd811a3d5f01d52f6f6502e3078fda5079b9d54851a0206381ae1a5a0b94ad2936b47bc2a6244563906bd678cfc7376ca8cb71d5f0953e85542db0ef6cfd0fa3b6709a7a60e28fa95b4afec64b76ee09b3a840e8c85d00632c2ead6a0d08f630ecc854f267cab7d93257366ace083b32ae946126e5934fc68562691d12f90ca586dd3d6d749a40ca0b06de94cb42d874729de531890f78006df109cc4519488ee457f3f1b92e9e3316266ab7dbd0fe20122718ea19195a5707492b760dca447a1875aaec00c5c429ec36365c17464c10fadb15078f2a64a3fb71795b896a81acc618f161c3feaf271d41ac44cf535690976bfbb5fdc89302c24147c063b9fc9e752e4605b2f5905ed4780e2a432181825016de5dcd5c558d0a4b39bd2a17fb4a9a0d0d41e79d97978f73ee9f4740b3ab0ea4b917ff50c89168669b5a8969a8b66f42ac320e2a90ed340f2ce102f851d43a471701c65da95ae92da057a9976deba506ae2a6ec332a9860bc70ac42d91207e5a1cffd94a6a9bf36f4ec8857f8aafcf34195b6339ef207a01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "866ff9fffae8304018f65082409bbe533dae9af7d6f800d6a1fa68ca83ba7f23", + "proof": "74347a2c088613b3aa7fedc52bbd5c3602ce6a6c98bf418114563cd3e1f146055c772a0524a64859840227ff7d32e8b9dac87e1eaf398d984cab5d7dd9f97d5a7c42591f8e7b634ef7152525b91189af93503c7f3ea67959459973968db19b7e8cf6b18897b5031c5a68db15db0f1d8609a2f54523935bebc09ab205c38ce228617c0c804e59691e9bb072f31923ed25620ee49aad3d202d934ab23fbe1e5a02529884bde151a7f401dfecf7442006db8130bce5d8d9b17ccb23c016c544f3066d87f6ea6c6892e6f5b527662f12b5cadf5514d804cb799929ea99eb4acb860942f6e7ba36662aea4df0089b2ed3c7648ba0fe2cc8eb2e236c47730ff067697d0ec8ccb9aa80ba759f87a5118847663fc2e2df03259e35065b3f38d3ec25102fa8bf3de03f20a9b57c31de6223ce00925981274aa1aad647d67b90450a45a81baae0ad6d47bca1be106d6334a51183476736a68bc159297c00df044eb737a504de7aade67d06f0f8ef49e6e7e9414ea7d72013b6b6339fad94fc789b56dfd941ac2b06af6d8b9b7070ee816a12a1100079833f785466720114b5d3cb7f62116c429ece857299b0aacf221a9df202b88c6486baa4f11acdda388fdfd7fe7e7d00d04861390bb6310a8e58e953a4fd48f8809b8f3d1276ef3ac35d27c3caca162e788d20d95e227f7ee986fb2dbb722b7961736881a74f79a921df4f8514676c2a1a2b08bf75e60597714673ddc0388e646f6124a31dacdb1ad88dd5ba2041a970d2188d290c9567a29687701bbf16bf13ed5d5ecf57c19be5ee9235d8450f4329a4f042b772050b55f6048f6fc463292a6917973c4721ad773040896ca5cbbb2ffb96ce9d3324e41a03e8d71e3891f6ed613c26796dbf42286677f5aea921960587b447683e39a3dc3f24ba537f37c02f8601c1fc5c2c5db86a7ad9595ba74508" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "964fbf5fc20fde2ad4ba3f8629aa259fbeb1130f82b35324bb1225c6a67fa374", + "proof": "06ffde2169c9e8319282e229bb8a81ef2a72f61f92225688abc792d07c62cd03241dd1ed121b36a27ea3ba76a5690e2a6e37d00687de0eb1a825909252d79932c83164919c81c65d3dc6ff9256157f6ed8bc55f47f9cdeb8f6b5a67557b08b1f0497f60f64099d42fa84774d9fddfad35c80271026dbe7e5882e1c6ec7620542c97023385c8d114b5f5abf0ac45fe387864224faddfb211306005d4980b8f90f840c67253b7d6677ff23790f603594d75858b9255acff7ae3debc07815d3f708b333c0700de1d5b5e7e7ef2f797f810e4d95666d95f55af62ed70a64850c1f0076c85ef2955980e1ba3d70ef58af513e91bf1a05a8d23b7b2cdca9f1558cde3cec6ae88a0e9bb534d864cbd3934427d8808be901a5ccb1444d61e5362a8bd7560a9090cd059f490dda2f2ed6e0bb758637971eb4aa130d46c3502ef4a75fe02d82103b1e3301cb3b74e965154f3137b0e44cf9a754923354e4af425704d8f1724054c177805512f1981d86670c96b603bef077c634a22dc1e9e23a2f77d0430a3c5f4b2adcbff76089eca7ef110ea97cd89ad59a7862bac384a8ea35b57a27011c78d2a635d6b7377ee22f55482e1c8de793da88e72473b235249578e931be752c8b884813c018601f2b0e220af9e4c7db9200745eb07ef3b92e17c0cd061b4a708109b9fc8a98d6577320ad36cffa4826f93b774410099d0ced420ba00927756a33a0b9d6e124f8fadaccf15bc5306c6d9ef81a1e85b9f8f1694a25be7b0d652c2870703f9eca53571664fc687fb3f890625fd4727ef4e909a62924b7c49e1a08904d093115256203100357e730d2bb6067f69cd4b2608294f5c20aafa92315db98a12fb7c8b3ca16fa7bb328d688c837178a45461e93dc54ccd1095e5a5108bb43f7b5fb8357d75f08ed4989fc1275da77c26b709ad31da98de626cb73bc01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ba90a6a42b25105d3690ca49756294da8a389f54c0a0e0593e7fce4b9a2ec231", + "proof": "6a48bca303608d1b6159927424cfb1792b08b6896c99c09cdb15f1669043583c04d1a45ed66fe63894ff61798cc941cf3286accdfd025f2dd656e04f9ef73d7e52f2f8c568b2be446bcb725a493ba65096e826944ee3883fb2064dba02c12665e2a47b89188759efeaadfc9e36bc33d7cd9a30889e988dcab3658ed96885935bbb2ba4507257e22623b65c8f014ae16df0dfb958fc8a91af69a73926e1a3f3069ed65c14cd6e4745d4a3ef1c23ec02042ecaaf78575cc88310022a5ed32fe00580f82b94c704d021f9e8520fdb3cf9fe7ee15396d1bcb8e12eae2eafa36e750b8c57efdd3fe7ecbe84804dce9a4117a4cb1c9807ecf5bf60db2b8b15e09a80049cbea08fe8897d803693423da239787e7e77912e2e3dc625b8c996ca9afe6060207e40677105752e6ce6aa9829097d8bd3134df78d42109a0b1dfd72c436d3684088497a9332701090235c2ae0c6a1ed3f2ac53c0b5d87c4aa0c6172c071e523f6b87c0eea62e07d2932a5c4352bc93a162d6a1697956c0f290e505e1b373519a2346cf3021e9c91ee1bd8635aaf5277bc7e6aba4718aafeb2ba7491f56735674ce7119d633d7c40af162dbf4e767a4b6ceb2f88d17e089553c3fa37eaf60d168894d37b26f8cd076f6408bf94aca1f03882a5ff38ab30e7363c573dfbc12112f607816782dd800b05847a6f63b3f93d2befed0fb740930fdf8fe85379c6675068e58e2a1aa7af1c75442e0db974ac586d8f8086a0523fdb2c495b23dde34b1092905e7406dad2c4e6ae54d8b2b08cf6ac17963a7ecab39842d88fc15355fc14c05f4030bdd37788fb419036277adc8c946219600c33707f6aa07764a685dc1622b8c2611df4b21e7d5151b989b3ec344bb5bab22f4d94bdc6bc4f81e5fde40a3b5750e67eca6dddbb4af5344a630994ae8a8f78781504153577690646c0bd0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bedd01d88083d065de1596d3b99282983ffe1e6d4dd5ab813c9d71f8fd30b562", + "proof": "a0fc3902ddae8f5ab88a637bf603189f5f456692268e255f79049d28bb07a77608d1ca536efb05b355517c8852c4228b24d58feb66f049ff7139f04097e3f65a3c9d153b007d6bfdfad682cf3684b8bf1b8df52ae60d65efa98bb4d745ae587596639100487bbc58b992dfd39ef22f6164eb7cab7f3e190ea1c01918a0d4035634055ac249a3e687535f9fa308c1d14991e98624b21ae092158281f3942107012787b7b377c3871655b2ab25d29ae421732c2f685e0caf2d61be97c0e145df0012c535c0b98f87c7f6ab9b6b0f4ed40954fa97a0d94b3c379674e4b33cae8a062c5240101b913e543cdddadf6ba5de90d2fb472eb89e327b36fc2de62e2a987c5a6e9412a9234674056b5c77c47fbe6b82f6c7b07a42d3fb91a71f31e705565338ee174c2991cfecfc97c0ce598c8ea908f081c9835065185b5dc4dfda2e2e4c3e6a4474874047ad706aa791df47c7e0ee108f88aa8df91b786f97ec161be90ee21adee48564504371b95001dfa0048543d3be6572ebeb680d87353e4649c5096a3199dcc6dc45eb4f94df1d17372b14fbb2b8a19e5b3db3c5b7454d42d968016a0e8a421bfddbc0eca7f15fda26ef188460fc72fe787a202b1a461c7209315bb4ba869893f6cacb1f6a76d6a0a1a8754494ba53e52e49dd56cf96d0ffd60d6e8858e183f2ba7ce93def92e70c7b7547a900a5df136aa812d30724039bc01e0948ae961311e53d3cde1a08dd267646ef1526cd40fa65f677242505dfe6420b4b0cfcc74e32646525100cdac8d6b04b9d7dfba0c98a708c508618c0d56c37737e204e26011ced824ed5554aaf1b03d18167570f17bd069602251bed83eed68c798543f021262858c4ea1c18509ddf75eb5899bd8d7eb8485c624abe78e848710bafa55b06b44001288b03b669319ce44bb7cb2da340e5ff4d2030e1d7a9ba0006" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c2f82a7acc5f4b16b8f8a04479c4a2a1833df726c09b9e8ebcdf2c9f29f6ce67", + "proof": "705afde4b6158df771ba781831a5e98ea568ed12c682868fcb6d5368a833ce48bee6f820296a6f953e2bfe7a85b39e26a8ca54a6a8d5aac53f983b26887fd67cb488e52c6c054bc185bc320e466ef9725a6e842484076c8fac297acf2583736e7a6d0caaeee687b8c633cfcbdc827a3a5407b2dbd3c518a548043ebc9ba12775a78c186c28bcc747a111d5af9e364d4c9fec151fa29755cdad42fbb05764510290281a17074d94470c7cc45a8d7ab3da94fc2e47c5014f89d027736ebf26ef0854ee8fb75b5f388976dd7753f98feb1e023256e2035d2bc2824e350dc8864a03d8a8620b8355f5f4e6b226bf631fa4f5ddca5857e1c40cae17472266e250870d3849f6989579cf8a320d5e6214d504ed42e2bea8b9e4bc6388ea35056a7929756a1fede16ef1e1228274b7e0999f402cf25b316a89c156d6c055b7eb0e953d12e22ce5065807c28b332c9ed44736080afab71bb3536a15f68b5d89a0d84b2c6c780ac6c01d12c6381b1f9e30e1b7c7a62c8669d323695bf79eb85d0cb26b1c662ce827476d5195100857c1389c81ae33edda453e2b2fce6c448021a7126b641c0657b503b894dcf5b8261c1221c0d82a6a10a8ac53faf27c7c1cc03b45fdd52de2c163ddd7e6fb00abfa0c10202603b4f3c9029ab51a800e332f766d5da3d44a7626e26e4e31c9f96b6ed6040def72c48f0e5f672b2e8301bd6d3bdc4bfe6511fe16b32fba037e4c8e756c2a19bd44acc82513098f690200b3523aeba3a40c0724fb75a60e804116f374e95ae4f747f9cc931a03da40480d8264e13a04c4d42780e9d3a0839695b0d00dbced5de4ac264dad562e8ee295d856da2e52404d7c5167c3e0f717e9478921ab57c2f92c50d19d3acc933757c63b79eb1c16d4578a0a57f422731f176a632764d30e7f3aa04ad58b8bc9f90e3c8de9136dba2b88f201" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ce03f25709841b22b91008b19abf8e17d664fb7a5637e4ec563c6a4c6e7f7418", + "proof": "08d3ffd9a2bbc990e15d13840b0bc2e8a1f828349b23fb2fac458a996674c500ba8b8783efdbf449c73a3c7cd2edfe5cda178199cacb8addc129832f5683910d4e5f328fd88c89ac83ffe464fc9d3db051b75a1d6b82b799d30adea1a70ad34f4acce67faf370167bf2597f77de17599f7dd2b9bdd71391d88adb1040de4c847709f6449298a73fc1dfd5eee960da8e1a94bba4692373745e3e2b79396eb230e1fa540ffcf96a51d0c0f3d6a71404375f56f879afb909b96d4d551bfb72e390182db9765774152c27fa2cc2f6ec124f59b382f53bc05c752817ac828af960108480de7d82584ac407ddcf5c93e3ad4ceacb302236520dd0f4505d9ce1467fb6dfe1e0faaa1ace2a6284019f9836687177d4baa5daef4d3400af37ad84aa6f535e28506825aa46645dba1fc3b9403fe7af3ac06bff637b605819f3a88100b937d8659c7c247acd656b71564ebf5d10ecda61fc4fdb8d2a65e9c9e5afee930985674fdd8d59cc7cb4eb48f7d379c1f4a5bcb71d83c83ae8774757e70e0898abd6ee295e82cfd429ea5f240b65523056b43c316d9f664d78e30323ad0c0077775296241de7f15abfc090d0b02a387148b98dec14a484844aa5497771f8ed1d6fc6eb43e6dc2122052eef7292e5eb87aaab0a91aff8f6a048302427f36e3099d9821ec1e62723e9587bfce651eb4dcdce5017eb15c5138b25dcb668064fda923d95a706c61a5e534eaf428a549ec5b98bde925db48c32311a8c4fde4ae549734e56d9cc4360fde67cae215e82d752e543b05b19d62f953f5b295b4a3e232c8d20a24e09970d90a9744c862ac5cf8ee944cf6bc2d67db681e070a50582f8a17d334320b897a0884a4e0a449083449e04421694d2bc1cf67ab7d50514ef23d0be5dd0b6ced75574f736f6ce09b40a8ab102d684e7c45a83c712c21472e73628f25ca04" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 7 + }, + "commitment": "145e74b7e5d80ddb4a280a6abe7f295480ff093f1ca91c327ea05ec22402af66", + "proof": "ea90541010059cc344ed1245cf972ec9a31d65b05816b05d0e1b30fda2e53c6288b0e763d8a469f556c61dba5c342d169d976b1868e2c12a379451b76d6fe319f2cb19b53112ea7c192382f2222e1733cd10065627fe012922cd988ba2f37504dcb6e1d7ee6b86968ee0b66d5068de2189888ba323deaf99bf8bd6b438e9e95c490e7ae06702149a3aff018d0eaec7e61e4a4021161d889bc565477f90eb2608cf6253ff89ea5e5deb360e8abf013b9f90e20d6fc0f1144575725935c7127c0a8e9a2f4d5637f32b04f2a16d74960a4c35bff97132b3f80b09eed9ee06da170aae971f7894c4f6ced7be466c3af674771c555dbbebb3fbebc50690ca9932170536b1eeca2ef90a3f53278024aae469534833a3213b64eb8bb9e6fe8b4d5abc05148482121141eee76f7821f5f13ec9074433010ee1416e5d77ee4b0750f8824830f41f5db9f983c48002f0238e5988f6e8c3b62fee0f0642705f341c9d23416e745f0f5e586fd13f92a09acf41e7b17bbb1c3c23cd9b98801db0e1f621e9e358b86b116719cbd4eb0c29304ed26914e2eaed2073403301ddc9e271eda7a8aa4fbaea0218a6a824c6d572036c96b6a86df6ecbe628ed13c0b081493fbec848915766c6d137c0a1e53c311d1575c0341dfa315aa0cdf982ea49932ae7c45795705da5b6c1d417aa578e8caee94288ea3a91eca0ff747286402c72e01458c7fa1094a2c303256fb4b50037f2ac4986e5d85a98eef7be8e8a3801e73884f6ec3c37a1a1f82c23c521abe85beb5d33a08a1cb488ac6d41b4629018282545c0a9d9177dcf06b2c4b5b275b87f32a458657ccab4b32b719ad44ac58b750846e453fd64280a126adf545855ab21bb5f764ac6923eb0e23679c55d3101d80deaf29aa9800dd2a0d0dd481ba5e153ca9f74c7f163b6b7e035f40fe589ae2abf687e40cc00c" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1e84397529383e9bde69a26c211d29bf4873f26a7fbc1ef2cb8410cf9b44b251", + "excess_sig": { + "public_nonce": "64da7a5e482218fc73bd92987e5d1b545727a1a9e128745c175349ea0dece305", + "signature": "a05a10cd121812dfc1db4bfba16f1b3cd9f0e8bb8e958e887e6efa74cb5fc40f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "6ef2ba154b42b31412b3cea46bdee43c04b5230e18b1412d52149987dbef2959", + "excess_sig": { + "public_nonce": "647ac318f522839cdfadf12e9623adefe0c218b69d057e0fff615ee665dc433e", + "signature": "90d4bfed5aea4584905969a14cc1decbddf749bd81edb6f60c475a8eecd8d90e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "98d69dc72fa9f3b87a490132aeba47e5955768ac963e9be8010ea914ce8a1a4f", + "excess_sig": { + "public_nonce": "08e7cd5b19b0336af1fb414fcb8ca1e761021f5c6f908cffa0cfe73462db0352", + "signature": "1c84b838572bb9182f91ce725d1a984d3eb4001f0af3535e727a36a818f25708" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "a0cf8cf03c61f0a58c2d900dad2dd35fe7973526970eb6d26f72acd93553f06d", + "excess_sig": { + "public_nonce": "facbbbb9cb0044e6176855acf4cf5becfcc1db47962086c7d3f5f83c04dc0f7a", + "signature": "2b758f2facb59556ea691cfc4b489effb14eb4a4b9281f45c3cd157bccca9904" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ea9e46c0f90fa90de2bcdbdeecdf3c631b73b392763720db2632ecf331706a48", + "excess_sig": { + "public_nonce": "9c54316bf573504811540e3eede13d9bb6394c640a2006abe3920360711c3e59", + "signature": "fec03280d30fc6ccb051e51acfdfb81ee08fbd91a8be0aa0ee15fe01bfd3cb05" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "6ec264a88546ad4566f11964f30952b69863341938318e7a1f8fb1297f0ac35b", + "excess_sig": { + "public_nonce": "ead4f25de1969d012716a1185e9ae83297e85f1be8732e702b3ac127f8aaab0a", + "signature": "d266c18b220a7ec4af31fa575e7356da3a0b807ad16a4cdd2d5af00d6fb6d601" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 7, + "prev_hash": "a14e1ab668c846704300fd7739e3be6456d9121e3fd7260dc6e1ef2aa66b7a9b", + "timestamp": "2000-01-01T01:08:01Z", + "output_mr": "dcab7b4d15338769f6a5e2f65599d694b2c19bbdf2a7f49b7f767150a3f07c02", + "range_proof_mr": "86ec0060f1862b8f13fc4fe514b3434e4489efaab2961d0e77f375144d8b90bf", + "kernel_mr": "6c05a8749813d46a7e0723adbbc1b4d66c02c4bb140c1457838a6cd6fc2257ec", + "total_kernel_offset": "e36b41967b512750e90495e733bd3a2b197fe9ee8bc09c56b7cbff98bcac5e0e", + "pow": { + "work": 7 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "529b927f4f1801dbb81c762b36b35bfb24f32aab338e727b4c973dbff6311964" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "866ff9fffae8304018f65082409bbe533dae9af7d6f800d6a1fa68ca83ba7f23" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c2f82a7acc5f4b16b8f8a04479c4a2a1833df726c09b9e8ebcdf2c9f29f6ce67" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ce03f25709841b22b91008b19abf8e17d664fb7a5637e4ec563c6a4c6e7f7418" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 4 + }, + "commitment": "70ea1ed62cad4bba017ac73b24959403753e264567cc5e26c0c4618c7172d347" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0e5e7f3346a2fe8fa3485a1db7e708348a20b154d6dc1277ec7cc8271a63142a", + "proof": "027c0a0687b2c1a410b4719301cdda8d4bb33b8de2552003f814cdafeaff703b28d603505f5094b9aad09568a06b05f035f197d1c1e1efde64df455bbef58a7e7695538d28950ff3a5c7b3280eca82a2facc91474c29f06a69dd382e0987f950c683c229357c7a00e8521984492693706b52713303455250e08ab685d1253d77b9e0cf0e022a18c8621a169be74c9edda1222bb10efb549238c4186b9b25790dca1e1b36f9fd102b68ab0dc2d53ba62de6f29fe4043e90034082c63290f63a06892b73504305d908af3b61c56ac27259c860247b518b4e70a140d764cc7ddc03e638dee639303986b631ea5d216cab616ab702930a82e0ec8ab33de09c6c393e4801a7bf143678c5650c85347adf0bae7e98394665c2671b52113808b54d74518844af0066d97fbbeea54d5c93e85b7b6bc51119d2041c615f882d4b18dd3666a6f66fa68bead9b0fb867937b14ebc546a1191f526f251b06d6d15f54c53e7153c841eafda4ec2d8bee5d2f4c9fc8ef955e22d1f9702b4bbb5df6db29c801d24c6310bffc237e81d90c33caa588ecb4f3ea76b4b029665d62c70bf916496ea138040702cb66f94990fe4d33ff5d59b01310c3974b75df48550164daf5831a244304298383abffdcaed2551030c94117cb6e3920d2f78e9aecbdae91513b88f33de435dfdbd9b2e75b9a477bd10e8d464afe0b00e286285a06366ed341ef90c464eb7f5a1b002526a63348578158dbca1e84b57222a1210ffaccaba6b47106e71da0ccffcfdc1279875b436c85911530ffc04e55924e6aded4c8f680dc8b30f01c04be5be9ed705629905813a766bde14b40e2d4a7007a5180194d4d68497d43694a11d3ff1fefda3e074530894a1dd689621b772598fac9454b1e3b7526f780d0bbcb92832ad3672fa5c152b24ca0fa400b0153d4f03a03b28dd57e61f7aef07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1095ca85b2c74077bf4b6500c4d7bfeda2ce6e0fbc546e5e6caf0385d3b5fd41", + "proof": "1c6dbb834a679db1b7f6dd99594dc0e3fa4a31595d4ec755b53103fe6165bf6664fa5e01f6ef07a05fffaecc235d3e51fc81b0a06db96124d7d49e07a632c34c98bd471c942052e9143aa132c1c28b2b481c4a2aa1baf6d2d2c49ca7e7f3ce2050a8b99e7eab363bb65894d20386096d1e8512d2276d0bb941494522b2b4a83103bf01102ca69215523cccbb9513fb7be05dcf595c0378cf7efe5edb5221780e94d801de0bd6d5a298391866e22dbaa59878acf8bdc494f5b328ac24e45b6a0b505b824da93ebb674d9107c7a27dda0c378075aa8dcfcb16e23b7ae76e4d010bf42ee66cf2422520b3a0fdbde98563d483d013710e71aaddb3f8634c04f13937c42950df459a7ab74f826d33140399811f373c0d4d48cce9b31e990703a70e2c18cc52fd399daa9f4491fc9b31ea8df008eb190b1a9fa8d8373806106379ca1a68b5d2838a9d11e81251453f5b97aeb55a76d848c1f0f311159b49b9dc935668f2358d1b2fdf975de8b8612f2683defc6f7fd746527110d9a923a8fbe3a75674e8cae4e934c47db03b6d544ce7344079a241d0c4de40a50bfc9a2e0a51178f6daae499efde2938e8f0f36b0b0f595b86b385e2fcb9edde9d2fb8d977bb19a243d2016195884381816bbda677a0a807c9b64ab76a1661b613d5adc226fdec161f32ad529bb76c5bef9ea7e40bda0ee88484885f3665a0249f7ce1ea197130855978d2234fcd9c9da78865ee32b1ed90154d280df6b4e3f2979fba0df5738ec61162d40f6b4e6b9d801b5b48a656244073bf3fdc5f2dbee8aa55a8c94c1a7afa212244799105bb3cfd5e0321e7d0741f0b8a02ca9e3a739441c95ceb5c9ae71c4f991f46f3514fe4d85f72d56ccd02d9788c683ef4220789595188170cdf38b80acfd7db133deb0429df70d749c81fc64bc40c8b02f36a22d3f8ac5e3ee25a6600" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3c1019d2b214cf264dcff93b059d75e3fc0f41a7742fe206bd638f10fc0ee821", + "proof": "7690b4e9c16c199c0c821f051dbad7dd4c6b5a7827ffb0389421a3e3ed5bb35d9228eccdd4e832c258450fcd9297fcdee9bb50a7173ec5f5683ff47276fe3d6298a02291ceea84b684a98a8b08404db1b3a097c6a5c63d4e2aa1e09bebe08f15f64e9b0498e7a97d89a4ea459dd3f34137da5cfb2cd0a7ee25f54da4a140ee61dddaced0f9eae473f07d13318acc7d351edc471f596052867741d4748d0309028994763cdf7fcfe7bfb1ec2f219592a60175c4c6cedc302c84435af381af590f3cac58c14406f93c4458f1326e04a07beba0018d3c922184eb5b3628020a7a030ae3d2ded627412476226229f45524bcd9a69058ed64633744ced4ad7d15e20d3ccf318f92ed8053d979c009b66b1d706b91f55c27db68710bd84fb2ad29a40af8324f156ec29236a31304a5b2a69b790b23fb45f446d6dd52c87f5522944542ca0fcc1949b6eea8ec09959ce4e3a9c7e2e84f470d8d00cb04cb0cafb9a8f9121001469f8d71459462dbfbcd90aca39f694b25c7e5060667d20f30f2baecec7fc4f7b8cb3b09d0ac694d9d287fc2d4e9cc1eef7dc833790860c440553bdff05baceb387aedf16f518f89ef4417c6d3537db9584bb6d4f7ebfc280fa37bf648603a30283479ab411370a44260d3a2e56693f6375339aafce1d65d0cbdf72883009637b5f7253aa3b209eb750446d8a7755c2d171309776a48f7e428180d12e3081c631f6794c78ac92a2f0f5a06a5c452fde961831a6a1aa0625d3ff73991862f1ef260fcc8c3cd756e8236b87713bb31abb469ef731f01284004673a65aaeb212e55690d445ba24a3e6f828a1a669246327e2cc067e7b904de91e8bc2833884a9a89a191ee614c91ff53f30d4a15898c0764380b5b21858989f89cdaf629d00e5f806bde14440f51cf042e5771f0d6dc1b272f88702eb5ab6aec211214098205" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6e4727eabf292f829cb402d101eb137db211b5c5af8f50fcb256a712ca5ff47c", + "proof": "969c57670840a629d1003563d667db7604fa2f3d1a7d3a60d5043d915827de36acd7a926f621268c71f2091f1e7afaa61339a70261cdbfa7e7e3d1b01a49323f1a8c17c8f428250275b8ffb16cc8425afa968e36024a0cf0e7dbf1d62a75070e70d73b0508f0af9e01f3ee87498bf982b46f49380c6b77b84751443ebeb6355289d5548257c4d763ea28d543061cab60788e4c4094a41fc1ac84a9bb1732fe09078aa5a0ef4b093e0c5982680dfc82c3db834b9c4344689d03632877479f2200f9c5af9b921320cc8ac4d7a4f6b23f8990c8e717d46a10cfe37b7384c487b80630eab5cabad6ec11c1a766bb22691394b55bcb4f920df4683c17e382e4c02a6460bdc243636df7ec59df659473ea095f6c47232d666521efa0cdd25c28174e03aab5883a13e4e96aa9a9a8402f76e74ae1d6963a8423b47d94b5d66b3388a45f5ed47469cdee2b6005160762e29b52e9667ea8ee7deff4bad919d1314b718d409ee60ee10d6b48924466d0b7e5cdfa06a1e009afd742955d4619a2f782579022b0cc08b5b7953965b7d699a4586e534908c46ac5023161088045c036a8292910ac7b0466505be5662ec3d18b383d721b538a27e7a20433d7e1cfd3ee8d50cf700a3304e32695700543cae9ba1dbcd13edb5acc41db3966ad7244d065ea6b6768b2cb0ff8145cf0a6987476d516dcd3fbbe718043a4253cdcd6e30c971969c81ed04e8c9e8f0c69a8715e402ef1bccff6efc8a46f0a8313867cd990807b1e6b7a34b58c655ca4f1b4bed2e33d25a55ed3738ffd6b915ef2be99017f1a0cb181506aa38bb51f40c7ba50eabe38dffa2f19bf9014e2e02b746d0364013056eb48676d9cc969348fd51b6d7e6c2c1da7006951815e902eb9d8466ceac58fa95a390c5552f3f897e5c5df719e30983d647064f31ef636f1f17a8aab8d0d1b83ecb108" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6ee9ebb7bbc9a0a1036084b20bc31e3bf42993d885d07da8e8a2d151a14fe332", + "proof": "40a1d6f0f24d73822342501f332ecd51814a473f3940b4afb0a5b5d5e7582a729ebd04319c614cb41a524ca57783ea01e81eb7868380c72fd64463e50189af20004d8b5741718d948e2c4479c21060844dfd22fd4dbb13d5ee56238d9aa9551c24d8b77364e6efc7f0e83134b18b71ba03bc6fec5096dfbabe99635ae54cbf46066ea3ac563de6a13e06b27a42a46f4dbe9bb35b40c771d09da7fa3d8d59e1089c20455fd6e0c6883f1c9c66224b6f2344eeb299e4d801691e82846f4c4bb30a70982d2afe18c764ec3e8601033765feb68411d98681202d0908690e7a06f10b725c63b606978d0b3a0e14d236694fec5581c2fcbbde5f59062475167bd32c3b04fcf44f7e6fe71a7f94d24794d43daff5efb181dd11da4d72f9add4d48bf22aba48dfaa162e8f31914838f0e2745874b022242734c2091bf9aaae8fa9d8d6166227611339810b6f60f61bd9740649aa5e865752e7c2d0100a8229455535d423b086d4bce457556d886a7a8d6218bc31799ee4b3bceaa79e8361ee4628e97142da39bc0f0558fbc02287d03c3a8cacc3855c934c78585b8ee94ba42eac43815e067fe0f0c239091ac76d84c067c5beb1d48f09810682ee840c6256d3ebb7fc756004fe6a784495aafb9e74e8c2082e0d9f9eca0aa7caa64e85d6c27b9fcc892a6cfd315a322032f20f0a98bb841ab17e4d119b2c2fae7083691ac5ebed32044f8efab0be3bcd1644d47d4fcbb6ff6ba64eec71555f60482d99d09d0dca90ee3dda119e182a4aa04605cc09ea784b0d0d701f5c8c87a45baa8775172a1988c759ec36c84013de5a1e5530800482931afd601c193a1e2b1a23c01e47291b384520cd3aa6db7a52c37a91c0ab8a8c2d0e5042899e3c7bec7671570e92f9681c84011f2bf7b0cf9220b25b57c2649e9e26777ec612d6a2b326f6fcbbf34184092306" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7210bffc0723fe763dc31674087a37335da4ddb5fca8f1e83a8860ddb394d473", + "proof": "744d340e0a3e24abd3b30e8264ec919a4ed1800b8a0fcc4ed4a2c71e449eba095c791bd91814165bcc5ec97493d7f7c9ce74446066c9eaac2fabdb7bf560a87552bd7ef8f4f7b4838c4e8a0775f93593b80c4c175ca3dfd7e969a9f3f8417f41f635c06d4e3191532278a01d5b2d159d2fc7af2011b810c25a88edd7ef0a491a4687237cba182808ad0bc9e1c006324eaa8bc07f9c3c1fcf7a9d656452708505d1f450b3e49f9423eabe3c1f7133d99ce041247aea234274f236ce6fca660007345d9dd086aded92d325c03f911337a7fb5ede76600b6afb4970765a9d9854026c6f60cf97675c740e3bd65db6e1da7f855b4db760a91a0963759104d3037a648e611e0d666af80cb82467682e30f9de5e0ae1188ba0894d26d0b3ef3684da45247173564def6286a3506268d7daf0f41e800d11b0fe6d2de6b5dca6dc63cb6d8825acfdcb98cb396da9767dd39256f751c22fd723bd328331fe3005ff1a9f379ac86656c54beaa7a2fbe283d3474895a6f3690aaaf01828f3a26440175a065902b6a0b14be1ecc9c7ca21d89ea1dcbdedab23d648bbfb3cbc403850d2775529be8964ca73c43b4d6ae5ed295422f3310fbe21aa4850e92946ff10a0a0237e40a6b4a5d4bb0222cb221c6733b603248394a849e76298be0c696b591e7b7160757626ef2239e26fefa86840eb45070eb09ce9e285a4e7493952cfdec07fc77325ba37bea5e7aa7e91d7f7e526ae81216d0a2f494ee97fa6fc33e0024fc9cf2e670ca671b64861fd3b52d0a3e0ed8890b7b63879c40d0052bfd125f93c3258725ba42708f0ca6b390fb811457d2bd5c8d53a8a6a075379c3308c8329d23f77a42054e23c02ff60f06fd1b4011893c113d5d53c1392130ac5caf1029bca78761a023a885e22a8971eb34b5a9e9a91d96706c1b7b954473dcb13fd551437fac60708" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a4d276e021abc54452596406de7c5ea281e68285981bf66923371c0e1537d251", + "proof": "b09c5e4e165cba00cbd52766c07b0ef624104cb8946ef63d1b57b5590672ec281c83d2d39f7c9902dec05d0c404f60224ec1629d555dbda065a2883589a4d64a94e8091023caf09d678ef71db87f7dc05de2bcd8519dc0cd93729557d3ae551d70f46c35a2d968820ed0d54bd2aec94e40eaba05bc98a1c4b52243bf633709091f244030bfc1b7b372c0a5cc18bc8cc920cb1d0e767031fd6dd612547a9cec07d7622741b4fcd4b6046ef16701c6b1d3e1640e73082398773fb2f4a67ae3fb0bc5257f0a09394b3a99a96dbb0100e9b27378fe631f963dc1e57796a1efe04309e012b768d08bfe1e03419744aa7bbb0c0298b14ecc76c3d24ce569a6e9c1b478ae3b7b56121c0a035fcb4583a25cd3ea5054e3808b92c148da1b1b31e8eb0455c2ca10f024bf225bee9ee693e2035abe87bfb421cb483e18a0fc9e687bb9816f8a01160111c96ed1f84603ddcc319bf283fad52765b1465beee3701c472d986a7afc78bab526fbe57174bc00317880bf1349a399e765ad403cb103333b68196f8efa84b2c4c6b4f47ecba4307133903fedb7462aade66ce47218293aa3c81109c857cbe0ea62cc4c8188aae9356e7d0962faefe5eb2df00f78b5f9634f37787c6e5ff095d25a67e32046141085666c991968eb1cf37f9339140630188820e21d7cc229edfcdeb706fd19686890414f95fd5eaf31f6a508b3df8c33e7c924215ecc9e710c3b3847cad19ba2508cf02422e5a532dc59b9743ecaa80192082ea84104f7c5e91cf757419218f439678488e9b23770e0197ba9cf275553feb1eb5f4a7eae39e7b802edf3e9e1f2ad9303e06bdc6d422583e40c131cd3ecedeca6b753f538ddc64e44eeabed6307e204ec3ed9086833d2d5c1c6600afce3b46cfeb40b1ec0158ec185c1f13e1d681be5de52ca495419aa95c14786c3a9a7dac5be9909" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ec2e42ddb39a55fc4b099b8cc95298d75d4b1d7a3e85a33f5fe3ffde8ad9fa5b", + "proof": "f0566aa9a9891b765b8e55e6fbca02e57de0d1479c3b9b46f6ea6db80096131fe426ca6f84ec9ec043dd867e1fcd287a6ce7ad541f1ab8885de622bf512a3f7508f3f2ac2dcde3c884e1034ff9f328c078780cfe63fbe1da7030c67fad48ea1b36553d8ae4636ad8084b7dc9ec745da330ce775509d13e09c9c1d7d2ec3f72778371881f8d24ff71bba906163f3eaaf0e916836f209d18418d36c13394f8c408abb705f766c49b28781c0da21298297220bed98b27706008136e2bfe80752604c82ab47d2700d8f2cc2f876433b89a09a028b1a6199ecffc41ba7b15b8a77b073ca2013947561784e15d3d1324c86af4663225fa182b13a8110a66fa77e08b27b82d3d43ba46323a87bba8bb2c173cdf16741f4b12644a2b97a6fe20de41cf60862cbbb1ecf6b62b4b49e56e6be6c54b79ef00b9bea44478be47937493a8bb3408441ad0652d4f5f2258bf2ce7f128f5e2e9e0643eb20b526c9898c7bf852b2de236c31a3d91d56a170be15ce2a26cd944714c476648588ffa2826848dc280420c54501bff845171ae06d18e6b709f5a977f07b6019de39d9b3212a0292fe145d475f8675bf46462ae2bbfbdb9f29addee566658d1a77a3ab2fb36f336564a22fa80065512c9e71b619d427ca0c915f4536523349819a5e4ca0da37f7f78ab7efe677ca1cd2d4cd4b3cc1d1b58d633b2e30c9fa19d4fa988865a3adc3fc7f26fa061558de09f734f1670b8004dea5ba06ea7b91e657b150582d766d7fba56000246a18f2e9ef45b88cde6c151203eedee934f24986e49852a11e638a5329a077568a82644f8a99799694661e419cd629382c88377a96a8d5f4495b225b3ad30dd238d07288ba9dc61527538bcd0e5d712a33039983a56604b6ed563ad69286000ce9fac9910056800b12c5b0f50011d5f88ecb96de275cba64e91273e8346e04" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ee6c277567c54e5088c5e36ff5b742ad295df891ba7bebdd8efc1ffa19d9236c", + "proof": "a2eea31b16701b111112e61930b770fe357786ad8b1520de3062205994b88d2e7ed2eb697dd17f3300b0b8d345b4edc04c61a503abd22544aa6a314de5eb4c26c01f7b259bc8a6d0e92bbeea9eeda0142f07d139a7e8cb3fe699bf6c4c54cd0f8a976fc74236b74a7516f78b354b814400c6405c14d7ecd09013e5e92794d81f55c7222bb09888d00dc7a35302ca5385913b484a3b26dbc0ce053bdc106628094a375d9dd5f7a344d0fff5d8a75859d786d5fbc626bdccc1ad94bfc6bec1c50ebb14b6bdf63efa7d4e616166be5d377b07cf42693ca17b557238668f3b9eb40f5624002f39ccdb50f6e790cfcb0ac87b9ec69ab1eefc8c6d9afa8f3c59afe3676c4850c5d57985d42798d7152e5184efbcd22bbadab13e85e60c145787af0159dc8cc9c853158e836dffec8d11b774dddcc0298fe95ec2d2fa88e6b8feb8c23778aadbbb1e4ef7932642abb7c0cbf3594968a7c0fc08837b9e10688fbafc325824c9a2a25556b4f3e10066877e8d2dc6628dbeb7483bddc133ef43995c262900a25094d09e5ebedc8120f29d15e247846ad50ce64d46c2750a231354ba3bc72e9c2aa5aa59f643153f3746a151f86847688ed9fda5c46bfd279fcf2c0eb0304a9460f9ff86a27d35cc33ecfb2a9a8795537571d81dd26ad9e5ed5be151542b6aa0a4bbea8cecc97b2700826bedc6e873a8bde4b9a65e9a81cd8cde9af38f220d6617165180522013534bd9900f4e11cf55a518c2aec7756d11b73d64316ce41e5a889930d7d65ee76ce00f0c6086e00cab77bd460a8d48dba53005d081b5933eb249d87b61bfe5529ad2fabf2c8d5420f5bdeebafc07bf1bf6add8826479ed43a603151012093ac3997a5624439e74ef4fbc8d2f3f8e927139ce3e88081b090d40beda2f6f1ce851df194f9d2bccd829b0d4b8a011b52243f12632e074fddf09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ee94275b1f0824f593ef0cfd8622479d15102028f186c8dffc76ad5f9d739858", + "proof": "7a1aa3e344fb0ae794d1f3cd7ae263d0e421893d9a6c0dfd48f8fd424efa914a3e2455c9c6e89729d2f310b0fa3d759bb19e2b8606626218d930e80f415b0e67a6b0aa8240100c08474a2115754bd6a4de9e23507a0761a29cdee595de194d7416b6a74d39ffa05e1af80febd99fc80d2cab3640fd76b2a445978e508b991e039f6ad3f760c1ee22a1ee950d599470b5494bdf06be5bf5be965f1109e3cf670781a8d53abb46b57dfeed14b08d34f9359241cdefd3a92fce04b803bdcdef5c094b3f04662aedb0e2db08bd090cf8fcb2c2a8c2b7528106c0708b558d0d6d330512e748913a6ee869f34074f623fc4b5980beb4184458ef3ba868d9115a09883afe63f18d2886844bd00cff6f043b2c8135a101bb8f5db4966b415d50c66fbf7028c1e1bd5d6e2337d169ac57ae39c94f92335f3eb23c03e77cd71a4b8d963558a876e44571353f52e19e7d807ee4fa605e89b6deaedeb886ae995c2d8cfb5620f249ed55c4377db4bc3536f864215659bc01d2e4ffa615f7fd3466be6805aa1efe2c43a4e0a62ea82ff99f51172ed71c45a557cae879cc58c64267a82635a85ea892db4aa505effa1ecf4a0ccd64414a6e138ec4b67a60a2f2cbb9e065e0e9525e0a698fc35385838fac714fc0f9bb9ac3d5f0e351ad499152294ab9c7899801f63952f9300ef26ecc76cbdabaf367d8b4b567940c9ee5071723f6ce9a8ede55f42628c797596b70924d40f657416656eeedd8b65e93be46a58a1b2e88023d1768a339178933b2b6c3870c089899b9abc17afd4ddfa43ad82111b1c84b95c64ca892a4a3fcd44b05b1e2bebf91a1e7210149b7bd3a82dff5962386fd3cbedd755da493c02490c9ae7a26b5c4a89ca82ed74673a46d4042ead0e0f072e328650db23d4897ecbed0a79d3320745d0f2812c438c0064b93173931f9e3e01cd57801" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 8 + }, + "commitment": "5001a29c380652d4c263ceebd4f1fd4329b34fe80a2fdeca378c695205f61535", + "proof": "122bc4368c8b2bbfd41be5e340a3f833c115c597d8fe90342d94e052b02baf531cb71f1ca0547efc06aee726a983ba9826ac0f774a7af672b729395d6eb66872309a7c4145538b180dc36f45cba166745cf27debb365999dde9c706c3e31a01e36bfc78556b9b9bca3b6ab6a67a98faad68acf3371b2d801d902ba632da2842de95daa04c173d0b885a60d6b3bf6e71b1c3acad0a33eeb9cdf12a7f862508a0c4d42166dee21f00ced7c51e74a82a6e0342fe52ea76634dd0034e351b15d0f0444ac568e847795f993688ae5cbc79ebb2a5cd5e60c2ad2cff9230560f0a241037e06b03fc602ebf26575d7da0f15b744977ea64fafbed6a42b5a288cf6b9b4339436ba21b370804e9066b444ca27df8d473f37ce307bbfb1f41b7e2a31448b5184bb065594c9b38bc94d03f1d939bdbf08dc6222c472bb5ea4826c0bbcbdec49dc76d84435124eecbba4bafbc41da45f3ea54f63d94fbf11a60f8a9e2e5aba2e02e096350112dec3b0195a9ed7820991d23b7f48deb3c87f14b58ff086940071b29cef709216f18a5b787c45bc7c60f50c03abc5c3fa8455f06f671420b194643062e87c3930abaa851052bf168b2e7aaad2992f834291555b9eef53bdb46b1c50d71fdf70b3d876b8a873ee8f9706e94e1b797c92214ad8d7cdf4ffbb16552530be3c6bd32a098bd63ffab40e39f1ec5f3b4af0eadfcf70086e4d5874cf8e76aa4f13c7a2487ff750ca886387a69acb910badb017168a4f0a6bc5985c1f2d64e4d6b528374f49def2fd3514f92b6a133106de37dcf740487ba46865aa5c0415c838aa09bc8ef6e82477b0a0c3af3e5c8b36c453716a431d3fef031d0eb3ea6b09b2cfbecd9717d584a410af67df66bceec9a22b879d75301bb868aaab5d0901b6aff5bbdf8f5e73fae3fbc5ec829dd361716c1ea45d04a9302cd0a34deab401" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1c5aecf30f36f1f7a3b42f084480f4a64343bce458b588aa0939b40edac9e324", + "excess_sig": { + "public_nonce": "82c6ab1c59bf1ff651154c90dcf01f2480d94afbb032c8a7eaa3f99d3a9db324", + "signature": "5a89a5ddb731247248a2652fc35aedbdce47e3b3d68574fd1e3d82a930797e01" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "72fe4702878d6f13333d61f2d05aafa88e436df6206780d8a9ae2ccbe0042e00", + "excess_sig": { + "public_nonce": "becbe21739c78ec32b71663abfc3a3f5808a350e30f376c6432687aff4e4bc21", + "signature": "11f69e03eec45d129759c3e6fe2f80089dd9a5cf153ceff7421a3221ed8e0c04" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "a64a82991efc2ee27bacbda0120d7d6888310f32b5927d5b316d327227d8cd07", + "excess_sig": { + "public_nonce": "88c5a59c363ad636a073caaa1db228ed1e0f2334b0df4390b4085d40d7d19e5b", + "signature": "7f4a3a4a9dd22cce78ee98067a442d8b69dc8680f92e013ba8ebe29ec44acd02" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "e0b742d4b6b16e50583dea1a1f7adc8bb4edf1d1cf0dffb4ca1e30a9f16f1208", + "excess_sig": { + "public_nonce": "347b68c7d402b28d60bc4581e29d50471f11c1bbf16989ae5e940ba0f6db3e5b", + "signature": "cabd6e9455124da5fed81b69dd44fdbeb83d9eed7360932a786161ac120f0100" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "f69e24bb3df5880fcc4a384fe0683062f2a893263eecdb71263b0940baddf80e", + "excess_sig": { + "public_nonce": "02acf7b515c6c54f63f65877d3eb41941f477231a5426e56c5eb26d010397b4d", + "signature": "466a808cd09e4b82460ef6d3dacd2e4e0a28a2d7d2a1176f4f382708f17e1205" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "dc793e208b477a903ccb7e959fee200ed299abc5041be86590e0596a5253a273", + "excess_sig": { + "public_nonce": "aa34f14061ae508121ce47798b4741ad11764fa6ef1c6377beee227d81aba70b", + "signature": "13739f132aa3cafba84812216b974071123589d5f6d323a1a068cfa69ebe3d00" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 8, + "prev_hash": "94a8a59508af27d48608b47c8abbf16ef197a053cdc996517502ddf2c042b3f0", + "timestamp": "2000-01-01T01:09:01Z", + "output_mr": "fe39a4cd22c451e0027d09e63c25e4d7cd7511a038e3c4af2699e00b402c7314", + "range_proof_mr": "e0e2fe305cd22aedb7dd8454c72e054ac235108f837f655168faafacaaf7dc67", + "kernel_mr": "3782bb29774bbdfab90d90ccb269c9163e74df880c44a236878434370a0fc20e", + "total_kernel_offset": "3aca1715ad121f154981ce6fcf05d126fc1305ecfd168ae474c990fa921c8307", + "pow": { + "work": 8 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1095ca85b2c74077bf4b6500c4d7bfeda2ce6e0fbc546e5e6caf0385d3b5fd41" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a4d276e021abc54452596406de7c5ea281e68285981bf66923371c0e1537d251" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ec2e42ddb39a55fc4b099b8cc95298d75d4b1d7a3e85a33f5fe3ffde8ad9fa5b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ee6c277567c54e5088c5e36ff5b742ad295df891ba7bebdd8efc1ffa19d9236c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ee94275b1f0824f593ef0cfd8622479d15102028f186c8dffc76ad5f9d739858" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0c9df8687c74e9b89a2255fe4ee4966054619d1a022dac53db65cc3056a7402c", + "proof": "7035f75f98a467c27155353af4cf2f3e32e3b94789c49fc113f9674a7698451726e028f04c2c6b8e4e5e2b15c30d7f45a95ae78a6c1dbdd4cba713f653efea4dfea6aab187cb35f3c7c606fc3aa00150cd5812943a9e575f210d1377a8baba03c016ba6131b9618bc57a64b2266fd904fe0d4335d72556d0fee8270725d814617e95111a5f8a389bb4a20a85700304790c55d0215e47f060ffd5b88e0b0bd5057628292ba81ec6e03ffba6fb19decded44dc6f02132bed904228db07712b8b02034f586fcbf27341508c7a43d7e7695d4b79ccdfaac1e43a91133a36e32ec50608ebb4776f2863aa4afab01daa416dc09939dfb867a0d5e6ae49897f0a98b50fbcc9d540a79026eb127894be060427b336bccac05bbd4ae270f05e2ddd6afb5fb8a9e4065e846480fdf30ff16da0c69bf67353d2cf4051b6347a7fc0e0041a1208abb63bdbe71acafa4c16c019828c26af8c6034215e82a7934fcb98140ea77ffe65af71bdd2a855642b3ff0ac2829e3732bedee5da5ed6f93c19036ac4cc94bb406e6ef6bf68422eb46e72b22008d5e91257a156715d1226729cf5ab8db5e20645f5679f03d213431c92e3ddfd58109e2ae810e942ac30452860575cbafc9105244abb4db23891f81e3a012d2d0a512d306d6ce3999ac7ded43001a9eef9e77e2af84c391b8e1f4fd8a3e7a209694bd521e2d6eb4f082bfd679725bebc3ea31726e2dd1eb6db20da6d9b673f4f30f68c6e01f283974b414d0462c2b788f137d4890610a8f9fdbf8aa8b9d4933ac230d45309357a988f053c3720bbdf913f432a0e44c25ee723d74c557ce9b6c05d2674be2d9dac99b0fe41c4d4ec8b0451f0e819bdd8493b38268f07075c627c5cd61e3d64ed6db886f00a5d7fa81dd06310602cde8d0a3b3932f5d8febb1f14faa96b82697195a11c76dc05669f0b618c209" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "128b997d31ede16377c855ae74de018de23f9564c0b3f878bf4966356a7c7b18", + "proof": "58035a166b601987cfeca41e77cc950bc0fde413d6d1ae6673dbfa7eae156965161eec749e38489133b46177382b5cbca49929dddb6829a3788d4347ea9ca7698ca096089d99f7f8818288380c37f63f61a82c0e1d818dc98a5ec1bb410f9818d406f309fb0ee04df0cdab0e04edf1b9cf14d585b3fc8310bcbd7f24e069883031570c44f9621cfdee6e08ab724f979ce406d58ed037ee92ae54cb4e43a1dc0db22e505d535fde4be459a6b7469e785de87109fa079321870dcb01ab67acde04edb412a89db2e38dffe1037f14c9132f4c2994ff1bb2f4a2de172fa285b4820cbc407ab596332cf91c62d040e6500b971864a8a5264677b1e5a1120b61177326389cb63f786e6c133147b4417fc1ba94b9de893bf09514d91325f01cd32f1e043819cb704bcbc4ac54f2afdc18936eac13b7130b61224ef1343314c42c0abc77e8473250afa3554e58c47b26da47fd165581841f1635e335371f7432c7417b69fc93a9995ef3bd94e932fe31188945aa52109d1b1a41033cb201c3c50a3fc83c9a3071d18a13e010e0cff5d0d7ee02ba27c9ff3853260621e221d6944a1344021c1922129ee0dec57aef9424fd6b01eff8700dea97110033e77cbc730147fc1ad08507641bd1fc97fe838a0305779c5c5074dfc88deb55bb030e7bcd022e9a476e5b564183de8cd6adaf6b084231bbbe51f5304ebe8220343f2306700628bb61f4e4b51afe43ec669bc3a3d042703fcc18d5cc75133e8147a2189505114fd426e6e1a495072920e159a370974aa3cbdac3fb8d2ee63a874740da76d5b67091619e06372c681c256a2137a2742faa876c1f2bf35611b25614c615112bf46add4d31c3fc98f0df7f18f75485420ed4a25118933b7fd7389450f2c836c6cbd5b904d5cf61e28b73b6e0271a0f44cd086cdaeefc7e7071a9ee948ba6f3be5fef100c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "682b7b9317b92f722f1c28330bdb4c5afa19911697807979468dd3c2c10b9d2e", + "proof": "d073ff69925ef5c906bcfb630d0b477b25d91238603e85e6d6e2b723f8477f22dc39a75ddce1cf56b491ea85d5b19143e3c6a7d47a7544eede44bdb5e0f5f22d864c0eef027bf37d4580868a458b3a45087c968061476ba5a246092f6f02524e4491241cbca39fb33c5354f47033e78f3a4fe963f573f69d1391fcbafdcf8825206ba2772ad753a9591c252fdc0202f3dcb798066005cc25a80ebb3374acc209d6d718b42f84f1757ef97b82714e30516c59a5e4b3302c2d011fd5e06c172e077cad58966ddebb99748e8d072887d8369e240337a8a93f55965beb45304c2d05f244d88fa892c13d1c2024d31df71b7df5a40d309936cdd154fcb44b4740f91b7c0a392a1be7577d843528c91d056e40cd35a5212e6d25a8651dcc4802300f573aa6e0abce06d2328e90850f70c198b656aa0c199037dedf5117791f725e5664982ff2f77abca47b5cf3316a0b32eecb1b5c9107a98ea96fcfac13338c1c2c09ccde68dba2b80bd068e81018e9e4836538cd0c78dc9450447d90a6e8304d8951e64214e62a83f3c078ef2e0c8b824fe9903d70e34397bc9d1f2896e323475711766160d3b1bbac08e8dc21ef7963dd6350dcc51cc72ee85d9ae81a4af74d8a5646cc2388d189a95a1bbe0441e212a70dc37618399cd0b267626adf04e2e1cf3520e0a86d49b87a38a01643e88f3c2c4e4843f5a017cba1b7538f727c842acb10d81aa9aa663d6106b696f7b2041c101ecaabd4b8182f764aa46bad6c35837474f68fb90686fcfd97123c1d1684afe9caf7db1073f89541473bc10bb59933d17dc2c08b151d15947820c419cf0d44b6f24927130163acfbb755d4a62ed69e8d64372188b334d448a307e1460425b37cc594663e7819bc63214c1b424e63d1e30f2395a3ca45d46c2729f3c238043210d5160ede328a6b2042d3d3fd994f656d05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "70f44685c6a164573af3af966cfae1ef0e9a722ea7f512861d34cb6e9ca3a94a", + "proof": "a86396c9fa425a51968d83ac0c98e6f82293b6035ab52d981903b2865f31b77ef45c9c9f99d0ed1b71617a496a4db8ac7cc694eafbf513f8e71ea5f1ff34dc714e07b10b079352821cd815931049ae847c6851feb601fd2a4cf76cf288c4035670f69641ae17d1188b2f80004cb339db4d1f26e0dc134553d5cce18fe8f13055e1eb52d05655840953fee9a6b85943b9b77e7d58df00e79979bfb3aa88cd2003309e386ad8c3f077cbb884e8a41b7c194c20f6e1a4571806bf2489826731ab02619d7b0d48c59e18f2e991d653941dcf7c261293e500bae9f1c6b7846cef980b245a1870a6a300c9cc3f04cb371924f763ad189fdbf5925c5bd314dd9722e714fec4227627adbd67272cdc438e18bac26dd98a5e8492c20e403b84115e0a6011e48de410b30135ebe8365f7e112e6a4642535da03800ef7ac19957e7fc40dd39802d3d041bfb056c3d431257ee86dd8f79d08cf8ef4245dc9ae9c0c40559105e9886b72f3f9e24608c4d5bccf767f8371fe5298f04173d051a8cf32e8368c4773cfa61e40e63133724e12cd3a16cef54c9010a2586a91d1c2735a05ce2d9a206b8c9b84d8361123c4adc96b6024a420669ad94d5d0285bfb9ec0ac6ca6724a61eacf7bc8f86b5fdca67ccc3eac938911080ae063a10d30b47ff1ceb617db2f3dac85ecd55fd4b229be304d5173dd3ea86238d80f7e0bb1cc0a831f618bf8ec33b637c44b586c9fc250da31bce88e3824c81af13e71e64091ad585f02a4866e181a518758d4d0d5bdf2b94643107de2e0b01971b3281266e5e605a9bd298171094aefec26447d01f1f26f625b58beb39af966dfdc4d55bb1e28bf6e299dbac82501401f6822cc9b343c12e444bbd3f5e4eeeb8abad8c1188ee1c015bfbb41b0048878041f1012539524cfbf8726baddeb4393fdb813ff9b1aba8412c265c4440f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7ab0ea6ecf565fd05e1385c7babd93be8e31392429ec0bcfa28222e577d40f0b", + "proof": "78b7701eb9548c999e2926ef3b7af163e8ad8445444158954e7f5871ef093d206c0f80e939f8842f4e51280dbea9367f9ba49095f468a41cb0385a14632cfa6fde387fdf83f383267e7363546a0dc295993869e2a2a1ee61523f2c517d57d241ac0edd3ed0ea7e0b5d446c5b7a0d9512da45d36e96a84d6ea1df6b2ded83926d25129eb11b10cd21bf1fbb9c7d1c69730a29d10bbb46f0647b0308a4e7afd209cddf4202cce30af1a1b2a7f8e6dcf3b65c62eba81694ecb69d3aa4ac86f6e30ef5e6b9c0d3fa04419c2297fe2b54909784fbe62dc0b6da0cc2a9b92cd640aa095295a09d9ceee8daf7e3740b85623189f5657fa396769a71e51892247dd52d34e691becb07c03275bea1340a6403e0da26c9b073dca0c6f11e69a6aafcf6916f0cb217cb70af8af980a2f2eb8f9a48132dfcb0732ee78cc963d2672e225f49511edd1c1d20161e181388f2c2c7b4684751d2704fcbbe7a458ba5598d439c9e6ec45e58b7da7022f4e0ddf89e502de4ddb78f0747c124013d4b32f69017c147394074e3cc56ffaf72b137490c37c320a4630dafe65152dc17752bbb763ddc845c245690d65963799f3805e2d0c3b51068a921003b33223b496c42f297613b792282edafbc06eb9df5d359c6dbb6c2e3e2511f0fc44257f3dc7560bdcd87cf373d52dc9e23fd6065b5c9111eaa4b46d5c726cfdbaafc6029ce7523c63e67b06d0ceef09d342dd4135b1bca5d5bdec3fd7443a0739854905d00d2b606dd86b29214d447d240a6b76375187a1db2de12e65d46e6f89f7189e756adbcbcba31c5856aa2aefc34b2267886b5c407fe4b9248fee38c0939807788f794ee2ea14a54977deebcddb1f07765b2fb74b07a5702b831aa07191954bfa5d8498cd0e21be9850768a71365797b2b9f475fdde673676a512123ce82e7505ab631cc0ce495812e00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a42cf71b29fede878d1ac17e4bc86ac502874c6471bde74cb74c1ed9878d647c", + "proof": "7af97390f1f217b798866cd9751b4e99115945cf039225a6599d9ed9bcdfba0058481e75b6d061e732a323cd7c1eb0d0b5eaf1e116ec96c71c808210d01a653c3e77e0fb90c40e97d600401f666e3926a802b05ba9ee3ec9e476672fef048d4ef6a3d12bc0494f2c0eb772aca1db065ca559630a66c8d680a00fd4c3b8eb4b7c470ba2047d090218f83227f58e8fb2b7f08846c8b62dc5693bb3f1aa2105ca0983b8426a36ccc688b1b47ae07f1662c38f4acff038419b441b35fbaee1c08e06cf1c56e0d5744eecd80e3b8f6d99d83a85696ab87aa0d8a835cc0f82ee7a770e2e8419eedc82d734783b0a2c091e2b51aaf262c8d9dd1d391eaf96d9ce55d73148f633ff0efe15c28d0e98996bceb8d46b7c7ef2ed2bc179f6634ccccb7a8050306be78683373ba90867e2e5e9e9d5effda4253c9106268274c79b83aa151f37e899beab673b8f61634da0010c78eb877f11c9f7ff7fbb2e752b7e87cb6faa211e46365cf37690dc0b8d1527ec5e38b15d0de931391a8aaf842efa1d415fed032eee4f75043f37341fde4c523404ba3ba87088b5c264fc787b3d02e13138db0702af3a569fae9ca257cbcfdb107f24011abc2991cff7b3c5a16b0f428467a77958c88b129d39bf6304b55d2794b55c88305926986f92cd53e34b9ffe5b5e556fdca5487a26aece8cbbed349619a075a394d17deefa70a23d4343abae2572032f006f948e16ce36ab11d4852be29e97e29706bbbd85cb7f7b473f48c95f2f944c96076d4117f6b17ffb4c268949c5c4ae063c3c6a8639d3b2be1dfd86433049069e27600340cb54d5d654f13eab8ab21e35e31b2140c335bc1e63ee1b4700fa5224c79188bba9a65779b83c249b0fead0f19cd9f126a965f3da9c150ada503107a77db9563b5393b26074be3fe07745e52de1e1c198dfe0227113e2ce6d4fc306" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "befd853c5ecc99e5b382d4d8188ed6ea0f5b7b74c430b07db9295b8cbe015035", + "proof": "c4b49559588a543d50b44714c131013cfd013f41f3fed8c8acbe485725095918748ef6c54c8023e9da97db94781b981311596ab63378c3c8a59088d07aa1410468526bc18b7bbc02be7bf2addc203e20389e54e429e8f4ace09179ebc366b8318468f51edfebd427544423b1afbebfddcd91ccafae00a0c9a6a2ae8ada482063cc59375c4e7c4f544784ed79ef931301c00b0e36e7985522223c28e77986fe09df5dba915bfb54f4faf58f68ad9d5c810de56fe23a10bc8d0de6f52800c3b60d49f623a20aed32e0d49b431e815c4ef22a208b1ba28b83803c9860aff406d50d36fda3f02063e515d4a928207b3a919290b1134d7442bc1e4495530475ddcf14584dc96f7ce6d24713ec498f005a01cb26933518c2e13394d997f70bfd00920496df4cbeec3a840f8d24213b01646810964db81e11fea90eaea1ebc99aa30a11d24db19deef9994589e688e34e1770ea57afc3041c4f9f7bd160f7163cb58d35f04c01a496c9a32ee7507fb8ac34642370228f89fd853093ce702399e6c773057e0e0ba0c89da9b006421e951e8b0b85bf524fc5c8a58a0a7e1adbb60fab692d7036830d6470bce605d695c4b339ec7a74a06d62c55fb9c6315437b60efb6c458658694027e55a4455855a911a39f58872fdffcf4e06f070936c6f79e9b1642ed241df635b62274c61d35cf3715f28405ba7b24fe0f9a0519fe6aac1c7c5d02d42c53ee381fb8428ed73f7db1ed01266644d2c596d9862e9af909df7087c454832b03ebb44ef3e3caad9d80ad41424db667816d6065d997a49ba856540c9456f70987ac8e4541725fe0fdc081d1691530305abb0296bbb33ffe0f2db228dea29b64cbe7275573159d552d3b0117b10aba9f74d8298dee4ac8f382ab57c75070312b31de0a80a3d0af165708b366439105e854b3b92de377e2d37429a041ab702" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c40c894c49d6435f854bdd871e33f835cfd01b0059816f276d730b61f8a61903", + "proof": "08b53e1ab34a4f661fa099dc84169e6c28c92799d842a36e37096c7016f44826a865cbd50a62ab988a146d3a7fee0289f51be34c12b3b6a28c3145c27b365611823b0151f305db2a0cb73ac626e8880a2abf5386a2d7d08478ef6e442faeea29ea58fd8e74134bbeda9de049776884f6795a79df4255b8f6bcc1824ead0cf776ee1188a9b29ef27c5eb85539fc860d51e355b0834e87dd664edb87843c49140ae1bf4a575a7174b21f5364269e5e498d399e45e3c9789feab14f19f2085895068eea9dbb33c0705647b1373f5a9e932a6c10125de97d01c62cf9479e21cfb80ed60fb43e1fa4bda55035da6638b6c81ba9bad7904803264ef917e37d8f3c0a2e620a5fec5d70c35990ad0870415576b77abd5cc66e50b4595feb1833ee990812dea5a52a9f71ba5bf345865e5081856eb00d2d55f29bbd02369d27f101fdb75fa869dfa5b380ea8f216e7e74aaec8e9b8d85128d97ae2d5b75254e8078104a1f9629f0c3e747a323320ae4d828d77c7e508bb31ec9095cf03eb0d314d8b83101b231c8908858d40a49f2c315c7280b2bb1389b373e5dce6adeffb1034f282040c23d47a9170ded33a0b5df730a8fe5fedb8cba0969b522a8ab3feef2565a4f495608bb1b552ac88502caaf56c1642b78885ad149e1b68f68c68a8ea01aab0f421472fb118a8f417fee0cdebe864bbb6b18f9f10d1cbaaa6945b676ee2fd5e35b50e506effd24065e86c75cd9b9660bbfa3804ff4396cd4939a51c20bee1a73149c36ab6b34a5ba90e4122a155aecf7f1a6a4386ea046dcfa7d569f79c3f03d77aef32f2cdcc031f0162059756efbb758735fdf15b2857ec5ff9d224240aaf039ae62d99605b92c331266ba765236d82416aede9b9191dc72ad7f52b5200ed3084185efbff8fd7eba680e46e14e050d0fb6ff160872039145f83cdb1b7b72970e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d0d1fa5a4013ad30700b610a41c97e8c54f3990194ed52eef2010fa64787af07", + "proof": "f693375cee8ca25d859b2dd531d76bdbe7a730cde520f3e79334d44267e97913b6f77a4bf7d6ee7f547ef0c3c68a701afa0fef675415b56d66570d1c0ea9d05e407be5b5616a8fc670894c72da92c87ce2c5350b0f02c5ff4d1eec30491c38024cfb615cc64e7c5b65e8459321331ce1e3db3375bbc2445f83ab3bb2421c9a02b75c70b3e47d0c7887226e7c68a87f1429678f1f992bdc8c7280c2bc4a210109540be4b0eba348322b77125660b277483f732050fe1b2981410ba5de4ff34c0e3a8b31b14d5bee44c1a9c3445ca80ab63974585b15814bbde665622bf5a6010e420ccc88be72dc0559174930c5b8f87c36aea8974fcf9c5fe0ced9855a6f22730464f2af8e4d2c53d09885d640efaf301baaf1e890fa4852050b901754749760342a0ad757a10b37c9728600d866955936b6e3afc0990f43ac413693d13b4840945751f88a859c4bf0c703eede7bc05dc124f429422cbcb453c9b73ae048101cf63b395ad19e928264f56e0e22484e106f6ab0b3d83acbf4f1cb2fece1f5577db83aa6c11522be2db0d192dd5bb181d101f4e4260bed178870df55ecd4f259669897b1f74ee6c49999889565d06050d303204c2e6152745c4438877b580b016c64c26402e00f9337bc6633ff168e6a506edabc22afd8bda5ba94d411dd144a722c3fbd8fcef67f40d38b31594791a33fd2b2fb6084a55e298e43b9c25e4a9658f0343693dfd4a5920c19c06d6f5f738a28abe640d75b2ec334e5f3f7659a9847ea81be31668999dca8f60cb3a0754b51190a44f8b0d1429b92a83e6626321d146ce41b212d65f17f68c85837be43a1f35ba5abcbdd2845422dae1275d06bde28cddc3b7eb995b0b164922eee3bc9f40a1b7d1eae6cffc6bd10525b10cfde1f0c198e5da259ff8a1e4a4bcb9858de0a0b58c51ba6c1f1fd7c4c4972a405e9c905" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fa8209d1998f1f8e6a0949cc10548c8b32c0235815a9a3f988325eb6e0473b74", + "proof": "8a69189e826e3fc8e78479aaa0370d855310b3a1f7c400d2955a395c7b41b05fb672bb8dfc34cda88a0cc2e9b9e6a9ffb9c0cd42e059b3b0904e576a7d08ec6196a0a2d3d34785f743bf262b55a8b89979344b7ff2f8268a0d1053055c4d095f5cb2ad4d648f908d9ada729486a982e3edca92f758c78d06bb2c7478748c922443edaebfa653127045c20c80cb7c30300c3b594bee8157603a2ec2b610833c07df5d41dee3a94a01c142669ac469378cdd0c9c1196f9c75414d5e3e61da61708c5fad5db2de0e6ae92614a29e089397e83ec4ba6a4bd445940d7b40f768e3c0bb29a03254d77032718120a973b2ca0acabdd42e1b9fb22bf39a82b55e20c24176e9af50ac3c0d95634fdf91a0143ef4ccdab70ca9ee98270166f71dd0a46673602d4fec5817530c4144bf61c6803a24dfe466281e165ceb08f9824218b439f11cc4a943e5da54503c702e7faeba697457671fedc54f16136fae321f3a46884530acebdabb42f1fd2bbbba31a67312f8b48b9eb5838bfbda06b166a797f7ca63b722235e79bbc59397ee23ca787eec8e21ce97fa95dec90236ce6290e886d0875b4a2729def79071f4a48fd2cad3e1bc24ec23043e08a4f6060928ae093565f4b7aa06822c7e941d1b2c9240607ee5872c9ef122cf22caac7a94ff1695545fe243caa608a35efd28384d7332cad421860b7442e2ebc1fa49a2e2722b799eb346592ab06c14fca2137daf589fd1aed123066a008132be05f39006b5e9cde6a97228ee71ffbd335f2ce1120156b1cca2ce8a86ee69572b3bdb390b676bdd7c6c60b6e5c301e4d22aae2c7feca514b6bc05cd65314d12e4834ddc331689d60b5e95752bffa8ffa2aebc1eb83ac6918d0733abae18f973ef46ceeab40308fc53a0900b807433444bbf90df5af2509e522f05b38445f35cce80d3dafd58cfb8443ee02" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 9 + }, + "commitment": "2601c6458faf644e71550b28a6d3fbf262c650fb145d4b9864abb9c59e3f497b", + "proof": "9429ae452d8476be2442abe977ecae5e61feb9dbef5defd8b1cc358908715d5dccee07ed9f414505262039701e4455762ce72d71f5be7685032ae21af15dfe057a51af9dec9a1afe926d56646b66748b94bcbca0d2a095aabfb2de2f65c5b5568cfba174c0e98e8f880332790b4aa6f6c52de2f97947c259546ae8c9b6d7cc73509b7852ee9adf5dae29e2a215e1a158d38f331cfca111076ea10b9a812f340aab6bc4af3c6383f167016fb5fe743dac89d1bfdde5aebf7e3b7e7e51fcc63606256f5cbe7f66ebd1f9e3847b83d39018a42190a2656c7a8aace1789e26a4e501a237abcf076efdf888606511f148afc2a7fca66526403deea4fccbfef3b79200e89f2a5c2855e3f197c81021351f29c44fa6cb3435d9378f7798014c9c68445d309d5b05b4df299e2e94a4cca98789463bb1d2465fda68bd83e661f4287c0152768d6149a4ab746e94a0f079776387d3fe3a3cc0ffe52603058c1c5aec367a3864c1605321b6560e942309f035a42648bc47ce9114cf6aad06045d378933a116b8735b44eb8b033a860dfa21f95168bc1bcb46d4f6ef734a71edb3db1397a269a0098278c51bc74747c4de51fdfa5872667fd6f4f8145477647131668595616ed6cbc162c79dd105aec1db8a41b339474911e656f7a1c360ff58e9088e6ac160fee427a0d76ce1ff39ac0458a2e23ac2241c247ddb741cb5ee74aacd344f142202a01752f630744e892b8b17a7fba0df9a9f5ef5290cda38570b38110ccf3476a8a62b90b4e30ecec6d47b6e1f574fd8e63ce812cf5eabfe7fce0eea15c88757e8f584250047c1e1d2d8ee2dd538c1e66bba6485fba6de6c0a6459c177d87838bf8b0ae392db05f01427c38926298c5e0730cfa407fe7993106e50e4db86980d559dcd5a4ddb6837dbc88111ffa1358ebe9d77052398835576c5968801206108" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "46eccd5572aff754a6bfc4a3510a081c7e6a3c5e817fecdde157810f357d3464", + "excess_sig": { + "public_nonce": "d6815e3cb1d45ed0ba7e236df455436cc5e7f034d4bc29310412f7a1ac6dc945", + "signature": "9adf45f66ea67607585bf90a6f8424e68fe514d4fab63527b51093f91836a30d" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "6c146de5d8ef668d483c4d7ade17cb215bc8f9e8a47b3cfca3262e0e9c6d9f50", + "excess_sig": { + "public_nonce": "221408367671d5c85026e16c8f3a205d64aeaf5131564c62f56b6528a27b1742", + "signature": "a48cba1e4adcacfc00e9e773ef1d064725fcd3277496f6d27dfb8c470073ab00" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7eb6cafa0e81e65bba764136eaa7635737da0b98bd9a0ac7fea7c561b76afc0e", + "excess_sig": { + "public_nonce": "0c7c4bb4ce705ec1191be1c144bfdeb20d287fc5873849b280b97be7b1ff3109", + "signature": "2e13c58804f7c40682d7335a1e8496d934db2a92640a4e055dde0bfd24689708" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "c238f9c216f122c362c763892edc0889cfacd09d22b14a279577635d6079c135", + "excess_sig": { + "public_nonce": "8e9805e621060ddb3da35bed4ac7a32f2bae3bb376024dd99d28071262e7762a", + "signature": "9d6b57ae1aea9e1cf0569bac3eccc912e55ff4c95ecee0e785280ce54db20c0b" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "f8caf0d35fec5cb1e2f03c21ac6db306684982d85b85fb693bad289dafb2af71", + "excess_sig": { + "public_nonce": "163b5eca5709ad348bc6e4f59485d641cf2418cd5481f0eb24d11c94dd63c34c", + "signature": "9800f44388946a6d84b3a96b48b0eff1b913872e1b8fbbc0c7014326894f6404" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "2ac9b07534022494acba41908bf63df7abfff9a25cd1358236655ca9becf262d", + "excess_sig": { + "public_nonce": "4c0d87fbf7d057de34bfbe72c3e34d11c578491c491a9308689417fddfd4d560", + "signature": "8ffa17a9986aa27e38303decadb7a0f47cd64f617d0aaad1c9e4fb3212834e06" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 9, + "prev_hash": "b69023e28a66f9689d4123890e6cfb47e1f35a0027ed12eb62b04f2c184d2552", + "timestamp": "2000-01-01T01:10:01Z", + "output_mr": "9bb9c90f1200eae82d171a157250fc87957419a1dbcf7677c176a053db59b34b", + "range_proof_mr": "bfa30589ff51953d1cc66535a43aa27846d9b3534ecd8d819f9308f48be91cd8", + "kernel_mr": "faf63a08fc3788a67e53dc49fc43ad9c2111bf76a874fa4773582c58fc8b4755", + "total_kernel_offset": "35402b422d5f54996265f21d34f4ed7dc8a479daf9bbdcb296e540972f0f6906", + "pow": { + "work": 9 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "70f44685c6a164573af3af966cfae1ef0e9a722ea7f512861d34cb6e9ca3a94a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7ab0ea6ecf565fd05e1385c7babd93be8e31392429ec0bcfa28222e577d40f0b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "befd853c5ecc99e5b382d4d8188ed6ea0f5b7b74c430b07db9295b8cbe015035" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d0d1fa5a4013ad30700b610a41c97e8c54f3990194ed52eef2010fa64787af07" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 5 + }, + "commitment": "9a0b7f372fdb755963e5fce6f0f0bfe78c39594e7c5dc8d05b41bc42d5467946" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "008e1489381ab5ad8e5a6e597f5315b8e2bc9dee3429be1567b39dcff3f84853", + "proof": "06ecedb924f86ff3f99d3f9dcf737fe0c401722d0888bb68d1da9b2b87067252ac5a13a661a7d3881d949857952705a2e60c5775904e3b60b21eb21c0129d9049ecca9c1185d508824322561628b88172e53466164a1d3290cad5a8dd551326ea481581814ab5918a66cb87538da4488ee0cb7c519a86ee8e520c476b309874432f20d550b0c9154bf7d8082524005ae4847dd836d9825b9674b3f978964990366b981bc6bca594d6f65559450091a5e3e0e0e05c975a8fc40c5409ba8dbb601cecd9faf084e47a7b2475fa3153ae700950cbb47e1a75071b65f93e61319d20d82758f3f9978b60405ddbdd90502e7b5f4f8daa065f0d6db0189b470c09c06495cc9cef5431ac8d64bca8d1f7b1b6a80c06faf626791301589665f49d3f8cb7650ebce35a02853517f16141848d570633e201935c01bc92fdfae76991fa24b5174901c18ea885d349b0a6618ba7bae836c8d19f56f68a8b9643c9945efb4577d44ea4d338edebb671bfa6120d9abd01819c47826024995e73089f8320112ea2962c9388da695feffb7807ae4bdfeb216a83071f980ff6910de6acbba1c1f4169f60320fc100d25fdb89e2ed93cf8cdf0ec4be4514a95af6c811c6f382cde673b30d1e7f6293c5c580d22b28e360e7b48d1c98c25746e9fb29da407854acf44609c97dfca7dcd6bc9a253c332b3fbd7eff2bae5fc4bd2afc99bdab8dd56d8284ae2a93ce36c27bfc9b11912bc2dee3c522cfe8229e56d3fb872e5d15a0f70091cc8044031d29ee4fd9f49319bd41045237093bcac48437fab6ded3c873f09352bfcab98a3f0aba5e410e753a97f0725d1feb03eb531248d39fd89635588c6257b5301a64bf6586cb2bbb67947226e50a4ffd6c65bd5db552f28f04d00470f9c0a2c546518c337a6adcbd4c82c6254855fd98bfe809b8bf4465bf023346f5e6d0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "064b9659eee7aa620b2b28f3d6544a04af32fc60f018c22732ba115b63289e5f", + "proof": "0a34c9eed0a7896b26416b58b2a837bcfbf1eb47d4f7d8a75010c6738ef6070da6b94b4cfe7727d491ed35cb4c7767fee36f35d7d57dd09a2c697ce2f1be350f70c8d720cf9c9e46f4ae8933e49c67cd4b7fdb0143af43fc5ba05a4710a0ef07feaab0431c6d0c3a6a54ff8a7a0ce296d1654e68b33767049e08adfb68b52841ca46c3f9b90c93a55b412d67476f387107de5725bea02bf7cd9c9b8392933b0963d41b018a0f2ac8484a96b786153442f96871f0e5ecdd3dcf916ba2a198e70ccf2a4a1f5595fb13085ec9a3d377aa55f2ca7fe4bad61405540fd21237344f0ca68e82af93cd5a7fb903b67d98c8bfde49c6ffafff830f55e77cfd407f02760ca8dd4a4c894f7797f4e913931faba6002360601be170e2bfc8a4cf93b80dac603a6e629082005b55b071efb669922bf13532ababc7bd82d36c03c5e61316dc6cc08b59d53dc6f3f65180618ab4dfcfa62db26421f924b03e1ea3c7e6c616ee5a1c656718209fdae0d82b82833fdb3d50289a3678fb216a955ea201f1b4d7fe73eeccf5ae5f42438fe7b033f6598bc1bf029c4739e6c88e09a7afd21f932f436738cf4f8eacb1ad10a1bb8bd3aea31d1e970587ca85b1d44f0bf633e8b7273210584e38f5dcdc8e45f9291a0680cc52a6061cc2dd309ab8849214f972941b5427e017377527e6c0e5a73c1637c5824c714472868d309035b9a5ad4b206a8303123c1f7c0857a1ad2998342be9cbdf499354dd9c0e32b5337f05d6eec781621a57b278a58cd206f409f65938688bce001585e6dbbd849b4ed4b779b43bf9df99722e433ae7fed22edab918554b1f1445bc9db5550c09808b554b9997958ef95c585b5cae91eda5ec2a05576ad829c953371a843e16b4cba55d988e5bf7bc761306b04c79058e426397182a38db0926c0167c2a60e9b5249a0effe91d870c91700b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0cb189ed7cff6a5fd188ff02acd757a40d21f1c4ca51ae2c2dfa57640dfc2061", + "proof": "7ab458d8038cddf9080973692f17bc36e2cf1357a2c073742a017086afb9784afe628064f3c5c8868088e47c10199dd8b1643c02fc6adf3b66aadbf26785674bc4fa95003d78972df160c61871dc8e007d2ee0c231f5e037e9468832b3a30d78c4bdd82d20c6444f5a9c38781b3c4528cfc33436ae1be9c6f8b4b41b944df123fde8c28f06e923b44bc47f971077f2184c8ddad3213285b3c4461e7d4041450d7980e3510e50a84606b56e035d44000737f6a0ba2aecda04125db2eb97f6910fce815d83a7e6da01fbde2007e7ac15d0fe62d5bd11928bcf501ff1583f4d0c086899bc6d6494909861a1b2f95466dbed3f841cbdc4f390d95e4a675ac3246301bc3602bee86cda463595322a03e7cc75968f89482d073cde92c24af099bfcb276c1e9b5d8ec46fe937ea5c41a5c57f3eb92aac08246805ac72c85d3196a1f34ab45cf8b5c7731a9aef40ac31c588f62e63a4246b4c1f58815bc235bce01dab74e8341a9cca4f5eff7ea6c76fb3ef31e114b311979b91e8b00f7e43783d1c3d74cc229e6b9a99bcdcbe16f479307d529fdf344f4ef9ead220711472d890abcf64c8dd85a740b6cb0e68bbc446623aa7bf754533e49d36cd4c92abdf632fc066165ea7ae5e9781a45d11b8b8989e7427f5e80c5560ce1ec1141c289f91cc828241528cf0d03407945fd83c0d9bf36bdf8ab8490d7552cd7bfd1c1877688982154ef6a2bed4acf4ecb6cbe52de77bb5f628c1b3ba91bcc45c5847fb96414eedb01866ad78843c42100efc2e16cd3b61bd9b5d8ac2c36eb81efc086c26ed23a5c8258ef6542ee3decceafb19ed0de466033f27a661200c1748e2242e08e8b6109d429ec14511c03b179bf8bfb8e05e71bae3f9655976312e3295a4ccfe03ff1c9a0c28c8076f8ff09ef9bb159133f099a1795048b4378f803a01ffd54e6efc90db07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "42b65274d2b594f6c75bf436f2d346efdc0ef92be9e89edcef70fe16318ced55", + "proof": "7cc2e800871badc56ecbd762cdc4173d47234e96f9ff5a07ba8bd016de427f34fa2e40b6d36d869e3460e5bbeab2790c7bbda96c9ecbb71fd7c2154cdbde5133ce1e01c39ded35d31fe77d03df4e90b0f3987eb5c9b9d1fb403e06ae76aca168ea07dcd7545cab61e4993b1096254b74b656e279af01157d48e34afe70823e14eb5d60b413087a5c34881a4d344fe4cdde73d86f57a4a6fb2c22eae46ccb96052e33722a5366029965eca94dc9c036169afb020a9b073a0d10fc7c4570e2180d457b42054667e72ab6851d41c665c2ab876b34853f67d877b2eded51e05f4e03d4fc0131cb756bc9a5d14116c5f412a5f06d092dac526e98f68896b800068b7c90969a96437bd196f1c5e3c412ead4ff9088bcae40d7bd337c342eeb31080e6718edf367de128c5749664ca43753aa3a0ecef5c997a78e4be19cdaab4cb4b81a306b34a79d00291d7fbb090ca8041f4695fe9ce4539227b50e2de1a87bf22f2940b5b52222d1ff541d0feaf8ac66581bbb46b7339434c8fe7d0896a67384027e0e9875f7a3c6dbea6734920a5f157b227e1f2c2b3838c009452407d6e2e7ac7b525af9126fcf1b0cf3a48a9a6b4f433ad8769eb2b02defaa6d1a3ba22ab9fd5e50ae67ddd699f87e4b4aa6a8f82ec6f3f5242772674d26849c899e0cd3c15b5642861030b80a996294a187e93884e4726dd29c52c8658a5a4b359e19cf2b845a1afbd95dd643294d45bd9244bdc993cc1997f82963b53f2fd465254a52048a3b26cccdaf061a1b8ebba09c36280dd4862fb2daed0c7d76002a35971face0e31196cabafd78158c8c0c3d0aadf9a46660cf78ce36400b15de2032d6cb3f29a15f4ea666ce8aa253190f51ac218a7411f3682b6c5595eb68b1bd054d8b1111840803295819d5dacce0cd314ca4ddc1be6e590dc2732e9bf40d05c80aeb1ee94508" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "50b2cbee304d237a4fe98c37b2d9079257fd48e0301b603378bb563616acb00b", + "proof": "809fa2247df277b4a69a7d87d584b5a205ac16abaadacfeac0e6a5010201245bee1214c49a2d76d3dd4a29e43bbdfc9bcc57a2067931df46219fe1e83a748d364066a1d8a37d1e2b0fdcbed72cfe874da547c85c88ce1b14fae7fa76fac4e67ac4e5b200a582a48e2c12ef0d24eae2b5edd0568ce56cf5bc11e38d906442c339e0842e20371213809dfe08fac291bf6698cbab3cacb548bf947ce5d768c9be0215073c0461eec675e5896a1386dff58c88bf5a9f5c9bbe1ae6220c6031135002c3772789b0ad02eaad7e9acd3ef888d365dccc206ffc8597ef1feafcfdc9df04486a353560d2dc6bffe9abc69c0644e268b7cfd2dd410df10596140a4028047844188738f480731e6f08fa43ed58155a56811a82d62c50d9c9388369e85eb97fe8547d2a9ecee863f864faf1c6a93aeb5f38f42822a4712b9060f328428d9577b4e76e59fafb700d3913fe13d0c8b72232150a9d83defb648eb35dfed0e71138c2273b9cb612b500c67127acfe8de07bc39bdeaa037b1de81c57426a28ec1d7430a50f4257c70bdca6a52fc8cff6599d76245b0a65f398c37d60584d8038867c9ef83ba136f3f9b8d2876c99c0711f553012ad1892b551c230bc94c5b1a5907e1cfe6177c1d9c11d1cd07eeeaa3fe285be8c386495eea56675cf3237cfd4fc0038f3020ab9327b2c58ce3c6b67da64a154a91aee985ec5694a92916cd27ee918407fe8a898cb6554b929f2846a57d47487d14192a770e0ddc2c9a9508bbec4314820b8e071e8861661b2f5614068857ce4f15373ba19fcf27d4c3d4cb400c523dcd0c07f070166c656a8f7aef371619a6fa78d22d211acdb6d2a03347237593646fb8742d8e579eafee1fe5680b2e1df2f56102f1aee8ab0d6dff00eb11d3702bea8cc3de6dada43fbe4ea5dcb4612982a02ad4473933d2ed884833ce645be08" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "90ab152f8f0aa42e40c4b8c14444dfdb4adbeecab5afa6c8e569942dc9c7e97c", + "proof": "44ee26e106d5dbe985092e2b49469c04cdf17e31897b262259c9558561509f018acd19432de57a1d367ca19c2a7c7465bbded4c6e6ad1ad288c7d73a99536e55eccdb75b87fe397eadef4c0d772d7b92d00154d3079e2971fe211c159290d638903d8c9f6690ef84f5ce3383a0e9d2fb2258017d89eefb5aca3f5dac49511d029d28fc361a8649b58e39927d73765337fa8a9e7552132b47de2f5c42111e5500cd0e382c71df1f8ddb84793ff864ce39df17bbaebaec0033b11d6ae90574e708884ea8f294d8697655569474de83235b58c2d6e387487249e98c7659ba026f0f90e145e7c9afae7e91ba53ebde84bcf16283205aad83fc29ccb5c268bf854001bef8e4a34d18c6b126313fe008b5d29ad7ea2ea9bf8bbea4eb225635b1b6550d26812d392d1aed784da202ebd9656826962a49a38a57198de9e70f540474df7fbe51c3f0009fdd329e71f6d99e1ad4e36350a131eab610521a682e4ffef6506d1ef88c567e9abc6ad3fb1013111c54ab7c0fdfd8058c44094e5f71697513126baecc133ae032701bbd42b4875b3f4da2bd969fd7006d66c4898e7bdb88763e2c8032a9800f7583c02aa60b1a715f0b369da761d55c05451cd42fd19fdd36d359d29fabfdd85df1914d94e58ba016e42243c3ae4565e8ea39a8a265d35289a10928630dc9019230de3815f0d0f8ce3dcea62f8833a49926a306e01b91a1aa40789c8e61781c1015846c67cb649b44d5bc37e8ea4a4b893d51541b54b05b5d4a3dc84b390da37acf9abca30fcad1cccd2809aee6199f216f2f8782170e35db2062de1a217432876f5c271c0bcb0a54bbe5cb1cd77c50749f87340d69b21a128c6d7e97d6307243802c84bb131eaa429de386bc7aaedd425faac8cd10977922df003fe0cc00b7cdbd998dd7e1eb9ca8ee6cb1a76763653f0c33b4dc1acf8df6750a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9ab7224c1f2deb0ea32f3d99334b95a6e69f7f3a0532d408c9d63d5ccb424f1f", + "proof": "d453784f8a96391ee287d953c344f2388c5935026e04ea9c672ab99dff47c907ea0ba82fa07b09a671bb1b141569ccdb598f79f9ab331672471b79093c77fc77a2c24a4f6057bd379b882b011a02ac0335a6cff3af0acdee139e0e3e080bc63d78cca53b87c0797668fd0d92fe216bba13212239e731b008936fae2e55c80373e2ad9d5317b0b768136632b706c50c412279bb32f0a28c9e30c991a232367f0abec381753a3abe42f4d24bffac5c843250ad4639a0d8ebfc0a89fce64dd4d00b423c202350e73ede6e3b3ad68dd1eacc9597f61b09251e123900e1538adf490d84d431950cacecee718b15d3c5dcf935796c46d434efeebff02ece651aaa706ef8f833bf0b121aae4efb861d12a75db865acda9e2fa09157f2ac6fb0edc21d4b76fe301d933f57ae6176b5820acfe8f75c2b40852484f6a78a4fdc219e2d2a4124c0c997fa9fb51a9f923986ed7e48d84d72bdf09344fceb4311c417ca03fb7c087b540412ad8253d4dcdde46eaef95c09c1b894122b20347bcac1afbfb87c1cb4ffbe9d8fcbe59224a510e76c4fc48d829ec2c0fa46ad778021451a21902607a25902f64636785bf9f189dfd61607933d72d915bbe1719b1dbd761aac046b0fde595424314e2fe41aa7ac7969b50f332f51238221d5c0e6d2029e63df625b1f52aa6883be6a45f3d7d5c4a5d2a11d257c71c07ae12de2b768481fe0a0a5a945044555fc92dbf514250ef36013755a86083b84e744ce6ea94e8eb32387f4bb483e3e372230db8c69a645cca68696c69fafaac507b2c23b0c1ddddf3fda0a857434810db367bb51ceecf453ac3e2961ddd9b7b3a9ac0fa0946e7ae882eaf505149f793cc4667eac7814873a693a98bb09f4735d2f494fb369d47f2842e37e4b0c50d0f9d51f1bb5a336f84517e8c2889b8d8946888dbdc84d2272acccad376a08" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "dec93694870d0bbd61208f37ca76bc951c7d38fe6117de9b697b8d6bbd82f25e", + "proof": "7e22e89a6047d7f15bead79a1da3d6715aac5c2736cdfc861c90667f2daf155d5411897774bbebf3e8b2cebb542a65940270dcc43071ab73e73e6cd572576941c6234e80c9475e12d9b0b81f115968a53ccc7d007e683911518129d9e7f21554647a352c319c7da6702ebd26bfbd2d8a36271872b5c457ce0b893fd13bca167587c73f1764186bed4d7e4d571de394d541b4b6533dd627259145c874e490710f219b8921640b80c2ce51b4baadb14274d714dde13fb63fbf1d8d32565d0fa10444e243690349247f59d2180e833c0eb1f2812de469cd085947f8ce0be0783f051a02f52288859e9866ef1ade7a2eeeb41d3e9f3d61ad376086fd6c718a2c54301e1a46088f5ec368600205cb651472544e41bbd07758022a52dc684010d2c452faeb0e1bf6bb754abf025d4da2d6b0cd1c6bd97f9d4d69be9a45c5825efec60f8e9c41ec7944a4c0e997db749eb71d2c8ce3c6812344d16ce2d6cc6a02f66a284a55f6e35cc968e9c62c89644d62f776e4a6ceb0a442d2330346a4b15ae9f5607891627e5d7f205dda1bc267974169e26aecbe32724ab1912070b801953bad29e6b5c16dc3c6242febd215cbe804f33ebf5c427fd16d06fb0f5fd55403ca1e7748972b2f81a8f705d3616ebb20611524970343b85fc3fae63dd9705a4c10a75af208702e035344ae8fbab8e1bba91735859ebe1158d64679570dce3863e93b29c201e637fafa411e16d7799110bb2c643e17164e466f6158e080e1503f4b8d77fad7c707b49445e5d7c7415e9b1880eef16dbfec2976d6bfc621b798d5acd7293cde650490fcc64bd62b3cc10b71f1c3593a6e82630a4add2ee4d2d0dfbb2d4fa7eb11a460abdaff0de8e667ec0a429afa719aa36618e20ce660426c182f900663924d2d05f324fca4a68fc8c19e37016391703c71e8259c48989c9a54362306" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "eed51fa3bc418224616e3048472d1530a33a3aab028e49c12329bc70ccf42f3f", + "proof": "06418023ff6a1522efa22a18b03a76bab97a2e6507894baf85a344d10f5ba87fc4880536cbbbdf502bcb285c9ee37d382d8ac71058a2846b78b4505d4c527429223f0d45ccc6f63ec8c106226f7bc71d7e2946b6123f2e004423dfc75fb1141d561481028596e2b5380083386b6fe236611f968385be0e186c5439affd4b5a45e66d4f77ee00d6ac84e5a7070e44e22f0b5ac8a6139d69ffef0db05c3f95d00d732b07f1f589c254f4c5586e118dc835216e0da665140af8f35501ca1831f90b55326b6fc16c5e1304915d2259be4da59cfa6ab97743a96d80f6d8b85378fa0e364162564fc7b17d846f6908e308e5b43aaffdf646880e93e8f8bbf86a7cb813e214b50bef86315bea53637fac87482878845c23012ffcfdad6884b6fbdd6c11fa742ab5d295c9307afa0aed5e25b52b4904edecc6551c86e03b50365fb1b4441e66c57596c911c552250d5f0a08f9c86cbc9c9669b4e9aef91af44be7bd536f56205d78b47d49cf967a6fd4d2d8fb9146c4f54b347e65ca9c598f53fa5d3a127e672ff84b24b04e9f0f0666d45d3b9eeab5ab249ac49f7a2b9a009d94a4ec07980b7d32375903bdfc0e85b8637ad9972eded9076b77e32b5222f55d1ebeab0e905f257acac037fc327347cca20cb8b6bad2b9c013244597303e7a739dc4052b48ff1e31f77866283e5aa1edce4f4849ac0eea74a84fcacbe1eed313725d516ae4ab6a901c21bc361514f3ba31b854aaac5498939e81c6a93c32fb05d428d0286ed30c87cd0d4068ec97ac08462987930008216af6e706794754ea016463436590e0135f2eaa6f7a0f9187d2370ffa45e99d237b89bf82b19261cb853d806404d843f643f7a1cf907919bbdd90abf39acf8dcdd6f5e182224d06fc09ef62b70ded90da48d46e5f8f75ed5fe2fb3b79b642325e4b95e107001c550deadf953807" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f67239af0d5c5048bdeef0dbe41811ddc2cb9d450ae5b8aa07fdc949990ba501", + "proof": "0e57afcf8722082f2872660bb1fe0ff62a26371b945496bd2c6528c753c74517ba50de08317748458f36fe62831c2cb03fbc39ba0dce3525821cf63de98835451ceb7c0465a334b9d7e020c046eef2675dfe9ccbb10ef741b7b72e7c39dfec4bf0c8ac8ccd335ef21d72bd2b9862aacb10fd4339431870d85d2620b3916d5e3af82898bc77504c4593f2faa3014f18afa5373a14b7101a2e9dad6676a956aa001713bc165a196d729f99dfb1e4a3c6d20bf364f9fcfb308ed7cb75fde7e82608230a8de9916dbc85bf0aeaa666def95d970b6794dd4516402849b53c8424e003c66a9bb70385e9962d22df6ccc9cd90e5f0e281557c1e3b2dde0c49d350a2b09e4ee632d83af97252bc4e869fd96de3b944d3cc6719a645e074ec9e2a95c8f04c28e31ced05a3ccdf4d01fc59d311b58fb076325839947e88e9c2c4636ec2d3ad8451b1bebf30770b670e1a2d6d7927081d996c5eae2e6ac35caf42c23717d26bcc678416daceed14e0b7d3591f99ed7db13b0cba0aa876b64e5ca0336b2ff71ce2bf7dc89f8b0980a397a6714a44768671718089d7f12ded8f7e10084d671678abbe11415885f8aa4f65b18e12447ecfa80e1a0d86c38c543095ea7e2d588771c3a84143f385b49ad3d4bfce3cd2a0c250a16fc712af227c3781f85d6f0fb5ec2f42baf5422b1ac6f844aa6599b08923620eeec66e61d5fc66f78b1f4e23a55de06a5cd198a1c1bb62870106619fea3709631e823c05d1a84694c8b556b2e597897e97bdc6e5d3c2af6803499afc3c1ed7f2dcb6a47f7706ee97cbae5091024365836910ce117b9456ecb3f504acdb182b92dd58e505dfe59c9b4654abb4610a1b4dab2b05746efb393532a3288dea5d2b154e81f4ec64ebdccbab02fdda10902c9c3fd415421634bc8230375ae64d5a3dc9f0fdea6e16f17fd98774a0ccf0b" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 10 + }, + "commitment": "9ef8501388da66549aff0bb1cae35fe8a4ecfc3c79595726fe96dc21017e6a75", + "proof": "da075d3df5bb0b807ffea87e32796f336d2a617efc1dcfd655030a8281cdd66d7c91d2486d2db9c05ed1a8d602553b047617b6b91fb3426bbb4dc41dd4aade6ffe768e85f79c7ce7af6486c55d25c1124e3355bb17b250df5193985d3d59e3544e1ddf68d721d6360336a237469dd14d7d65d48644de40370a52cb7f2a17194b21ca0fb127e0788827edf04d001e90c8b3241c1baabc322af670caf95fe0d50fedf100c7ccf7128457e37b44d755151b17fcae5281d6bbde1ec31c709d629708d7e8a731265be4c6e753e04ed3bf2f747b3d8e2b2b660ba6c1fd4fe8044dbb0b96b834875d21d4eb6dcd76100f1225e08f831d70934385276bd74477b034177f20ad0c301b8dcb3de3f96f1181fd26ff3a5153772d4c768d64917866558a253996656ec85d6bb59e9ac35a6115ebec86a2b431823b7edd3970b72a073f273b2a7ac109665bde6340f2b75b5f8bd0ab24cd710af4941c834ebc9accbe4e12e968b6042986cf3e13bd063ee5f48c43e1965bbc88b4a01af889d2b8dbaa2f9c43175cd89daf16e7980d5bc3d5f65eaf23dcbaf477f1cac616bac92cb5829a33ac697ed679f4ee4516442207554953ecf6f33452ff25bed5f8adbec28ee3aaa5a568fc3059dbc76011984a6c5509c4d90f2faee8e7558b6522189f25d28e9680624ba8a8227bf3acd30b29ff759f505f003c9de25a8468e96b68d51cdca271a9714566771552095683d46052dc3b8da6e62fc01fda6007ece5d06bb0ae7055821f519c014b40fabd3aafc2198c2b6ecefbb84be7ecf62adee9d18ac41f101aba225ef65a22e3459663b6a7538776a43cab6c9597303f78481024c518ec859d6c005e45850cbd8cc34e7f463a42b1639ef967f9dfb0130fcb07f4dd2619c78caa200cf2bfdc7dafa7c59c725c46c4792b1c70eefe8aac5b41cb273c93664ec3ee1e06" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "34397f307e22077eeb3bd4413b76acedeb4c1b471b3ddf8a703b07febec9927c", + "excess_sig": { + "public_nonce": "58b1fbc61e8a2175126427614423c60abcd6231ce29180336b25ae39ff9e6924", + "signature": "c709a900fd4e1856171f8b90aa7a964c17e847b0a3cb83e97a148ba933878005" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "369f9e7f597dfc380ab5b5c84a38683ccfdfc91370069e319450701736db2a79", + "excess_sig": { + "public_nonce": "96514e3f65cc7ac785dc3458db1c72c5a8542b78919f9bdd85d253c7e2f9d877", + "signature": "5d659d904f9bcf41193619c128b9863507b7ea6d00d287e3ab9344ab6831f803" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "543cad0c4a3e4288cd08e4a8f29702dffedcac9201ccfd234610d5d44072d33b", + "excess_sig": { + "public_nonce": "9c3ed035c24b35037d642e14ac12f0505d70f8c28f88fd894853029fbfe55c4b", + "signature": "931bec9ef73776c9158ad7d53e72f226573650736c2b20a8df1250c3f2abd900" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "96437dd99010c1e2f97d0df9e4c396dcd21e4fa6909cb3d6aef010d77ac23a23", + "excess_sig": { + "public_nonce": "ae52b575f0fc2c5622fe248deec9147d770d3a4f6fbef805bf2645c79af8881c", + "signature": "7f2509aafd27539c0ea584ea366d584542f8fdd052b3b32afb906374e45ed902" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "bce8073fe2b71121066470520cd650597b27125e9290197d4ac224217afdaf23", + "excess_sig": { + "public_nonce": "1c84dcdca48041933807088fd41992324f3ef2ff7681a80c72917c04ac720d35", + "signature": "84162d9621c76604b097a855a6c0078e0aacabc501fbdccd6f6bcbac6e992103" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "5adc945d4ea17fd65a2e2ee054db2c4c748daab0088bba9996cbcc416a5c9403", + "excess_sig": { + "public_nonce": "2abaa8c5347ae70d0fc571235e3b3303b8991896a3d4e44b491be38bf13c2c45", + "signature": "16746063d96079f50492607f679300f0db95e21544c3302ce326229ca167cb0f" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 10, + "prev_hash": "38b477f43a8f5f35b3fb78adbb1f452c55dea4c681db144b83581f0d925d8ad9", + "timestamp": "2000-01-01T01:11:01Z", + "output_mr": "cbfe7866d438fe47485842d8632a05f3a37da78fcebef3a8f784bef1e94155b6", + "range_proof_mr": "b6db67d3055db2d0d5bdc77d8d003fbd89fc6373f1ae2e097e5e33d99764299e", + "kernel_mr": "21d776a206c3c9579942bd31d761fd511a3ddb1bdce0129e131e4fd98f76df45", + "total_kernel_offset": "341293acb3b4d785c773b6d8a94b451ebae3d480e79ce32d0418870da7341a05", + "pow": { + "work": 10 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "008e1489381ab5ad8e5a6e597f5315b8e2bc9dee3429be1567b39dcff3f84853" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0cb189ed7cff6a5fd188ff02acd757a40d21f1c4ca51ae2c2dfa57640dfc2061" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "42b65274d2b594f6c75bf436f2d346efdc0ef92be9e89edcef70fe16318ced55" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "eed51fa3bc418224616e3048472d1530a33a3aab028e49c12329bc70ccf42f3f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f67239af0d5c5048bdeef0dbe41811ddc2cb9d450ae5b8aa07fdc949990ba501" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "02523e8fd2e00ea3de0cb19079f0c19642c5a2bf1973e68261d88b0ed80cac21", + "proof": "0caff1aec0948f27f9db2b7811df501deb8193dd364594f7485400687d54f46b98a72d3b9cbc140c5c65602333063328aa8cf5ca033a32776c35945de79b9f0ea46138a313e77e7ffff61b0ade564f578ec5d01691a792f50a21aa20a1648b300aa433c347ddc60635e0a13d6b3f11a9f93ad9e300123c500a216c0c3ef1a96900c63ec7247955319550ef75c0bbdb4053cacdc82a61b7df0bc188e40a83b20996f86a172397388427355b0b77d0f9b8b473234cfe70dcc5251f1feb351f3a0ded59b9cccd440a1e346f5a100a099afbace2e5b66c04266688440087bd13320998f777e9c338d9a9ef6c2cf1e791c9486e9b9c73b57b4f81212689d4c4e7753084b5772ff181d7dd9be3a1d3be24e5b0562a9bd634b742d02604c64b76cd00143878ac13770538551038343e1835eafa6caeda946f3615556f77c98fac90e4308a819a5236642d0b0517c0eeba5fd0019d369083a4dd9002ac8d896306da583516fefcda8dc81a8f23822fac92c8f0884ce2004be88c58c217eb14bbe419d8347a35461bb965704b09810e96b42ebf4858b62c1543c97483d410ee10a854e61efa27ccdcf3f19ab92e203f5c81663156cf54da96153de6ff9c3b5fe0765be65146f37ced781865954757c62699ecfe77b01a0107b5152d14a7502fb58053011f40a6759f0e955a89b1d83788f6940c6cca3374a69bad58b27cc0a9a96e28be22a69554f623fd4d2826952bb6fd6f5357995033dfc028f8c674976132e32ebb45066a1cd22998b21de9fb570437d6eba4da99a67beeabdc6318c6249f8f252c46ba3961d3b89352c3aafa112e257cb5abee5f2e2d53391b56e1cb32a534218c1be13cbebf202f284379fda92ec34d9de4d8506bf262c7bd6b6498ab386fd63d0337442aaa8b0fb05eddf0869562fe21c8da9e44fe3409760329669859353aad02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "10473358350647dd31e3db78e776fa110f2193214f07076eea7120ed74b4770f", + "proof": "2c1eec14d532fe0758a66c3d5e3e677ad6f1a3e992ad683c973192c4e8dfbd5c2c4d2a6a4a842aa5e8edbcd1048706aff7d01491510e2c9410e2cdffac913f2e820746d99a6da951a51296a084c77c29e8c87f9ef2b79935db48126566d7e22408f7db8c90b871e30f383bbc388b6fa0855ab1afbaa3fa0d2f791579b404de5e43b37471b8bdbffbd392bc29e665159a7b56e89d3e21f92eebde21500f8b7e0c1e0b814c98150113e162da6361a49813d83dcc39c1fb92cfbac6041192a8f103d80d5130bdb671eb9c884ca054696ad04be5009de9756752c705339c56cc08009a90cde6c4b39f76cbc4e12ddafd6dfb0f3e3d0d2101e48a2197782cc120160fec72771d5ead0041ef15d34f7b4d67243b231b0d12d73cd4528873c9b893601800623ea0cbcb6db71faa71bea005f56b0c4ac83401ec283a6e56163aedcd6b77160ec76ac9f6eb2cff5e36c9b3846258529e99cfe8a16f353f6d8dc786018402b43fcabdddc276c3d639e1935eb4576e7eabbb67efae3373c062ceeee8eda45dd4eb7bca4e544c857e10648f3cfce25b5c7432aae31b0339e961c404359cd9381e11787f5481942d82e7b0235db17bf7c441e35ac08a4337279bf6a481e41b5a0211585e423734547735af3fc1242ba317fb1da86e84d21a47ca170a1f213d6fa26835d91f1c2ac01ad220b143a81a5f65268d466f71783434d5b7fe32cfed7786b99cf8b1decb87ee5dabe2dbc2a3aa2fc719630f772f905930e4343a92813a8e9e6c4c25e00664ab19cf1a37addee908f6e0089e66899d722bd0b4202bee060e15da3d6edaf730843b8acce9075f3c04b3004e04c1cbe9fecc6dd201ebd66e65a5461ac416ba4e71d2e21c6dfcdf8e0e839e1a7afd053de0d745ea546775085b0c0ca0da4757489626790435c9cb36fcfae2a8295c5b179a4287108c08af01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "28b2e790ce8ee396b7cfe76bbaa0ae47620193aba2084842f94ffd99e605810b", + "proof": "36504b8e4c745a717094e8f89fcf754d536b5ab9083f3b0d389967d4af002a16402d271333683165fd2c652ef35062d82ae0a1c0a76aa4df1f31be9ba2bcbe63caf12e072c0f02c05e098473a1070426add836a2f5547b35d61d2cbe56e3d421ee62e4b3212333ee88f92d64cd35917f7e00230c1756c9f915745cf7e922f2320489f7ad8455f91eeab1f0f535051cc1a9162b73e8ecfb99bba8aebbdd10030c01eca61f2aa9a074520b2cc37e8235e3f259b202d0cd9d9af98bb4e92037f309b1a7e2f3241c11ccc9d871f025e6b719208a87c637ab3c75417e5f24e741b20a202daa1a23f998188566cc8e8a07a15fb5ed408a7b54abc28475a446f7fbc33096542fda2e8a6cd32c1a4b811d17c089308cbf22e82216b049d2a5d185f63f250ab34914cefa674829481c8ebff98a069a943b333f64d1fac0dc8f75848978091c7fa2952541e93d396eb7e43e0d9996dc290ae3ba0beacdf29c89474858390f4436ecbf2718609c07578c92da3eb5f49423f739b97db40a4bd2254c6c69827b384221e47057ee82f107d9d6ff9d1bf2d4851b4db43b1819d15644e114dfb55f809cf6799143d48f3c89ea6821f45f46745b35302816035d93f583c861bd4a7bce3f66550b411a3f74a6bb6738aff22bcea8fa305049aa3a98adbb696ebbe23f98d9597cf07c0223f440339c7bb98e2b1c8896a54e46c67b6896075288c0722cac740b94c9bd608c45cba8ab834e35db40c19595a889be20dd5cabf22a5308375efb0588ef4f5e38082b1e09e368afa0af1b6dc6cb612ca72d7578e2c6067666967cd43b6f50d445db88fdc241406dc7ecac8ea605affc9675bb2cff86e48b49c6d82684e63ba0664de997eb23409b9d01a0753395066fccba93a3bc7448110fc379a6be999d7cc60ab964393564f3cf162ee3373f3dde8588bb8db816ca6301" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4ae5363513456e99e2cc9dd90813b94a6df23790262c290a4e878e42cee36a4c", + "proof": "0460a20d5ee6149f72edd64ad2ca8d927f68752c78e3bccc0958d6b537d34248e8b2861d5337c2c19392441af36e24600ac1519732ad9214dee8ca685d67c9791231f3a9406138849b2e28a659a0276f720b22836b86051f75855fb3e4c82e0b309760c34b94e72ed269f3505ab678f3cfbc1c0d891f93b9b831f784782a190763108eb6a0e3fd2e9f7ce9475f1787790836486afde2bd2ed67a79a8227c6707e8e941d418b79c5bc45f12afafccef5d9fd85186e9f1026728e9ce6bb65ba8055b52a343dcd5f118acd0cae03fed029eee5b5b1396c04018597f721aebee0e0f3a692c097a11f8acb14b97db87a79c8b7934c453c7d22c786567b92e653ead691812a48d30311506e19a4eb6772eb7c553c809928d5417af960098aa953d7355a6c4c09247e5faba6988c57d76b62d55c61b5860ec84f3a3435774e1d1c7597118f6ef2a40ffb0d3a013acdeec326856007e03b3b3873fdb801dece878bbee42205e56a355e1ede5798eb720b092d98c56d637b72bce539664aa35b7e8019a094caf9b0a5c3403bca880c82c339bf0cc34397f1e5b3e326aefa3a27cd92eec13f8ac314ef08204e80bae3940f2a8e4b72c158820136b48cc0f91bbcddd66b523c020534c32fe2d799582d2ca0ce697894c15a9a589540bfd0711a928c5230969bc2c543addcbe9bc595f2b6e9d29bda1fc1903cc8697ebcee48629fbe2060a7cde40d33467616ff1ba9fc45cd4449524d3c1dc02c46fa3def8653ffee17a3210e03261b9eafd3a2da1276a37041429ad220c2b8d83808d7555f211b247a5fa46326ad145639720b33e51eea4cf5b57aa9e2df3c605d6172ee0572fc726d06c04183b4e1134626d457e3b9017a686b94bd8e5ebacd16495bd85e8323c5f04b800c6aea617534e8ab3f109414ae023bde5145d09d897622a9e44e54e16ef48c60d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6c90af20dff9239de6a9f433d535602a85d8aa1b8d6e44486811c0dc88b1fc7c", + "proof": "a4f176edaa24ac387bb997a157aefb4fea99b3ebcad6eadd9917e31f8f372b004c2848d45316d1f53922f1a44a7d94385e26799d060dd214ca274b9033da227f1a6b3090a7295ff5e124041d6984b284709376c5c797c4860b178097a7169d0c88599eb76a1b30f081a12b600b514a46e050d67463072307d799c9763233aa175b15feea9f61059ea4b2db34a6ffcef5c2bc87bffd6adc819838685800766a02a2055ea381b546e515be50889b87bf66e5f64a5f1b82c0402fa13d26e142960bfc7bcad13c54a3ecbbfd3d1e9bd70fd68ab666486e3ddc84ef4655523499d1077c248b7f95a7f0130045397b524c6da41f5aed41be4d0869d8f80915c3657150bea4df7129dc47e3af496dfdb2fc991d7f0baebc3b3a78406c500ea8b96066551efcb5a7c19576f14c2be94853f4976f1fb9d500f2dfa45e6a16dff444d7df34a2774c974bc6c81f05171fa392076e64b199357635bd93ea4b6ffc40ae58cc3dfa3c69685363f142e908f3e1868e6453d7ab4ca7cdc3e5e5a4089c85e59d0f2fc811e1fb4925076f1d674733e751e8100a66c6c5865a8f2e6c4094965e150646e4d04b87c04a73d30e8c27a4515c90789d478bde96aa2634d3b4ff18247c22511cb51de8a60380d1f3a536e1b9326a1f77641765c9fa3f6e29256c9a07a67a4fa653e0dc6e8c8ff640fcf5ba0ff26fba8118ad0fa9ae57e0cdf1a7f5390adf17f0d25ea1cc53173a1dc9f97ad7d647dd9c0667750553a6ce834f18a68247f90490a0abb1de336cb0045d16702c4a34da94d2dd82977772040511396ebe64116f605a0c54d9df11a48405b07cf90433ee22285734e9cb23b6e393a56f4fd4da798d098f888354991fa6465e29d17f846ed2e23cf26e0756a4dd0ab3f70cb4bf0dc7ac93e16d7bbe13d9823253195655fcca100edb08e7d72aa570202d7cd8430e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8a93de3da2089b25c698ed9ebb2ba64fe3a22e3fa1efbde6f968bde886295b28", + "proof": "f4d525e92c77f2bddd26715e8f6a48eca763b5d2a3d193e9eefa8768caf7c600ba7c6f0acae79c76cc3baa3381b69e885c8e0c0ca05b581fcfc37908607bbc31c4cd855a73adaeb2190b422f9c85d8ad27f97ac1e79c4199b600c9b609697c46de831b79ba5875d6bdbc7ec2ef68bcdb72203c078941bb829f6db235f056fb0831d6b0e3edee4dafc9cca4b0513f1c6baf9476c8b4d1e441eb72414d9fd38900e2ca9563ec80c635160ee1a104c748d660472746371c4a6cff81a93d52d7fe04b363a8feb09d100dfe209b721ecc02a77e383f4d867411cd206d04c84eb99200cccb9dd642dd95346745e29fa6ac5f92c4c1814a9494f5649e559bffe52aca61d0e4847d3e6a74936a30b7a3d840501acf88988788872ff9af89669eb637665a148d3542bd850723a661b53930a2a2ff8abd92b2813f58b66a9d340af499122f2826b0ecd84d7d981162a5b0611ba37f026c2e91a765f73b4c949e63943f6637d0a41cd3d29b80d0175110e2a1336621ded5e8883ece84ea638357d16fae9055a411e562a305b12b39376f4e662ca71c0454837c828c3cd5027d1d5ff492cc199cda525a98b61ed8e73a54eb43783648e37fe6351ad8deadc9800349fe18c17f40a8ee13b41ebd3bc4f52a859dce9a871bb2ba870f3a1a6c18bc11670cd5294d0ab8cd0f52f53566bf362a9baf39a9f856a8663a5ad91f147b78d67f4a447f4314e62ef78e9d45568eeefcd0ff7ed996981233785a390a488e0b40c370f1d96464caf42edcd07647d8b6a8018c47b484a618e47bf6b36ab681381cfb2749250be873fb5ce2d2d5b78383890b6c8c4c1951f92476b8d9a35fa2d369ae2633b90ab9c8de497f821dc2bf6e6dba0153655fb7d7cc027e7b2bfdbcccc890351f3a03f679ca0c1e1c1b14182489b23de00484a700be0764a02588cc08e7d6eec5ed01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c8ea27bef6fd92c05d57d0e523d2d283b3073861e749c152f2cec97b8f68703b", + "proof": "ace6517a26731f0679cda8cf25775f823b2699c622a7f4c2498395e8c10226505288e44f474ab4166ddfeada2db62e4b38d5a7aca718a602d094ef72db08b00e98d52d2fc7ac413eec7b25b60c061e1984bdd2e09e5dfada05ae626eb7757d44966a6297fef4fc48c292021d60762c9c1294410574fe3fc53d7746e65677725e79299e2217813848e4fdb85f6948960da2f381db383003b479a2bb48a624120e54ef7f603ed63348afa5c10b4caf3f7db5437f526e329d1c3b3f4ca86751c2064ac4008abc94e9874fe5318b0b11eff47089ebbce203eff5e5c7dd9e93956d0ac413a5315ef630a76b6f149f636a1f7cb38be8feb01db34ed2f2f60b7131ea04c41f5775ef7db20bbfd5ba061f33a818af43d4c74850822376877b3580d5506a8e7e5c620c1eb6ad45f4ba16a05b311198ee5e56448ed8f8262d62ce437c682b1ae3d40671597085b1b3563ef2c0e54121bc3e22cd53ce0e20b0ca8f8a2fee78f2d1743bbd4ca057edf1c5e37e7cebe9f79fb8489b3229c907e25587c0a8761536427c92c60aed1811f429308988857c267986b9492103672b612a3a63907453081dcf22b08fa72d719908ce5c849e173aba06c395307be5aa8d1b534cebf51a6ca8c25b03f8122fd10ec9a7c1e4fa578f841f6e04cd57fe86d5875f0cb8f54d78ea39f37454b35f5ffbf9b18e66a5a1ebc93777eb8bee0cfbfce14dd7ee1f03da76269869f4d09541f91aba54e93ac52a956c10736f06317ca1996fec1d1323063c5265c15bad8a46a8186448b80cdacf63ae632b0a1d540dd6c685d779de64d0e6482b576a91f16f52bf8075cabdc65e0e63261f4890e981393477c67e9f0fd515b1ce26e7e00bf709851a2e98ec835ca381dd7a6ffac15220517780e45702c1f75b9c84f0f2f276ade61d56f7d1c3c87b26103083f846a35a5d622213e404" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cc5ef773cbd09dcc50a1253750f9dbc0bf78777b9e3aedf956e7f03d31fcc801", + "proof": "b4c226f0c6ac691a38f166f4c7004d02b0cc2fbde2389250983d66d49765e85572c38d3e2604e4a5e579e582819f948238ca27bab724a736b10bba70e8972a40c64c7f2c76f176bbff2fa544052882778bab650d2bb56fab4c2be3ea2cf4cb5c3a4dfeddfaab817f2683f662d8b6c4084dd6abc6cef9b9054b16feedde03bf73ebd2f13c51a71f04472a02b447f9a9ba3d710f03a94fc61ec4e572779571750c75024c96b10762b97eb269e360420b7c9eaa4f2fcb585b0f67a901381b69600d84ca5b3db7ce81ccddb066afa975aca1cc7e8a8d817433fe2f14958446311603f62bf4dcec08e62bfb2d3f5e173127a563659a4e65ed82b766d209673f5be537b4004abbdfcdf5dab5684f50ada1dbbf50ad20c377354c42c0078568e8bfda473c9b31f7b41d9c21d8be375b5e76b984b239d037d1118594afcd787bdea3a933dce83fe81a71d32143752f31559293e7097763bc21b3a55098623f37dbfd8f622ea7544e508b070605e7a0730f539334b31af5baf5d49274ea920ef9d4c2d4043e9ea0859999d08e64b181397ac28686e5dbbf3fa4942210f4c0ba9346e48d08e288448de7ae1f3494ba7ccaff475756fe8db8d168338cb62ff985674c7e4301109ce38999262c58a156e44b13779e053bca20da8b6031081ac595c6cf8eab49d8f0f22b163d4dc88c81775ad201ad21da41fa74ece113e63afd4f6b4f19ca587497e9c68b377aed942ff57c5aabe89c53762be73dcada4f1350e85c58508a671458943485e7cc42474e1534ba52c28abeed78d3b04048bcc43eca69073c0d6bda10b99e5d0aa77fed59cc6ed158e7d5516eed46dcef2c0e05ec61c2c7ad743b4999a5f764202b6cea75d51da66e61946dd91d3f079f1372f3fa5dd345d0220809ed7cc1ba8a368c19cb5818045495e21d0c094d1d06d286944af59913420008" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cca7c18d51acc09d53be5da57ea36053ea85c843e25e25b48cca5027e5ce311d", + "proof": "4a90c10acf5db49004ae550710cb78272854d3da6632ecfd769b5458a87a156de23168702db17a63714244cdbc5f16c16017eac0a4a25eb7ded3617b37c33878661bfa156f2d12e9b80c175d1a36f256451308359ab856cdcac8b23424a0bd2d0602ae08a4fbcd901755ed9c08aba466b77e16447e27678d0dcc9bdbf235191974e91beee8afa967c23c98775e573efb7ab69c291fc80b44416d42edc6bc5104b7488d717e0e4fb801009f6b6ed9ae2edbf3b1af6530f1afbff4b132b957260b50625355519be642ff3ad6c1fe1f32d7e629a0af42779f70760a2b3680515a005486c2e949d61e799a49d585ef6e6136314c2243305a03342cb0abc93178a252e24dd0ef14112fea4f3b64960c34278b994000f08e9b666feae5eb2e3640b87a4ec3a51a0f260e4a1d4e2ed84f1cdd53720096f69b855e468ee7c0403d659d0bd69f1fbdede625ed3ffdbddfcf3e5c675eadaa98b221a4fe124fe2534725b90be673deb335f6f696157c7a8651852e27b8489ac3a849b2e87e2c53520b25ff57d8e25165081d6c5c915a038ff542de41ddd11be9581d5619a6f23d1a800c5b42444781ef5bae55b19955e89a059091e721967711807c3eecfcb0ff3fe1cbf9548c85c1d80c70e072c8b61ca1b414a3fd2604151780a7a1fb3ca7176b75e4732c6e2646a21c3c647f5f5c31b9191b25197fb05f37ba918ca88ba6277219e86c067414bbeef09c75521db892b47c5aa30a17dc60da48bc108a4087fecf0ac56f7caa6e72b0781fbba0bee4583a79c2e48709c55dbd16fe51d151cef5b3fd937227546cf5d211ffd18d724b87cb156b13a3b50e38fb80291d80750b7ecd7480d77770d6642c2221c6f578736a42a217a53c28d9848297e3d6fe1b61e00aee6e7e06b8a1740fe9b1492f558295bbd46b54fe3410858496e196e9b954e02a53d7e300" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d468b48cc6f0bcc2ab130da3808cfb5facfbe4a8b179080717ee2d5c29468f53", + "proof": "3869a812fa9222782b1e79a7b0d052ab422b84ced6793f4f5b1fb8364bfab863bea2a9608b9e08f2351d54b6c1faa14294c1273e9a112a4138f5520b4e798a47e2e540ad2ecdae06ac9ea0bb8a407dce8048be114d4634f5a4d243b4265f1f4c181811cc958d06eb01da2dee76cd536c0cc68e122f5ee2fd19ccba0c820b444e6ef8fda27c4f2cf646ad7d1d39d2551bdd931785dbfdac34d687f955fe96cb0844bf0b17bf934aaca12c9cbad1fe2ee3e5ba3f1591f7a40a9fcc10ea57e2fb024437f6f84a47cb6e8278ea1bdf8a362089ef2a6b9e57646461d1db40e67fda04024cad85f2991cc0795a77bd84e45dad28636fbc1a2c59baf3d29ab294665842fc0e5cda630c26d3e5b16738c3604e247851c6e164a83b0fa0c70bf88f4eff26602bb51f4d9c029390ee3ec78bdf9e62063c757df28c1ed7ef5d94f05d2918499ca984f252c0e574d0e48db4b4e2588bd5699a0672e73f11af472451bc7e9c0e4c7e0d6e5cb9b924adc15f785f04e9855799c58cfea490ab739686b09d34a07b7c110dcc23488d005071ce1986d3ce2fc064bce05d6c1f4f358c74705370060fb61c672706f168fb511cad5b6925a8c6b99afae91c31f8d8655080ef16222604fcbe2fc7c9b01ef50d835fd5b75dc3eaaf3ece4c7ef48805fee0cb92117e143e2e52cd0f64d848f55827a05020803241fe43be52cd2142854ed248c0acdcae09885ba212866447dadf9e49b9417e2fb12c3f470ac4c165662589c0f573c08666dccc5a1fd4dba970a844dd6c195e68d72089e76435f6627f8f3680f5b1197f52e01ca8d55de6be4dadfbc6390a1c90fec3c7c8e1a600fc7f06272b52a7dcbe1b994b89e236387b22d780e397e5702bed072ebabc901ea759be1308bccf13af03337b707f7386f2f81073e8bdc7703953a844005762fba8418579aba0f6252f0f" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 11 + }, + "commitment": "0675a8c4532ec8188903301f2b79de8db2db4275192f6c78d93fc282a2cadb59", + "proof": "f424da7c1f6f153da35c138adf27a776e786e14263b6d598a0b4ef5c00bc0666343e07f55dd6ea455d8ee40a68cdc73f9b23e8c190133e457de35ae8222f704fa45ff344330a0175beea7c9a762e0ea739d0333dc3a614ed5f0ea41a665ab120ea59e31d888f77d3e983781dd063499feb98f7579436853320d7d3ef75c70e760d4210febc0e30699781b4aa4687fce8eca44d70cf09501ce1a44f410056f80a7d6b82d95bb83fc470146ecaf7caf563327bf4302413f66cf00da7d2c41a75026c8a01aaa7ec381a848c45bc24e47fcc1311ce29d921092e828ae46b431deb03baf1b5f7e4e47a1b369ebaf347b48337f7f9e1b54945abf0ab25fe493f459d7d6446eb1c930577ec2b6e5c45da2a718c6248c67be37df2d3457e3cfba657737ba8e1ab79e3192c336f47333159da084aee1127926308fc4243e1d261eb38f71bd2f9ccde98e01c384537842d6b49737e8996f9c1db3510fce0db299b0ffc371df8a2c384087a3186ada06e168c9b2ce65417b62c673c39c4e16485e2d91ea04854798e62475c23029d498ed210fe8732d8f842d53dde839c8047574604c3f731506130d3bf171d8b759a3a5933845c7d0b849293fc03f28c578a01bf433aaa20fc0a643e5f1c0cf70a286affef9785c3edba5718f8dc9c06f69bfcf5403ef87b4e990d0f9e4be28e41c18868719d2887bacb9d3c81edde53d9a6d5cbcafd9d69204187625c1fc4d7e978e60700b6ab51697bb758c8bc9c8da82b8817d375390520b48b98d3ec3ebac14b245f8c2fc42679ea4bb20d43f85422c219a58d5ad6307891aa90ffda035199c5184002cd5f7387dceb8ef89a5787b0cd5aa989bb4a2e48040593e4a387e5ccd0936014b46ada863adc78762b6d0189e6f1d24b34b608fe674a36f943cb122bbfa31a5b8c109a9d40870af9c4656083ab5a263690f007" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "34bbce60ffa7558a6c828f01082095becd3e3c8bb519f2494cb5a9ce18c32846", + "excess_sig": { + "public_nonce": "de5de95c5886ac17bc0beb7ef2a32f014988bff06ef6a9a4205784d9cd40e40b", + "signature": "cab6bde07239a5398e7ccab54c46c8a00dff6591c89dc5f6b34facdf21341607" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "360dfcef69f0af24b9b1d2b5d7b0bc9987dfc44203be52ec22f2d3114d20d606", + "excess_sig": { + "public_nonce": "a2a080fa00438c963114217e2fd9f6213e3e673a7bed04ae3ff5d243e1f19312", + "signature": "3dd3d60f5b0b679c434a4c504c60f506d1738b9cc461c20267aed7ff657fed0a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "942227c38615c164020119a90f0149817f0d74f718b48c729ceea93489921421", + "excess_sig": { + "public_nonce": "94a49355b443003a3b24564db5c70f65eec10604d56f22151742e74395830a63", + "signature": "79f04ecdc3d04ac3e67796149c5d1ac51383593f1fe97c559f5fa4e18abde408" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ac17910447e2c177d4d203f34fd7fa3e26bacac55e91306b5100571bbf2a3c7b", + "excess_sig": { + "public_nonce": "184ebb631a775c7c16b82f2deb688cfc9b9ae110646096a15f45fc465fdc3f3b", + "signature": "31f1d0776f8e7de9bea7fb1147e377fce02c3a6c5abaafed42f91907e22fd605" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "b65657cd4f4fc9761fe91d00b7017cec1b97c484d21223020e4ae5d8e96d0d0d", + "excess_sig": { + "public_nonce": "c47048a917ca86a3f677244fc0fd92fc52cb8b02ab846e3e3f285ff855598b65", + "signature": "b347b38a4df9fba7b6032afe56bb131cd167d72b194380f69bba9a775db5dd07" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "4cc9913322d127e55daf21aac48739c8c894e74033d7ea170c4cb2fcc55a5424", + "excess_sig": { + "public_nonce": "8c527a3f62fb7a5685c110b18383f39083a90a91413c559ec42e06b731f58621", + "signature": "ebfd738bc93868339fd352523e7f11322c325e4e97cae05832f9a8392e15f004" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 11, + "prev_hash": "333e58b6d09d389d2c9c69b1211a06ce0f0eae9ca96e23059c4c6b700877f6d7", + "timestamp": "2000-01-01T01:12:01Z", + "output_mr": "984898efeec13314333e4fc1676848103080aa450211f3eace0bce0d88ae047a", + "range_proof_mr": "2f0e9da14b12094d13b1961b97a0f7c324a0414f1a69ae52a9ccc6a8ce466998", + "kernel_mr": "d7867972a46e6b464c4a355deeae3318ab36d04069b40dffd667fb92cdadd6e4", + "total_kernel_offset": "d9250dd6b9446c8f76d3c02cae9001fd6320edd8466edac901cd30b616985b05", + "pow": { + "work": 11 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4ae5363513456e99e2cc9dd90813b94a6df23790262c290a4e878e42cee36a4c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8a93de3da2089b25c698ed9ebb2ba64fe3a22e3fa1efbde6f968bde886295b28" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cca7c18d51acc09d53be5da57ea36053ea85c843e25e25b48cca5027e5ce311d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d468b48cc6f0bcc2ab130da3808cfb5facfbe4a8b179080717ee2d5c29468f53" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 6 + }, + "commitment": "a4822003e6a42970092bbd7f714b7b670c0b1039e403e2bd8d38b57d8ab2e546" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0419150dfa12d3272e9abfe281bd5ce59fbdc6fcb4f86534f0925a2f0936c832", + "proof": "8a7f20d0a0477818ebeffbcad60e7fd54516f39e6c8334b16c4d656695b4b43c56584dde8913352110dbc2bdfc4cd6db1783f6f30aa792142cf43cc900fd4326e89a458514d7656b08c0695b4133501958cc497db5179326af0b9b220cb6dd66f40bc93cde579f0c56561b6b87fc938b877199bea45c9a33ce05cc0d319bc90e6b4942336adb88fb0789a438eeb4cc9dbf61e262602fad0366a9a977ecb78d01966085f3c8f6b14ba4f4f794a01777f8f54b799379b146199cd659c1fa369401520e6217aa65beecff55afecbc23049820f51efc804f304f2940d682e8299c04e8fad879b982eb64a358526c1217471d8c19b8c8ee7712ea737e790d0994e81f4e1ddf15d0f8b50ea863ba45c5ab203d9205a3234f30a94312b354c7f3875f1dc8f01a0579ed8cda610f021dedc8546a6bf3006bfe275be3524f48dfeadd6f2bb6a7451a88dfc40293dd699b5c08ae25729882412ef01eff6a4b2259235cff4d20b7418abc714a952487de3217c71edabd47fc4c80c18b300bb19f3ef9957900407fd50b59b4846480485756ac3e2e36993a0bb42afb1de221e6eb59a468d172f09d142cae94db07ff88a3cba5c1a98e7b24847673eef5ace96fc690e9fe5e49d0e4ccfc89e3515db2ec4adcb4805cec3087008efd9653dd0bce3a4f1862ad23623583cbb3be5f178fc9c2a6438b516e1a9b13426c5ba22259d4ce17aad65f0c22fe9b0a75322b5024248ecf288b10d8bf9bde999f884cd8578b3c74e2a1990f6a72f752d19d27ced6024e79fdde63d9856acb117be45310ac8020524bde0a601ea6b7e7f0dd89ca01b28e0cdc238cf362b7350ab488a1c38a56f4aa9980ad262846abde89b3ec4062fd6bf0cc0b9ff39ae4b409444a66a7722669a3684f8401a57f85f56555fa46012bbe6b3e6b80368d1da1a87f4bd42bd8c622ee260df80d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "329998200f82cbe432b5d226194a3d855c03913cc03c87443985f0940558ed77", + "proof": "5e12a6a32160adde1c6a5d4e4bdc78abdff29fba0ddf99b365483e62c0cb7a1320ab11f688816592243ce29969bdb9759d56c0709b41cee03a418bb9b7389a180e590cf0843a0bfc7d1b055834852fc9644a5f2b42d89b3e81d82a12ad8ce946a41f1f9fc72975e5a7c7525b2d4b0d4d14979ad40f1c9a0f7b90999904af7b7178347197050cfa176d86249c32296433be37a3aa35bd236e827f15b4af7ab40a92b01159ceb74d9cd3ad5f0db6fd7225bec293322918f0bdc9a18712e8909f0d07ecf9cf4e51a8f978da2b99c15548f14b5f40682392dd2a3ba582289babfc0d1c82cc9a916866ceeceb7c8552038c04623aba42522156a1c9020360ce7b695ccc895c6a991c0480436164f8a0e6b34dfb1213cba2043025f65db7ace26eb417b61aa243276897a04dde329249b7a9ca349261bb47a4c571f4a963d56b06543c42b305951cfa6312a270645eaafc5a86d0b246259667ec390dc720c071c463555872d1d596fc8aecbb574d6ecda6a507eb712531d9cbb2ed185ed5196d766b188431333fc5447afe7ea07b33fbc2c8e9e40cdfbb3149081acc7a8b281aefeb2dbe04e8320fb24dd341388fdf90e68366aca5f608673c29249b078cf04a7cd15228467b658bee980da9ec9ae27b68bda77f19a90d45c12d589aa4303a079c187276de50414cbf1d08b74b2aeabd1e11409f24d950dba381a7df45a2b1abfad11e8493b257b9878d1603bc4c374c55a539db877801339bee30f74ae2eff209d24664825a323069c8de7e7233b8ad3ed17db0c0485ae0b70242bb2c18afcb06ca080855dffeddc45b7684392bd56a4f92203753d0c591fe92588f549e9272ec1406c26f93a1eec592fe454b3a1dde48f5779badd74146d8c17727ced29a30304805764479395bfe4650359d368f325a7d2024c6c1dde13fe422fdb3975f19cee208" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4077749bb79c431b96cc0431fb37e409568062c453323f70b8307c0214bc5114", + "proof": "b89209bd88fef374f537d3ed23a60099ce0e8e912ccf6605eb72b2ceee93a17820e38f1fef9951319d9e6b0958d7d4701e78b804a6a4ea1e70ba6e26f08fb6484eaee797be0d824e5e7fe975208a3eebd14a65a6e78d4ee3e2aa8ce64e962c7bd2aaf1291f2552b1e5dfa704ceefd35080c6b1367307750018227d92418d1c54a34c8fa4a46d551171a627ce0c265e2965628c268a8816237d22aa4eb0db2c07adf1c730b2b1db1b9c08a75661ce184227850510c259b766b40ff06e443ee9072b3942b1f5e8b90873baa069b688a4711b334c41e3b5384a205bad32724e760fe01b974a0e39fec12fcd68e8786353fc28a829aaad0c286fa94d13b6dedab1279a6d90499b129a3e6fc7eb859a1b7d937b140b41aa92fde2fd59fec532699b1728abaf75e73ee781cdd01dabae1621924e27b0af37be3a2208074c7ec18a5561b2a48d669ffca55097c208106ba28eaa6a2ea9b2f4d6cff1f2f77fafefd5f739dee6c49e3e965fcde07e9181f5d8393d1770489a34ceab6c72c036718c63f56136cb34954319343692dcba6a20618b43d0cf78101d3725e1be096e6b2dd0e033ecadece2127eb070033c463c15af6e504846d0d123f5d4267d3fd78d23e55f2ef41602964ce255111c7c7f37c7a77073cc298beed20a6e9462fb56fc42f7805914bfe3c44a29235a7f01c92f202ccf7ccb02d9b3e55dfba3c6d38c096457414110a19818a817e051938cfba04054a487c03a3f7e2804e336598b6ddc15ed2305bc8338e0c23d6ca5f2a298078b921732b5a9a4d66b95212c37dc416af11442724490c8b4111cea4433e239451fbe0ff202cfbee310fad93e1eae492ad155cd11bc6567f1ba7d4f9c01f3bde393617aaff76f96db8ce33df503954b52f41dd70cb690a535ff025aca12173ee3507a896dc23990c9340ddbdbf6062b04184e1b09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "96ab1eec731d5d9e0259c03974fe8f8e23a5a1d5a0e44e9d57ae7239de756868", + "proof": "ce9b04b5d19dd709f95b4153be57bca946890835403e672f58e9c87a941e5438fa1adced06ef7b9ad1524b947a67142b696e4332f34e022d35d0eaa1929e175ae46805ed51239328816b2ca94f673b24d6245a31e088b20acaa62804fd9237412041a609e3ac12164e6e600a3f7612d4afa2f3b4b196e2b3be5964c02128791f79880d2a592caa26389724bf0cfde399e0e32b488dcaf39f8bdc374c551c220f0a11202e1cd074f262dcbf2e575cc4592f33510628b9f6d88c9582b6143aec06f02c07430647a205df3e5b7f1ac2adad275c116bb9e5e49f22555e8e0293dd055c0578d24869d1593391a17c42c2334b03d9e495cc3123287288df77436cf249e24fd3dd526e74b1113dbd79b0d279ffb80b7f8c0b60335182452634c094c70cbcd439562b8fe9a4d86088172faa7f583d5603c3153e7b7d71497bfe7238de5e9cb082bfc8d194a4ab79c5278b5a7f61db41e8959eaadb2cb851bb93731670425a4eb6d18d5442578236b5aea623d8cae00e5c78526f397b19814c977ac564215e6ae39602bd9d912d6b97bee4cdf247071238495ffe900295827f573740b328e693f9cb422148833768f5c4f9e0873dd5f6f3c5946d9d9bbe3b55faf1c7aa44e2b3a1f7ac74be80e6885112aa7210add30f50c1450efc018d30b6c61e07a422ea1214ba5159a33a405a5c0483cb2212f11098e2739b390bb919923a82577c2fac973798940f21b21ef00e429f1712a7d3fd4c19b1f808bac99f0c05131a2704c0e54fa84b75a33e13b5792166f75862a6c4f7818cab1b942894e73f63430408a40c92bef122b136c8429336b01bf08112c68d24160ed7c9f09fd923e7813662aa01714ae1f1561166c3e6b25ee3b6e99d57e88202f421990bf0255cd312ab0024a9a7e3b97ce9825f24d33b1a5a476a54cc96cfc000a22fdcf88fd060c6c00b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "98f466e0a79c051fa5e89010ef229780cd36eb652ab57df590ed4439f2e06b7d", + "proof": "789f5a0a7284aa1ecf8049134fb669f865b60b6c71341f0a93205a1aebc9ad7438b41b770510ca531787ff64dff258f85a55797e2de2abb3108ba2bc2568a11c643a22464a1fb0f7306301b484fbb20efb652591d959ff7c019162b8eeca273c8c4901dfdf802ef1f734253fa53dd367b0bf7b9e40ba786872eee8189fe56c5b667e62e67cffab053595f2b4e786ad328fdd993cb4f5bdd6dc6c54e7dde6a00f1657c71bdd4fdfeec989f44a6b46f6191b2e8e5e8db0bcd68d4b6904fd9359096cb64fa64821854f14f62057b91fe80d9b3093cdf8fdb968c05aaf8ea1bf5f0bc6dc06a08f2e318a474ffe7c034d0f002cf3efb877c66868f2a4e4aed1ec21170ee08af6e9bc9b7b5e6a8c14d953ed4e644ed87211aaf244cfd357d4f2e54d68b811c21644233d8d0f0ad5a711f30377748356c9a8c7fe4ce41ca8f184c8bc505085637f2ddff81e4d0c3952a3c90ec214f0b1c6929b7a4acb36f1ab39c10c58eca0d21156178dd67886d8ac4c32d5086c6a6d5f90de697fec14c144c730a27558815a304f9dfead40a2cf5ae20aed7bfb5b6578e53c54ba70590c96ec461d1328f1ff6237e28bf7212408f9fb3a4c8966915880a27e91a388affafeb1f429295423ba1e8ba31b63cb9f26091822ed0893381f8cce78e811b093c599317e682cf0da7444815a775bd689e9a569d389f76c72be8d672ba11de43fc3bcc2137a0d4252aa0d1bf10d13cad0958babe37ffb11b95a9373ae4fe9159fd255b5debe6354576d2d05aaa857185d1ddffbfcd72ceb60ffb1d8bcea9012c1222e11945604f8f28d09d7b40b7a336d45956da759ec13170a54a5311f2c6f474701ca05dc0f965a0fa599d341a83a87cdee82253a036512ee7223d3aba20c39db7bbb56e000cf9f6a8860050def852503c34c0904abcdf6270bfd3a19a8e2ab2d9d933e150a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9ad1319fe72852fb3dc4020c2919022a3cf515bd8b8fae51d593695c72cb2661", + "proof": "b6e80329f645becd05fc680269e47bac980b21fd5243a2f341b097bc9f563c5c08fe1fe547926b39c50869798206349316985254a3b3427861965c8d3e0ec1533a4a2a19451cfe68d22843663360dfb33a79b615e2fd43f76b6a5a9ddfc8586d3e55754f3675c0a4990c3c6f4ddd5c9f8102d2265a07f76c43822075150a051de099ca06c7aca8822fee5a57b69fc31c0ae8193df91eda813be6e656ee661c053afc9a4ac6fa30438841abcd335db86be9c304436c74c79dd97ac77b43596f0b94043fdc6b45ee44e155415329e9be46c9bade1ccb4b5d42d99b775a9aeeb4010c023fb4249e5beed476f282fa10ebf0d2354ff53c97f4d1d5c67d59e3c6d55c8aea0d0d0a30b74efbf4405addc80fcde2cdc41a944648c1c863ff1b68c3157032d42be8923077561f70e6359354cd9841abe22056bb16da6dad5e0ff5eb5753f8f5b032b5a82a3c408064e2c3f3c2dc224f9cf1cc13770d2ad0972304ea32751cb394ba65cb6f0a8a67bd4078fd4b5f549cc0dd7f0f1b415ffc112c2e4ef0651610d569f2963ad43203a07ab6b3a690791a9bd93d8ddf45fee7f80fe40a2b288a23ffda47043bb96ea8bacb034beeb9b61495741de45820fc03c6051afcd4231a4b5f7d3e73c020528c0d787c34adc4c5e5379652d31d08cb8df64a754000361ad599aa179001225106977a4938d78f9a3c3e4b5bc82d7f2b2e0aa6754def42da34b6cda827ea616f9ba73649130e470f61524485b028823ac9770b65f2f1345a25a4d86cd12006861e1c67cbdb35e8aeb0e7ae626eaf4134b582bc47349a524ac609b7f0edd04747a1140a513f730a31fb004a6cccd74fdf1fb34236b1f8311fcb7a0864855e25d5b30003905ec1dd4876b2a2fb44f2db4f6b54edde4cc90c90045790428f5839abafe3649ee310854376ea7ef1161942bb813e1b60356d06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a6eaf372fdc47d5d3d9335f438e02d88539e9e3a235c6dec615f3d3668a44662", + "proof": "b8250881f3d7cd55a7859faefd81300b32a6f3944c17960c6b406a5df1201076f4315edb902c9aceef7cb58195b66bfe1025d224c3d8adf1f46c126069009d3e00b2b50e9a3ef33efb4bc9b7e1077e05a08ac153b57ebe9fad08c6ec56688f43d62aad1222735b3420d53d2ec4f0697bf8cb32d4b71af0be2331338c17e85558daac0cf43092cec0b1f579025bdf40db97a2802b153cbd89127dc71a7430ec0b454b63ced2a5583b4727410fd1e4a4549bf5c28c4fcd54543ab25a0cad22820e2b4e262ad02d48b5d39995da97b55fc306721109907c3541a0f7cda9de21560aec96b3abc16d659d1222b337829660792986bd2bac1f753c6c1662f5a0781f4cdadaf90ad0dbaedf34f4c8b8ce9c5309f618cb29b6618a179631522a14807822dcdffda69335c34f07a99d2632e3d502b6443a3a33046e8699867e3a8ea86f18d0225ff4bb35ce4201ba95e2816ef240d9a79c208f8bda6590e0185aa5584a1a3e0e31d8c95ca6adf162eb47ec9f715b8b47743b92f4686064dfec605d9c8b2d02206d6fdf9f69710f0949e32fb47f4d173d39fd5c862ee27e6ecce1cc799529004e943591c5a62565bca9d86c118e03288e0d6f573de1a00c4bab605d9ed740ac9b1292b4f8530c63b76cb8aeb23c083e2bb0edf2f8e602e9ef0c58c202ea2fd44be52fe6aecb593c580fb8319efb08c25e3d15223541036e6cad8692de72511aedb61fda92330cdb3578f81a0227c23337f297762141ec8028fe0ec96941406e6529a2ea29e44f048ae1c0c03c5d651f4d17f3d10f65dd154cc139541ea2149eae99a4dd02d52bbc7873e42cdd9877626d9ffeac27924ab3fc5c8efc4f22638bae27390f1d3e1f93f34ed61a4d818dca9aab1eaa7873fb9abc7c8caca8e80204b1cdbc5325d6740ca1d2676205df1312e134e10d3bc6e2213f03201f9c4805" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d28ab7e106e6709d734a8c4806ff8a1efeb041c606964a5e42bb6f2a394a673d", + "proof": "fa544b51333203e714b67b126615a57fc423799b5a24c3de2e35957859e8783e5c0609dd354fbe46872113323ffda81889480f1e25df7639beac86239f34fc13c64a6e798326fca027bdc7cbd2cf3ca2b932c04eb7b3e4b0fc82d6b7c8739b17d86a8588d9f4aa8caa2003efe678140f77ae0b98cfbf91163ea7bf251385583dfe1b0a8c79c98c42d70b4f54d7e313ff64cfabdcbbcd7337c1cef70708113d06b3e4c6a422b64f5eaaf6b375a9a59d161ff722fef25e7298504f0237859c3904f2d38485843e062f0b9400c1451b3311f2fa59f1c93dc0d6e71cc8e810a24b03209c0e2ada886f3e33b08c52706e32f36d23c8a32659747247235b95104784095640262d792124f004a950ec8b354ab067df9fc4c6840be318a0d5073e594c0156a52f444e1c6ae31a3a1d19afcf7cb8c075c5fdb2ef3719ac192bfb6fda3c1d72ed076f91932b4802766a78bf1c0181d3b6fd4e61b5a8b58d914915e8064d32b47b3142c5ef05f6aeab8225a43dde3c5f8edc29e37eddfff010365d194060464edacdc3f55073727c742013ff7b5dca74722d29cb62c3eb43bdcf5ff073d80f9e08dd3015a60aa0f82fa85b83ac9c430012570f949d50f7bd016f7a964dc5416a7ae2ac3875cfe1ab253a2e10eaf5db3dbb390a169c4bfff073c25e0789c42cc2ae5e24493d03dd8a3a81f8f028e8dab9e623cc11e463a3741bd3f71662ab13ce91463baefca9c44d7885ac1cda2e578e4dc03da3e025086ba777b61c5aaa2f5cb5d87f9afd8a33ff13e694f7f1ad2bc151207f17e8beb6c8164ce6357a6a0608a9fc6a69a09f2ea866ccbbf544a7823440dc3391071742399f9faeec73533e2e71255d2f3d3867bf6c6ffa38be3f723491d09fee1f1443ef1f57bc79e94d0d6b9ddbc3ec2ba710be08b22caca4433917636ebcbb04789ccc346acc5da37709" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e821ce0f0646ba6433181f5b6650f7f3e93ec5b24154fa0c06abb5e05e21fd28", + "proof": "9e6ac4ee9ab226b37d81e190c6fd5a8d77c6592f8e839ac3a11b3dd5fdd35913a212638eeece71088dd0ca284a09c775f60b18c387a21288db114647ee6570574241eb4bc36754cf44ff2309ebe79377e6daf82284865c3bad91c5729a1b4d445829ea5a3fd0fc13e7f931787a8ba660ec75d4f7879efa715e3e67e7e0e08f0453248b801f781b8848cafbe5362f517a915d6afb38af8c3bf59a1ce356b4600a236259933e37d4fd0dac2d97ce797a93dc9bdef9544dd814b8e96b5fa2409a01446b7db98bb469eb9df60e99104e6e8b17e399ba041d8083c83ebfee3e17d001941515a06ffcf3225ace106f431b75013b37388b80986097bfec0de345cd4b23e481cdf65450966a42090709f188788a4864e2791ccc3103accf10a34243610fd821e13e1f47d99a6d397f4a11f9df56ad85f53d3a0ccafeb33f4c21b99a201872e7e7a4dafc849ecf372c20f806feee61984b8476028780da0f728a54d36d677ab6f4a2639e9b6eceac4c43a83dfdb3248837f72eda08650e5f634ba197953e167addb8bb128452c03b33b9fc6604fae38c8851b7c756c9fa77fc5b297b0e43fae9d45ec8500967302c8e39047394b8385fb5dbf6743c19c491398dab9cf449c4a47f003d5a4d509892e0106609f141a4f25570c0c67e5b00073ef5f85b6936d837ce90e4d62521c1898833fe2807d615377545ac559eeda5f030287007844edee73ef8ca8a3f91efb10a1fd6f8011f44fd8927f213785c907b33b50687242e44ca745f1b5101191c64dd741c6f9664bbcfdb57d72d1a66f006f291704b3c5eec86a44e9ab4adf64b7f5e74d78c82a1c9eb58222a342be0a73de19fc43bce458a803b27cc3e4da706f8ea23b6ce80e0c4c8b7ee20afb8c8f4ab4193b98aff0f48bcd6d64e8eb1864bcb79a82ac744eb628137248a743d95d0b84b622c98db0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f29a321e90a53353b92699c9140c957cc6b6885c088ccac59787eaf6860c5552", + "proof": "4ac573e8336efb2216f9cf853fe527b8bbae4381048a9e12623dd78db3805f079c204cfc4d11661f844153fb16a52cf46d5a4f2bc7381408c1e447be70961373248072d05f5cd853a651c8a19dbc94f76425e989737b3b782fe556ffe22f3d076a2e2b883f0bba40830b387804d3ac3334314d390701e2534173c27793153f2bb283e3a407c1a70ad918f8a96c8a87fae14b0c32afd38a3402bbe203223ead056f301128ab6ec0ebf403cddc7674b447d11de775a1f8c8ee45647e4590d20203f06342a7839f96169f669c63e2bdbde4ccfc3b013fb04762c8da43dad6f0d003b0599161b3e307e5b03192062e3447016ad522c7e0ce106dd7c568445688365d123779d2e3d1d1c550a5dd7cd04ac6f1b7cd1fe240092e52377382dd769d2a0fbcd8356f775f49d21fdbc9f6777e55b93585164138d182eeef80822c0bb2a87eaa04d568a773d3ba064e7be222c5f28bd202f5b47ba3ade8fb7600ebb125ef47a4b5a984e685fd68dd40b4f5b6b528d5572d11256e15b197d11e646aeeb5987f589a29a32330d48f79e9121811c094c95a864413a36afa3388126d1568441632ce94805736bf261628349dc12d70ac64a5e5b3f43846a2fd6209ab8a1c49dd306044d5d9be23a6bc3e03cfd530697ef4480f32f6a33bb132327c934d83ca4c2e96ac025a5037746d1c998bbf806a233c40ca78dcc6149ac41b25a83d7351192c68e8e70f810f67e15a207801ba949748d6619974b1fd714d91c44f50940fcf4d22eecc01a79c2ee9d2122c808290a83a00188075bc4c05de16994f7299f05b2706e30c4be285affe1017c23fa18c7667117ea1892febf039502871ce2a8a8571c663bd7f03cd9d625d3a5da9d07f41d1bef878409c1fe4d8dec278583d4801043339dcd93b46e2c80681d54dcc79a7ec8966f436b530c3054c7cef8220623903" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 12 + }, + "commitment": "1e44f23761abe164ee449bd913f95d42e61a55c151e283483ab6e18c3c60f17d", + "proof": "b819982c4ac8be5ec37269ec351dd1dbd53ae19839c922e2a9552c79b8a5971dee5393f47cbaf7b2ffc09599bcf617a57f31a83a3fe5cad4e942fabf95d80a6bf874195d742a7ba79e7541066f6912bf64a97b02d0f788064b6a2e64299e036e9a7154325326d178f2e3f22d95c4e3ae23894c8b3e5f17fbce9ba815322ce36256515a19b738a7b205cf336f18fde151300bd70303c7f2c3d1a2bc13ef77e805ca246d3f3794cc8e799aa15f52236b85286bee0c754fb6763d34779dcdea2306d4cb910ebfbd40a7ef9bd30069d6bf087e0706a3dd6ac13bbeeb52626d9b2c0c88ea335a755532f8adf747df416704d4f0a3ad658ed2b82c7e7f0a1c76b49c1f1ad841a5f40bfd57479588a99fc0aedd2c3708fe1244b2080f97828a7caa5879d0a1539f4b61a4f6ce8a5357b2853521981107972009f00e50cafce2c9d4577c6665f3d5b176cafef4ff749ea465cbb54f9af9eedf744d40d2a2061f956a5c1700d12344725f654fb98b813a3d6911e3f217855d53be40262e024214705f6f4028fd3fad54d5fc5d84c06cfc9aa7b37059b4a8aa2748b923bd975defb27df42a004c4b8a0e72b53597f3a342e10a37aa2dd767b28afa42f279895dd4408c4b71368f6f78c1f7a5038bf56faae614a5c7aa2f573ecb65ccef252ddd5a9e91ee7140821c90178c5c01372bb85260d5c690a0b0f195fecb6db9ea79d59265cd3a1c4ad647f98eef647670586217b6bda635c50795123dbc5f47154cad52cccdb4309cc64f657ea46036298cbb665c12e3db214d7902bce275b71af1b9fe40b0dc3bbeb041d008b20947b600fc470fec6c4e1f16b9ba3bc31727f73312a74870657577fe81013a55baca5df5b4c8ecd7bb5b9c7d43336e854c8962acb5a1c0c8680c332ac3d81a880132a807802e6b133dae69bfe62d9d012c175ff24439497e2206" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "4015827743e9a3db90aaea801f5fbe0ae8914ca5d72e56c2922fe59df9526879", + "excess_sig": { + "public_nonce": "bc50384eba03b6d17c833e6ca696e6440b52517122863e559323ca20ed943778", + "signature": "0506bb4e9e906d710f53cb594ea8bdd8a25b83c1e792472d9067b5889c987b02" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7ae2a0fd58620b227a82444dfa34b3d70f1285befc1fbe9e38c0b94cd6dc5923", + "excess_sig": { + "public_nonce": "e2c507f1e0dc6fb6ae3c118efada1e5e33059959531696fc6d08665e90d4da78", + "signature": "136393c763fd414eda34e653d619cf6091689ce6d8f7a30b4114a13e1018f30d" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "98f4237a611fcab33c69af2718b7a6277309a307279f88b8c4d10ef3d923f67b", + "excess_sig": { + "public_nonce": "b83deb981fff89eb427eb963f916c7539b3e7e46e4a7961a2d2df415da32d546", + "signature": "5a4751c3d17471037498aa2c72fff9ed39ba54bb62e9d9c0c14d016254f5ea09" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "acb74932a17ca0a112273d056dfdfea1543b917d07c6be9bedee73e37449922e", + "excess_sig": { + "public_nonce": "406f7901b1443f884fb3cd8ee496c20b63c868a2d87060a40586fbcb4fe54f20", + "signature": "117ae33000ddda06d70edee08c543e510455c4f08173175951197cf8d71fca0e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ea9aca988b960a786d4af4613cbf664fa6cf22f27faba805821f258cca93266b", + "excess_sig": { + "public_nonce": "f29c2a240080bc831c4e28195f07d937c9fb1c4f8be81aea81e1bc425bbd3412", + "signature": "e99487558b6208ea0ee26f6555bf2154736a49d6956dce282e6f66a57c13d80b" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "08015466cc7ed27d4732b4158ed86344a90f7fbd7f603561d6eb9d36e9de1d32", + "excess_sig": { + "public_nonce": "a0979c70cb8ce74d0c669469889290d6fccae698e562af4783be905d6b5a6f28", + "signature": "427af18d3c9bdb1603ab847c75199f79713a19970f4e00734941eb2306da3b0e" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 12, + "prev_hash": "3c6a002c5146ea882939eb901e3c394d0ce752fb72c8533ab4e388991c1b1460", + "timestamp": "2000-01-01T01:13:01Z", + "output_mr": "dda9c7b20358a6613256242ae3c58681c3d21095eda13bf80afc318ab868fb99", + "range_proof_mr": "507c39df50db23e80f40befa37a941187bec96fa2d7af89ae6209cfc4f9e70b1", + "kernel_mr": "1fa9bbefc69950bcc878aabd8020624e77ef3639aa7297e721275a53a55602bc", + "total_kernel_offset": "73c0912dcf08da2adb11f725252cf56542963c58a4dba221ea29bacc684de30e", + "pow": { + "work": 12 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "329998200f82cbe432b5d226194a3d855c03913cc03c87443985f0940558ed77" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "96ab1eec731d5d9e0259c03974fe8f8e23a5a1d5a0e44e9d57ae7239de756868" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9ad1319fe72852fb3dc4020c2919022a3cf515bd8b8fae51d593695c72cb2661" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a6eaf372fdc47d5d3d9335f438e02d88539e9e3a235c6dec615f3d3668a44662" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d28ab7e106e6709d734a8c4806ff8a1efeb041c606964a5e42bb6f2a394a673d" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0a862896b7f27f820e3e08ab61eb865645f066ca65d16d5cf4b3928266298729", + "proof": "984330e8aae26b0eabac75f32735cc131921b29ac2c0ae403c06c66f7d51d214cc57c3d82810feda868231e683140d5b7402bf2c981d69b3a6ff0fa7f79926517e24a2be9f26ebd72487e60d8e519679816273b54e208b19661c84c6b86c092ce48c965e787c09e024243963a0f4ada5644aed50e975f53b33f53d18b263a2633c47ea423e5ae40ede1607fc13f836f0e75074a5a0dc43127695760f6d550206014ec1d8440189478cb76995729d224592cfa8d81b2c826e6141cbf879164c0c944df492937a1824103d56e271ba8429a3f1ee17cd2762391af3bbc2e4f7950de43ac137cb3451d3bf8871a85d7bc2c2576253798a6d92129c7decc296ab38189062f87b48bf6329e3225c82ef3329ce4aa8494b0a63d78df04fb59b665fd241948bc7d864f39d7c7ce03c7866a2aafee8919b51b35c252929d5186a98a2ad5e7460760439d0d84ec97a7a76f231ff406ea9a3bc43569f6ea8a135856a8c19116089d3cb33c26a111dd268edcc8fd8bbc6037e3d5c41d5baa15c5b273ca5cc032c462799bffd86aca0724a4fceb45fddd79b46af260cbb198261ee9a19796b3928f2054a07162eba52b968e5438b05c7fd1c92357c945ea89f6225e1164ff6695621188db238684090193276681d19afdcc589fce9821549783637ea71155552ae6616f8d35bd2943fbc23db1a00d488b89ebbe368d8c064ea564dc47a15f82492806084016d65be4aef242cf2e32d92d562f0601b27b7ea06785cca443031762080ffd5d852e56d4f8d1925851c368fa30d3aae6f78c0aab7904543c1a66907d4bcd997b56fd19e5c2426054e4238f76fc559af701d1e2649bc3247f58e2c29d9a374693209be414cfc09ad568c5ea41f0cbb7fa8bcb34b886d2d6ab3f9f8064fe606c93a9d1ce964ee336d8d8bd361227cb1e183cdf091f879b33b5d660f03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0a9798eea7971caf77fc9e6d61a23c60da8a2139d69f542a4eeaa0b6756e3404", + "proof": "7027f1c89f831961718cedab475e5c3dd2f8083464a2947ccbad320be8d84b5a98bccf0dc7464e683793a99ff9828cb11d9b4f8762413c8da51d3c5d9df2796b42cc30b5157b07aa1991b7a62a5ecb7c15eacaf8ebc90ef934cd21abc8bbdf6fa4e7316df0ad5145f4ba95578d3fc29a5fba936df4a8fb5731c132d9a10a210d79d6c9194f24ecd021f64d83034f53aefb4c4df000544eaebaba766072420e0fce7e8c25bcada9f5a8a1b879d4d22023bbf6be43dd87df5b4b3715766d5ef30cc163b50d32016f4ae648110e8e5db38e3dc40e2e35ca4dcde21e70097b744908089389ecf9be66ef6d097866f2fd5f171c6257079b1261fa3968fbcd5b3a6608fc4a7d91364011795bdb0e4c043f897fdf24c82ef8943f9a77e870116f45d138baeda018402cbe252381a351870c9baf239f9a64fc7de0f73fdb9cdc0f0124719ef181af2e70fb94a3af016b541e8b49adede7dc5909cadf2c54c10660cfea6684dc480ab164d0f9545b2f25b360561138baaab0be738f99354d876b9bfd706414a06a5c7989a2364b48d00bb096faaa402e025012933b510bcfb1936f651a2c0e62acea866ddcf66f0d547ef1441a153d69e025ea81cd063871f2404f8454629e7a6e499146330c11d134598be357a2e8771bb96dc446b8c6cc774ed2f50e72b670b49409ed48a8497f0e6d0d33fb2e0546fdbad0cba639f41fa60e23ad5e63d4efa92549e4ea95426935df4d1662ec9103ddec3f2c38b07b3590a8dcfd6c5982abcfb015948ded3089517c850b2bfa2becebdee845961fb11f9f58ec3f630d4c45470ebaf6432b08c475630e9106b1d3c181f5bb9117316973864ae7b50c3ed2281f6ec2fe04f066cda2ac29d9247bba3138b1b2107c1e2ddba9fca834a40fe7b72771caf0b598d8a86f773004c1d7fe90a34283033456af77e57bc4aedd08" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "12e648a1a9ea10887877c8a071d2f0e385ccb2c378b1d2d222a1f76dd42d7943", + "proof": "68b6b1d3364b9b13917ed1b22a1710e7d4dce8a5d7338e87513383b82e9a764d5826c8b337f7844dccebbc98e4301452433ecf068b2b22ccaa67d703f8c34762220a401ce88fa93f2d0b186c109372d8c26fa3f46971e879bd5129b00cf4c61d36531a6500c3278c9d8fd11d3a1e3937da71fdd93e746414a959ffe86974a42c1325d9c87288195277bbf3caa7dad2bdf9fe9adf732752b130d4334ea36058041a81841d98962391bf205466c40285566950e46d488605deaaf48fc17356ea02ab01c3484b5a6f3b672fe5f13a3c079c08e4f18bbf56edda4aad57b5b2b6710d60ed9cad3ed1b409e01f676fcbf0d1144d0230b5dd0f67166f04d7a2a5e6344640f007a97f52b7d510f2c0e05a20441b099b72a5950e96c7662dcc415d69a051e6281b32c323ff0326a77d17222c47997687feec5b9edf079c52726bb342ed7772f436bd187ec1ad8fd7ce1998b2fb45eb1d1528290dd505b34426886bd7c5438a6f1209cc0e3ac43ac874512fbecd66df5cbcccb3d4bbfa0318b693e836103a52e7bbb5b92e8998230e742a6401fa904a7b0e9aacd609d700c81d95c8091870b2ee9047481ebeb5c79b3044855590b74a48258d1b31cacdc8ebb45d2bb4a332ca671ad9273982e72c2a2b4fef45d3e32e4bb5dc2fc676f5f27be59278a5702b143eb12e011b97a442898e192821e7bd4230ca60543a2ae2d3eebadc636f6a112edd25bd5bc8bac9582712f95e02c9584736427d5bf3c81097404e0ea62ea803a415ba0bb4afcc040884e634abd9ddfb9c0c3e6c85e917d33e4082089f8cf00ce802cb8a8ca07bbee893fb327e52ba96196116ed9d7bdd6c3e9f2fbad5eaa652e3b4b315d80bddc0967c043bec18ea3f5381cd8c64a6dcfa8abfd0456ee90b001ea5b16d88f6a3c132e116cac6abf1b85b2d14ad7686233cff35fa61b450d90c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "16cc2c76c3e872205cbead3e52b7a6a91ea3a2593289753e7bdac955fb330760", + "proof": "64ebdb0cb93eabd633797f2840356b0ff9f00af21082d199510b12d8d728e74044658c311e760109bf23c692227520b713aa28c16f0f224adfc095b164c6c747a07ec21005c34bc2170ccd5a1259f38c247a8a7a62ee6a6f38e687fa0dd11a4da855a5840322ea7e0fd19bbd40ed36f75fed09e431460732e72cb6874b26c12e79f31818dff76ce24542c594bb4d47a43628ec15de77ddc8859a6c743b2af001180eda87461d45568ca5efbfd0df2dfd1839a791e3bcc138e66596f72b54250d1040e89cbe3498ec1365befd3d9223b8382e3712fe7d293d0172c3bff9fb7001e6342dbd88817d3537a58d03019d0dce57c9b98a3d8b0752b323e87c1edc622224ac41a94bbeadfd2cd2ddcb003fa519d0e020f1590a63980f85b3356381927a0899ebfae5903c50789c7b918c8ae916b54d156184f32f7fd5d47837e9e1ab340cfb426ac8b2e8a949493300d1f3901c824ac129b5b71dc95509455bb871b40b5082d2ad8420f5caf7cc2fe43f29ff281444691abc1b05e35beae3f5f0a8b0219ad3f1a33d0abde074fe32b46bbbabc7057d337003f0a48e5ad5e46661e9dc08fa714a356239c8d968b392f81bcbf4a897f87dc06c857293359ae82754cea86b2c4f6ceb5769203e6c41d9e020b6b31935ff6560a272f10b1ff79fd8c161a6126e8355aeae5d241ffb96466988ec3e5caa1cf4432ff17bc2889d29de4cde0639963d19f9aebb8d0d20ba1c16a52e59c5ad70c0210a32112d3a439546f4af1441c26ed0696d48467565b8c56889f4a5abccb71affc3bb9f97039983982ddfd43e1a3ea695a5324ef5d866acf7abae8dd06cb20aabf3c4ca00a0d983c039de9b17ecf41c540d0defbc4da3e306d648a93b1df916f4ffa0f87216edc8f6a5cd4c0eb08b757496a58389f21a5a89bddd21438a3163da1066df4341f9e1fb8c563602" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "16ff835f8300b04a44f59832d847e970dfc39e0a9a3f4290235cc2e56ed5d336", + "proof": "74633d7f9cccfee65aa0bb9c55ee673a03581e450671a75adc7ddc7413ed6016f8a84dec7c8dd7a8b399e6f048dd8ec15903f10f9f44192aa81f51c51aee6e5b3c5c92a6cf9b029943178e29cb0a71814e39c35669a31872b8188498e103ac022a701360150ee7a01d7579428cf15a15910176ea7672e938c2102bdba089f14e6c99ba6afc2a03b23ad5c53b9bd36de3c799bebd2fa6085c3890e6a7efba06015a9a115d8a6059926743641a0f42cde827e6a73c3627d75af507573b04c3d606274326e461344e40c4de11fddcd69387a56844b16f6302bf9a7c91088ceb790ce0a627281d9050517ab83959f0a7da50a6aba40f3d4529e8b4ad7d621346f95f9ab4210448bf1392b78097fba351f4a535cd53a6bccd0435bc5e46116d203e01e4a77fe7650c9e365a05f8503222c3cb2e890ab1b0e2c4244743e61b0d465b547e72bf6d40009b01655ae982440c05de902e6b65dfba2a17f68a51a5fabd6a07786ef332b3336a7dfd120f8707742d4346dc0a86d1a2a206e9f809a56066771432ac73bddce04d57775b3e56830d2a56d9ad3449e4a584ba726e051147547772b6500f91a58357fd7cc7ddde44eb099df77accd43f05ec8ee45aba3b3a404d60e64c32250d71a7e253a92e8635045d28158812b56b87c318ef1acffe6777656666121465791248c233f360083ad382ae6faad656cfbe37b3f9dd63d529e96d5668b63fd4fa93662d5c976fc13bf6e6eea1d259b50c3af4b5b6989105ebc7e44a30474958fc962d25ae823921a24aef94c78feb23b83dfdb193e92d1f1198a835d8198457b049ca6d419c2014d16fc4743f9d2caaaeb4e131a411efb7c4ab7c7828f1434b5e4a3dd754e9010a1b822bca905df15734186dbabc4129133e57610cfe46bd9760f320aa8fead6e1537c887b6d68c0208d286c0f2f2c67ffa3d04705" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "209346e8961376c3f5b6cfb5cc2973dd081cea4451d453277a0e7c465b066825", + "proof": "78833f282ac6797e02619d8d575b2910dc5ac5215c00c60a27a549a03ebb0956acdfe9fca4780fa18d4bf0f9c0f8577ab4b836fda8d6d228ed11d28e3de1221bcc349073278ba2a2f7d37d3f2705d415d4363d00b9782b2854fed782d23f715d743a26a57f605ef9ec671472f294fefc9603fee042fa31fbc4eda478e7ad3b2fbc7896671ebf7d5112277713111521eeb1a0d3405224967912b8fa54d537560ab18b46dea826e551c552bbc64e37233cc2079db3bfdbd9a1f4d2dfb66482dc00e2be32c1ca15f6503d33d16b347133d75946d51de8765d16163aa00072b5f70bf0a358e03e84c45e6d32a28551a2e8ed682b21bff5109e768ad945f1543a164142dfdd5b95701cdd577ffb9d438f4e0be861b040133ac61e3d6dbff4f022e7604c79ec952bf5909775937d98df0d9b6b6af86605ee93fd2a1e2a1c49c1daa825327f58ca771bde279bb08be9614c17d8d18d5db3c893e1c0181575624e34ff48a09ab35380e39246bdf649e48e26767e5686280261845da1d2f12b6cc31778371a843f3e8dafa62516ed8e9c8ee28bd5d0b3fb3ca889f86cd1738caa5eebdb568683a5458450bdbc26cd125a7b82c5fb1d338c36609297b0661764fbec6ed3723ed832f108548a213053fb4ee34b09102b64639d89ec9ccc8c823c07b2c3fa0bfa2b859659469481fd9ee26ea2f9f51d534d828cd4d2b5716c0f29a84a8cb1344a32446fa91758b4aeb2d606b771d050058ce706c9b75d9b071dbccaa4501a07ec203e89589d73b1f87f006c05f60a65442616fbae1ac87ba3f0812d686b0e2784752b0bd0ccb301f6c595cd3324b8c1869520406d973a0fd078df0c80b9a5250bea532f6d40a840d35be56b5634758da8c6812e8a7778a7ca6d3bb45674cc0d6e1e70a18d4b9545ea1c8cf3337688a6e1d50f82bb672cce873e269b30a62504" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3048d4381ef70e816c3ae9a240173c9a8f059cb2023d4d0d25d461f8d0860f61", + "proof": "d631ec13f387405f690b65ceb633d2a25b67cf90f4e24486e600fe060f99fe6c5e84fb2894643e72c5444815d7dbe26996981cf66cb0748ececec18db127d93620ed91f9cc07dd3ae201df7abc1ea470f039e361ce41c5feb72f24493ee47e16487e7066f6023ecb0779032b4951d162c51e102b10f5c7a61d98de16b259192bee15faead8692dbe03f782060b8a17d27637eb2f30be3a6cfb38e13dbd3a79038a4baa965aad1ffcee2897304dba9c565bdf824c39d25f906f2f73b5deb6510df250f1ebbae5fc7c5920587342605f93ba324f02e4e130332d6253a546569e06d6021585ed6c16790c2bd684d7d6f84d1b37855a64b1ddd3f1e2df79e9a5641078e231c30e7e53ab1944201fec3dcf5154c0444e64ab44e9689f4de68ea5d91ec0d3a24013cfb514392b1d55d9d0f42b76e3a26d5b6538fdca358b23865c407ee2b1079c39b7344541b545f7831ccb9ccc0f714736b6d20ff0a0d00fb9d07415dc97dd753b387c9d3c7f0faa41879ec3f5d8558d292eeae6c2eb51f9972bc930b67bc8f18751953dbb34f30d8253d08a4478909238e5b89a77288524867b321eb0910851e2d75f3bbf80cf3d3f9f4ed544b2a1b371a3d4ccee445f81bd69be0226096b247e52889d74145eced54fbe904ec6457de6e6b4c21f0e4f8fdd8f256f2cb134c64244c1d8cd0b94daf5af57ed7865c052c64d20423808b207693c6a4fb674e3e1af80a3a9fb94d100d94965b41f75bde683d7b0c7a9bb04d9e0360c04cef55b30eec7340394f10962454857fbf83ad5f8cb72d35e539b86177a950744be4a43109b09379c191946cff270fed09d9f0b05d8e47fe641e3af84a6a20534b955c3bafc4a6341f048a1fd9953ef592dbf5c513a7be2744bedcdb3096dfe09a3001fab0dc79dac1207bf1cccae5093522d483975b932374d1653f5a5c60801" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "84a8be25d259aa3078449c31358504e39ff8eab1a0a566abd1672e9e9619ae35", + "proof": "9c20d6b70b4070cb03a6f18bb36e17dc613abd8aef7d909c603e1ef224c1fc361431bcca892ce1e2d20f30152347d6ee17ae1d6139ba9e60c52fba8f2e12ee65d8c58db781c82fbe44139e0ef4f6cf2a4eeea0d63b1b62215dd061fc7438995d6402a2e20c482862978d909378cd6d056b28eeb775b013859325fb53a666416ec10f246cf13408e27bf5fae87fa647f1aefa818ea6ae9c1d3e72eef8b9a8b605a41a2aab922d1eb7f8d11d36bad682f345b6493fe7f5ff989ccfca843fc850066438f77e97282d8a1e98a3c12d2f6d3021d6de2f37505eea72dfc5bb69924f0ed83c00cd860fc25a99e375f450672701168baf135014e4d268b0ec1c6b97e72daa64be6db632a7a5e687b296bbefe715b1cbb8695a5ef563ee19de5f8476e1080c23d6066223b31c20f0c9271de308e1df6731be3c2453ba45e7b7d5ad0c8851caa21b37e8984abaab6c5157d3cc9a0ddae03b4a687224620920b8613139c97430590ad1937f052254803817c12f7c2a46552122e744a15b63669d9ed90530495492cd3b816d3c88dbf409795d839f1a0a3f83c750672f78a02493f37e56157cd835a4186cc9e6bf0470f578314a8f245b4806f5d0b4918e6d9bc0f853f5cd0c6e0f4e6eb42f63877de7555fd9160f514d887f19adb99656fbad82b0ef2f83575cd0904c3e83295840004ece23544495c6c3034552165f2529e64c5d65503f6bdaa983397ff6781a0e34b8791a147e6312f91d788b59a61cf032c6cad754567716879f280b67fee111158c398b7d566e4c8d4be48c0dc5f58cf52d3059cf1430bea5d2388824a5ef84853ca40afeb398887b59bdc78665699c1c20d0b97ab8641e3bb69fead8d337bb23f17683db07c1bade5f879466e594e37e623d26b810048d7d7dba47d5bb21a5060a6bbaf8144a71406935a9d3a1188610930491cb0304" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e43d9e6ba07a566a9dfa2f0cedeb91dd3ec34058b4b7f8808cac74f63c318d04", + "proof": "e67cdafe04fe2a38a64c9a640edda27daa26d09890b8cc856003befdec9109551e94cf512c56fe28883fc53b5394e0330acb1895471a6be46cff9bb8e1877f256a3485b01f3b06e1e0313bdf3565482dcc0472f30d02816d89d63ef9105b2d55a4102e2743d966aa54ac60cbbd89fe69b4f24081c27cc345e295f0dc9716b90ca8d5226d6b94a1f7bc3a28b89601fb3a19341e20690ae1027874783122839c0d22f81d34268302c8c5c942a35ec63f96cb473b02faaf8fa7220c6e6a99e41a0abdbdccee193ce7f99ef280f3d23053860dbe39f13d8f03b9112a6461aa59a70492a2dbdf7542669a984a20aa7e105443b888254857c29e145c921507f11c8d28408e17e4e8fddb51adf10222774d92a2081b3a52cf0e44758c4b026fa7c58416be98cdd59db4a49c5f4f71298b1b68e69fab2ba3e6f9b6f4217645333b4f833694533c13734719b17a106fa46b0b3d6b218a360595d20f0abd354715e6014b1dfc6ba45f61162edb29bb1ffe4305eee1b18e9f2fee0563192b74fb4b4b565f31c2b360ae31eb444c5a8595cc5857f15270bfa4895ca8df20225606de086bc55bda9d7bc033fea91b665e9a2746db67bca47a22b2c207b55f4d4c8088f7c1d168a0c106d84ef0ffa77a03270f646e28e57f8288089d10f34e1b555ff8a7014e050826336db526b7e0da29d797966731add268cebd00cd7f367020602675174f08304bd14e62a77c7dd149f1b5095278e0d6deb789792db2184ffe36ee239414706c8dd24965df8b1931a49351f64697a6d024d49f282ea5078d19111226d8a001b0239c86c4a7eef01ad12e9733cda6cd97be5ff1aefb58168c78f35ce039b41fb99c6faa73912d2bfa2f67b54bea2fdad87e099efbc91829b8d254971f1fe501f1b4632b3dda93931617d9cd0ac9e67d1bdd8e53805576b4b9f738eef10d600d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fef2fef99d3ae40055bf4dbb654ea0d258262e6e71e7d972e054f7d58f2b120b", + "proof": "b82f18fbe34b1f978455f737b7626795218eaf2646f07099ecd04b96fadf4e41e4459e99071fc25cbde8039510106e918c5384d48de3f8c30f4d7d6c5de0dd244621d38c6b1c982640f1ddf999fdd548563804f10844500f4c77be7090e3e524c004aa1c71c26aaf900e686a8ac6e17f4ff46c307cd83aaf673b9aff0583876ffe6b7ec651b16e57ef344d4a612e4dd51b6b2eb4ac4263122e12b27873f1b7029031712b8943c382f27811712d8be71f691db9109f8332c107421603718cff02e6d40433b35493f63737e091b5460eef2df6ab0d22c284a9f50649d373c8ec08a6af3b6a8d95e3483d5f21551c1c215ba1f1dd8a611ca54a0df13172d0878330a2914b32b33724144b022c8c0d8901d98c9544a62e103f835fd609b8723a84399478d8aa2bd33dcbeddaec1085eca7eb5b186184eee76fd0ce5a6e0546d715372ce23d34ac59b568abc6122130721de16877ebe8ed944e5ebe9ccc7a9ff7602dfa960dbb2738611ed1fbc4f7b40c7c5db1fe17a9061ebea2bac0c525a305406dfa4eeefdc49c473ecb30d4230870c2c8d37ac37236a6e09231a070953303e74eb457448512dd6537f5072956b4126ebdd2ec925ba1339daff20936cd814f650d8cc6040012527199e7470cce51708bbbfd51cf171889daee284da8ae8bcb781282f7ff66416b8468811e0963221e5d8889ea8d07d081b807f6fdf02128f6e45f8247452f21e65decffca4bfd1ff379f89cbe300ca8ff5615ceefa0f4fa50646f1463fc93d8be94c31070eebed444177c5f25eb1ecd03f62a468b810d8414ae08a4b39494126bf2ad145d371518ef5633b4367db3c249354d290158d075aee9775c28db928cc8976c396c5b89981385945b0fce35611cd693a48c9f7a56121c08ce3a26bba64dfb8ace9730082cc3902cd65d65de18a96658c3544ced0b78cc08" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 13 + }, + "commitment": "303680219244c897d9e9cf5e8a0ec570fe8515e11d1b89ff7c4f3a3afcfad156", + "proof": "420b434b54d9c180fde9f7c718129e886c089a1981793171e1c8dd6d3b614c62dc542f32b5fdd3eac9480a7e790c7bbc7ce320062b1297b715b4aabad6d3bb0a4239bb9e2b1ee97616ece93138a06c04da637d2128a7b44eb2b7c3338c6d86492003c19965ca8ba22f46f548963c0eb6e776ccb044a89be2ca8cd6bba3bb6e5ba8cf44fa65ef6bcfa31d446521840710ba11f116267f21fc7a4e78c9da9f2506e3daee621cf9ca9f1068ce9df8afd4ea4939f77261c4c18c8b4f23bfbb78b209a52fdaf9ce54f94d977554a08b6f93d96d4f149087843b42ba3182332db31c02509d62edec9ed3add7fceaaa37ff279faa1cc66fa6aa3570e559f0b4e29abf25aeff14a9a13753d08f3b191f3afb17229b46261f4efad211afa7533a19581665486ba0083a91abc9765fbedefb7ebfdc45a7494ea8266d4bb0dbd6040f6b3c04e02dc3f04bfe62d674f8cb6ea319076919135cd0eccc1b541b77a4985c6dc229aee56ad22c1d6531e096ff8f60efd9bd8c37dc4e46e49077bb1ba4c43e9bfa7a38895df3e38894ac7f8b7aed93663bc3dad52e76a462cdf1c49ac5d362718b76d2c916d52d5ca6850f5f141bc51e2f18c00e4633a0eeae3e7ece18005015ab5b54207477d28b3a52b5cbdbacc47ec37ad2a1239dbd691d7c6c867dd079b0036cd0337e641a350ab95a4c4d393de542dc993409b4ff914fa7dc48cecb069dfc67cadf45b29a173a218951f90ac76a989680bb195873d38459f203007417bbeb4072b65fb759c1d1e1567ed74e581395ac8d5df6a8bd4ce62105961e2cad57675138acc3faf03183728b9ec3556ca9014ab3d07082603ef19e64b563b0f2d4b958474387fe6808e5ac2371d086d39493d0879da3eb60ea9095c9aba1e872480707138b8beea354ddf5c7dd629e5d9d091c845d473bcaa5f87b69a8212ecdcd100e" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "04a1e3ff0e6573798504eae5efe67fcedf6f7fed99adc343e3626ff86c1dfc47", + "excess_sig": { + "public_nonce": "9cf2a1572b89119e2caaea61791cf084b127e8b2007bcf1d17042e9d03fc0902", + "signature": "05e94a4f6de8381c837ef5e7db8d03c2f79935dd0e7d992cf4c65aab06865f01" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "249d81768bf69a3d670feaab3090f6b541080a99115d319dadf5b72eb953590c", + "excess_sig": { + "public_nonce": "20a597f663ca3e7a217e27635dd20a060955c4bafb910ddbe3e34e58df30256e", + "signature": "c78240229a3d62b22752bc00696c522540f0d067a1204fbee0c4dcd97ae1fd04" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "82f131a8b2f9d277e219a03425c9af8e0d97caef49c660b2a5faf7e9bb634d49", + "excess_sig": { + "public_nonce": "22e9b6472405369c3b8eb36492632fed7f51af9388ba2ecf07347aa001decc36", + "signature": "8211addd532e426474b9a9fbc04b84c5c334615328a9e706a6db6b17b2d4100e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "92f58e13b2383f68d43a218c1256b58e75eacf187685fd095006921d30a34e75", + "excess_sig": { + "public_nonce": "1e5ab42a18f531dffa5d1a7bf441f68ad43d4c3485fc444c480349b70ea54143", + "signature": "74077b45fc3f945922c2fabb54d2379bd96acbabd78c7f48c3ecda9a87f29709" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "c2c54d1fe300dfefbe1755ca2d5118636028a7e7adf436ba83c71788facdd608", + "excess_sig": { + "public_nonce": "eaf0703069b1aa4035d2e6409c81b03ad4d81a43211c6b93f73cd3c79e322c73", + "signature": "5c8c49498bd3dc7b63f576635dc3eea8724aa74e3b98cf9231b04c68f3fafd0e" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "e609b813dd47df5bc8498fb328c85e6911b7a9d3c7da960c6e04ec2d595c221f", + "excess_sig": { + "public_nonce": "4c64a333c19d8ad71852dbcfd5652cfae20e37c9d26f63f9edd550b125bab209", + "signature": "afa84a779dec1ef9445a5ef28b91e69f16ebb84eb62a2ef565399241c79f840f" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 13, + "prev_hash": "30383c492d8b8cbf32b3b9c2b5fc67886de33360d80275ad9f5f308941c28a2a", + "timestamp": "2000-01-01T01:14:01Z", + "output_mr": "6d3acbee8dda81bd624dfa2ac271703fda0dba2b5198ea80c6dd1a56659bfd6d", + "range_proof_mr": "12171a7512bd0df639acbe9ca1d22224aee4a8d38281ac7cb3b3e93f4e0e1101", + "kernel_mr": "1faec4ba0722d7660e8a3ac2d0907a7eae8ac357eda1b73add9bb83193cb21b6", + "total_kernel_offset": "13b223a53b2148d2d6f8c2c97a9166e613de94b9c3f0186370b4dfcb1e72f90e", + "pow": { + "work": 13 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0a862896b7f27f820e3e08ab61eb865645f066ca65d16d5cf4b3928266298729" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "12e648a1a9ea10887877c8a071d2f0e385ccb2c378b1d2d222a1f76dd42d7943" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "16cc2c76c3e872205cbead3e52b7a6a91ea3a2593289753e7bdac955fb330760" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fef2fef99d3ae40055bf4dbb654ea0d258262e6e71e7d972e054f7d58f2b120b" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 7 + }, + "commitment": "145e74b7e5d80ddb4a280a6abe7f295480ff093f1ca91c327ea05ec22402af66" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "12ad4db8f1ee9c21ab624d17afb33fccb63491ccebf993e296b1aee6859bff1f", + "proof": "322d4d27f62b0ed985fdc07d7576f8a7bd077fc37d3cc9c69f57e33d68eed7401cbf3e5ce81886b12781a344d6e3293afcd931d5e4920220b6f580239ec5e7643ebcb1e4eddd8b785c5542ae15120e8ddf1882d2abb0d5c6378baf737e83d026ce9859689e0f0b84dac560f950f6d6fd52ece11c91025c94a40066b31c18ad6acc43bdb6e0f46a153ecf25583a8ca99837e14a45c987c45e4a3fe8d5a2cda30eedb57772fbd9b9e1f51d86cdf6c9ee39f9b0eaf9a3989b2526a113a1409c1203850fa25546fc69cbe31ec1d3b8a7721dbcc0f05ed195bd6dff77029bfdda0709a083a85cc879dc552ea85ad22405e771714461e8fff910534e3597c5efff3878f6a58df32d020c53065c82c2cb100c29009597e984029484087f99391752b11c7a0c828b5bd03c5524969ce4a8a42fee9fcae7c0c335c34f78fb7b00b75b9863380babe3283fbf26750535f7e9e1ca3f860af2328f6865b8b82549d07105303a84eb86022ec22d5a2db18b29ddba478965fbb5683fa84018ac5b9e4c347b1a34160ea3e740a09f49a7770c11347553952222115e1c5e21dae912516dac3be02d4a985d15b62ba04cffe247d0cfe5551c4d2f463ddb640f33f93339839ea3026d76f3caa42d6f0865332af40df397e75cb7ead9b9a7c70dc4e455bb6cf2438e7fdca3a9aec3d57031c5ce13875590f301b439b988c62c5492662b975ff423ab3546de4bed6d717f5b0dbd0ef0a721ef2552c5b22a93c1e0187a586dfe53fc7731c27f7fbc20b08e5b03ddfde02541d0f3334c0bb950267f1c62ffdbf8b05af45246ae56ebf039117519ce7ccc078e7b5197dbc338ec60328fafa06793e5e8a56d59e67c38dfd7134868059c67bff75af85738315488005d4d7937589fe2855906d8496c70082524fb6a98becc461ace3ca547cedaa06d5681d7e6396d094c5a0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "26f0822eaa9e43a5817fd1bfde541b6a1f49c0840a43183f72cb0be61288cd23", + "proof": "40d7da7c32b3ac2fbd717b5c308c49b64b48743c533cfcf131dc7704c591f13eb230b7ec7432671e139050009d5b87d57fe1b406b5039172decfd06ffe38ea6146ac317b10204abfcdd5f2427ab57cf7d857be3bea0a22676a8b781f8b8d507a70571aa30c5db17b60c5ae9654911b4688dc1673e99b925811ab75380321973483f869871f2a0e3975e85876d1321183bbf55a617c3dcb220c179f8efaacef07034142acc2bfd851f4d7d7d0e277f1d31ff716cc87c711b572a66114e59287026158ca4bbeba2d047792c63465a9adde0cc947ced110fd58195b58b32f7e490fe0f335f3c6692c2b4f2bf0cb91ac622d70957590f7f70d3cf9dd55fde24f255c4a10553ac93ee9f914c7cde54ce38466dfc8ff53e3f7b0888f45f74eed8ee75944f7fb6e254a0eb2e73d2c53b96f2d379e61e0bd7f0a3474cb228734cd524d1a28ecf8ea5a68567896f4b305083c047ad8a78da0d91aa0011eaaa0b9e37e3c77b8284005ac75fdde43323b05360eccdda854883388bedcd58e63431812d5937a3e1259aace5be01badafd2e411e5d93a1ece21a8abe0ccd4521a5ad64aa8e8372c7cf38b31c49f157efb2a7e2b4d2fd3fcd8c9b464358c30ba3347296f323731a02a7db7106e4ed901ba97ae06c4aee599063e0a76946603c3f0a038f5a2cc6f588f1f4b790be69891783fcdee5810d28f5a1852c1afd1fe394a315902dc6f18f661ef5ee135b1d3f52ce1f2f1de9ba23b36badbced2a2e8acd936eaf447f11e524ae35b5d47d8c6df856b336c1af8a06bb14ccd7deb899b3c10e00e936c4045342706480b75326e3adfcba6c1139c66790e19c5bb1f20d1a9a4418875299d765fa737c0be27c1c3cb85132c05dfab499d7db796109b401817f62029fae4130660cca44b0f00795fc79e374c0a89f6332dea95f772dff8fd7ae2ae91a0de440d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "58528ae562e6d94fe42c1caf7407ca251fff119ca873a97fb35bd8bd34c35002", + "proof": "00dc8c8dd26270f98ace961f7f20157503a057ea93e218cd7169053ddb515017028efd795aead97b04c5f8621259e8baa09760b54d2f333c23a0793b030c5b475a1073af55d9082a893af33ef4de771a7991fcb4819255ed590bccca4be0204f18554511a2474e55966fae759490b63f9f6182733b086bb78eaa6176d49b374e51f34358b8f1bd81957a278e0bb2467aff651397c6e8b23d7a4e79102fb50907c86639aea34dba42831365f7f6e8e8145be53f3a1a6c468f023b8f6ef1bf9901ae1813166fc3b893f99533779905eb6954d5c44b30306bbba8e4e1be6cde0004fc53bf52e192adac4f97ba9598814714fa94cfdb25ea8fae7da78783f320513f7cf4c3de8c75efac4cc512a851e8e7ed196c63e3d00bd402c17d7d3b7f64132dbc510abfde56b7b1042578d9a6db90b00c1da69c10209c256967fc9bb39c6e1bb438e0d6a15be8a8cfa38703810824f525cb6f71b1e13870d6860ed040b339649cf30b850a0bb8a4ccb98051332e00b691975ed3f97e5d75badea06c8d64cb751427afe6a0c3b5be653b7644a9e4cd34f3f2ba0e3c74cbff36144155be24d90116095be036bce646ef21934099cbfa67174a7048ce6279da9754aa1efa366104ae8615bf506bdc816c238de21d109d70d22f181f6f57d7feacfd56289f385a58a83844b6a558db9763045d6a58d86e4bd93c83355351647a2c861496367d7945c0bf8373183d97689e5be7cb1f30ed5b4d6e4f31db1f83e49649490549be0d36d8ad42caacdaf76923153890e899549cc4c292a599b44311a6d19f2edc417d1efe0df990b8fdf06fa910767b45dfabf7dd9225b4f60050500a1ce2848a910d02297c8e339e66f0891e1eaeb973943ed0d7fa0dddcb33dd25b767c7d7af38d202ff50643e2390bdf978af1956a758dc33360b25ab2df83acb9e6929768814f80a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "845188c596b0ebce5b5044439c3d174598361c8aa0d87d24b4d17359ec969d48", + "proof": "26e2266646aa581e64ec9b1dea14bc78d5a59df90e821bba79c50614fcdd454d189ce9c862d1efad02b2b1dcf4b4a1f184e1d1a877f5b6bf20be8d928be7eb3a1437f5189e896a898a4524f669489f79f29e1acb97421cb7cb985e5becbd575d246735f6969841a5ea03071401b1a7552832866d00f9d01cd121a3b34b696c03b62de024f1ac740d1dd6c465fdc6e217e5e225692a66ada0aad7659a0877200c0879f07b0e00d7b9371ef1e487143de912e00877736fe997ba3ff1966aa7ea01255ffc94859b79776de2ea376ae33deefc5e0c3ba09231c5b55ba28cbdf4780c24634a92f1d7920d4266884ea64f9612aa218ddd57ffaf0fbc4dcaf7732189780ac8058de81550b21b9dc5728c8bf9bcf78f550c88b4c7172b31150a8f7cc65f1a0b89504cf7df188902cf4da050146e93228272591d31fc6862b122b2fe9e0d10d8349552c72752a5f0b77b3a268391f56d8acd6901b071f3b88a3d65c7d22954e2597fba71e1bebc346d2debef152514e8aa3ad539f3a2384c22bad8d84400744688e571b16e29be8b81853c572d96d0a4a39290839ff849ae0d9f66db00102a187c78b52d3958859aaa5b2fdb5701343d4beb7cca7d3682e2d3ee73a24341b4ee51b9171d2ae1eb51185f8fea8a88645a9fac086504b7cd03aecfa576747c3865ddf88e1ee10f45f6605f356d0a3859d25de4089c12245187030e11b5be35d41d2ed76a14ba6c46fb387d520733122041b8490f9c3ef2215a1bc86b10cd3366ac0b8f89f3661494943074b4cc11ac2daa1b248f3c377c4326fc296b99bb392cc1b79adfe9048891e1a60ead2b45f158dc65e36ab26643afa28ad2a5856b2008710ec28d2810f26de2fdcb1f1c614aa52c3e5ab50d9d7ee509295e84287b0da3a10a3cfe5a9d286d554c5dab721dff6cbf5c975a9a43b5ad8d036a0a5f6305" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8e5c5e7993047ddd1de118aa006a9154964ed173146d5799d8b7b22fd459ba79", + "proof": "10dbf7bbab5d0880aee5a7ed977ddc2925aeda2a6d7ef5a46333458db81264528a77711afff27a7b14314d9938a66f7b68a6eb484456ef6a20a5b8ad93dd55638018f283239aafcef3bed22e2b71f966aed793d5d582ba51c1fab031d369155a3abd7ff2268a929664c85cf4f18ecd0285089d515dd168253242bda384a0eb0162b9155eff3a8e2c1d7398de6037cd4d066adb8450fda01fa4e89d1dd1b42106099175d7908a717424aa82fd5efd3b90b155c7293bf97c536cde45053807eb08a9e9ba76191d1c86fdbe288948e219442a542443c094a6393700cdebc1d19406ca0a72ba9921596adebc0e9de60c3c0b7c13b6391c254edbeec97838e96150666ca3d604846274abba965c751de5845a7f2aba4352de75f04e82e6880d87d3024ca4a5cc27a9a87fe802d5330d59c11d44b94121aba85bca3e98766434d18748847fe0e1b01e73f63dba7756ab2be59439fa77c74ca6feaa57209560c6762f21b2cca9ab1b3f4a959d5a7e6c6f04557d24ece4b4fa162e2d35e062d55c12f1251024f20b46e2aa3db553e32a5036b3dec38d9c4d7b8f1ae1c49399f143fafc65106e57ebf8dbb37d3a73c1e16bb8cbda523f1941b676b0f0f37fdef449de5d1e2ebe20fcc940616594f96d0a4f428ae77c7a4b5a15058193aa37a91e30038343785234ff1c80e9668624786ca724d7b6e4b84987db53efd32edcfde36f7492769c23e5100ab9c2f264a37099c93546e72f3b01d008e091e1a1187b723be1830c7ce3d97030a7daacc70a14ed547e1d31ef67867b9d39fccfde8d923fbb8d04583e4b40c870fbab0b1f5068bfac8bd387abfd8fa7a62217a1476e2979bb854956d0890b90032393814400591740dca721a99dc920e169a1da20fbc0bf8fca8407b9e4ff39a68064bfdacf27079e96983a4fa6c7421fdd43a00a030032bf7f3f0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "90af391bb151b71e2a2716054f29e6a0b436360364c20dc44bb5a4ce7f8c2d5c", + "proof": "f8ac2a122837a32fe3436b2274e8d45e6863a4755d4c53a388eb03fc3fa76f699c636cfc65933bc13a7aece8e4f8346781587a99cef842741b35db49f53bda52d2c7c2a26abf2077b57b12af016cec533afca30a6c264f44f3b5089919c79a271cd0ee250bf7aac166ba66d430fe3281b0e43ba4eefd7fa0f4afefa7f109f73f7ea60f0bd7ddd35459b2e76fbd88043f7c787a1fa9437b15c7a22db71a38e708166ccac966721f7b89b7b3c41cb6a2804bc50a24b48f6280cdedde814cf7b609bc2b0fe9022b5109c5b82978b7b41fda51db3a470a239fe51601fa35bdd544068671e74a8fecef0fba3808488b6c22890c814672b66a2ca06b8b1259e812583502a5bc4e7a488db6b938d0957c32e5ecee016f06c7b7be6cac7ed1b71fc2ea136a62124d09116ac7014f20b5d8bfd3e105ddb07b24ad2256f245a9016f66fd40eac8197982ebde1df0978edcacc7c9ff709a8534de60ff2e2f90bb812e059a77028a6dc9cc04736747b283667008abe7b6b0451f12b3d65eee9c8e71525f902c82458b3b02c1ee4214d4ee503be8b34d65dcaefd51dd587345348019bdba240d883882ad7e710f405eb246ad0401e76d84ef85d6dec83742aec53cbb238dbf0ff0463dd99db5687bc97cf2b35455af74ffb7067029cc5969beba2ed3196a1404e02f2e6390bf085891f501909d7dd06b796701d5b13c0a431cfa7eb8f29b892a0c2e61ec1fb790f9654690397199423e7ae7c1058d2fceabb4424cfc40bf2c65d8de9ef43edfa57526d687d5e15573532fcbdc714fbc908868621e78ad945d40f636bbadccef9ac7d5bfebc9ffdbb1b2e9342e7ac1263d470052eddc6f9f626a5571cd50d6de0f25f9287fff86634edc5b226c7d194f17210fb5259e52013d03163129294783f876aac415fe8d391ae4288c9e1f91dca39bcdc6c7beb3d3e007" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a456ecf3b718edefd05278f9c68dc17c91ed649538a732f257bef0d65f995a01", + "proof": "c83ce6c4a10845b6971e42275cb5d4b0bec310885632bd9652e99c392427346cbad861ca22c7475f1c7cb1310facefba5018832c2d85851eaeb6e0343ebcee14da047c94fa82914794d7343755f9bc57947cc85aab361a437e717ad4eaa5e942be420ce86c5ac6f4fc69d9d3b2c9063c5a0282a3e4ecd22089694721b1d6b5178694b0eae0d6e5d460fb20a2269d45913a1b75f35512431dde7e4eb435b9df0b47247fe03277474dd2227915022e5e338fd02eb194f345a1418a6d0f8aea0c0a69ca181e374c45656ef9989762b831145e391173a415c9a5e065d8640b0a68060217fd828f3647c1bfa660b3cfbe5806abd2f40511a78fed55fd0701b1e9c37c7cfbe122f61c2c40f7b85787da9e32be4681f9ba990f4024cb7836d34601291ebeff650cae1f5e23b75d9ce022fcd64c19cb2bbb28e9d408171b7bef6ea937705c77cf84e348af486db2daeb7f3f02eeaf8229b0563fc5769d7460c5854c44764ad747c8a1066acd1d47a26042284e416b4a463c93064036b1c5c1d64418640de6df08ee1222a7d3ff028cf0264d198f65650c62def007a3369e91d4df285a08f08ea68f42ad9746276ab85bf0c913ddd7f6a1aa56c66bdefb96a41d634b750d982129b5cbd7ee1c1fadea3afe908c2b8383695c90e612263fc51451bc7bf04686eaea2fe16c86736034ef73388bc30034ad29b7de06a22b09caafc505fc7f74dadbf7b353a40b227fee987bc8459a9ad1759722d16b346be3cd8b94ce142d12286a42de7008167ae3b98ce0354c433c9b314bd1cb542fbf14fadf053387453d62c65ed512b8895fb188eb14dcf037babebe8d76e087b3b1d6da5316843e36381195667e00929540ca0bb68c0d84ae0cd0dfd7052d1d64dc560d41be0f8a69065587c081bb76d10a7a2ef8ce573c1074f6003411356adbe730da0186243b1e03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b000abd87d78211387fa2e41b2d14038ada3f869e5839358a74e09e16bfa8f70", + "proof": "42c5aa030fd20b3e0bb1baa4610586c46a93731b418b6e7bce0b69905290040b92d5ac2e48d89d14bf01fe055f6affde1aae0e6f9c8aaa309d61255ad2144504d4660ed6c50134de22fa1fdc58f4fba0ee6b99728b07a058907fa14c02bb322ed6c32f4a6f95c5576b2b27c5e0cc2aa9e157a8ec59bfe5cadd32dbee5441e646457b0efbca0591a4aff7e94608bc825284a1ffa02f8fdb6c7065595de67842039c8b2ead186370e266e953fe593110549e5d6a678ce034a7919ced06b9e5a20ceb36e2c2930a33e6e664a4fa8922c4a7a1d6376e2af48546823a1d93f35d3e05e453ae6f49a412ce79e3cb85c090dbfd71fa2366bcf2fed933263e5da260920adc1b873f9a278e2b773add937cfaffe236fbc07d8319bf9f60a7825fb6e2656cc2267cd3026a2bb79f282f2ca838ac3883f85de5faf6ff281921b9a3e285583ffe77d8fef91cc8590df7c9ddc97064e78d35aeec62c91d2951afe0700391fd02ecf076b8cfa4b8e130c7233403cbf135d98a6c7bf7c901f0d9b065d451cc9b70261197ad44ae826062228e1c45dc4505a7b20c2ba8d58b9980b07fc393ee6303bea6ba7e81337e609964c08b6fff6bac2b9a02de9032be52e64c61cf3935380e12c47292d883d30fe02d596643b7c56503086864287747b88f9eb9770a5d3d1b681a185f305ff745a886e8427414810dae155d37d38bab072bdacb96c489456a9aa53ac5f154d5cf1799edb919dda918d4e1a2d9a15de6f908bbd415086c0848320fbd9a47feb0ca3a2f20949d2abae1fe4fcba51f2a61979f5113846660a5682c273d01f1f7b3c9bc6f7714023dfbdcb5e3db51ae708e1f401773840d03784957c39c02a7c60bd35a7b8a513585bc837884415c9a59795d198862f19089a80d77ecb214f6cc58afdc512f7b1278f8a2fd43c28a275adeb5e2956263dc12af0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c2b5a07541c11a7700030c79e2608fc2c10a4750c4f08938cf51d6cda8238944", + "proof": "10e043256c0e1a5beaa1fff6d50fdb4266ac96a643b7a782967635cded21465ff8d103a9d786ff15ceb0c83f190c6902ae646e1ebdeec6d0f47b53be062b5b7db6e37341904db504ff419b3880757ed4bdff4ef36da0b7384bd07b013324b40aa2de938079cacdbb2ea84bbae28de1aa19a77543cad5272989afc533e78af1556777f0fc399947592c6259ae64b565d418cda36ef6dcf410e25bb88464980709350fd2d2c86b1dc6f10f6f417e6c8116016bc507db8fd54dbc597ad4848c780f055cda2b13eab4efbe1d7cea675b5af177ed122549206cc154fbf6e4e537950c70a5c2fa02a9b73ac8aa99034abf65929d9cd2d7dea7b3ceaa1cf6ede2b8d96f3253a3aa2b961ac297c49b30b054d90a8f6e160caf7d9ba263d04ae3b5427219e23fcf164d0df6c6dbb796a33e6bf99aa260add3679139bf7f688ee7c9cabb78d235af6b84cb94b26644d66018d008da800528bc2c5a615591474bc6733937716c9fed88362303812b2378b11eee2e3fec373409203195708e970813cc95fd5f24fc6f6bc799d5c20375a2cc2ac6676063a56f8d0f475b7f7586c1eeaf838c002e6b4920e72587b5af309daa3d0202c4972c0ca61967eda61c0bac6b8104d00db0f30f85f3351ede39052d208b55f193dd11e5d4a807fb50ba7b330288b2966fde880233d4b652e900e26722974924157343078ade1491e58a47dff2a1deb75a18176b676134816631556b7ee4f1f4d71a5a13b8938308c60a214ca8fb6bce0620a94f2d9173e889c949d22f5381fea18f6a5418f198118da91b7b8598e7cc3c8260c1e60ce499524c82bf78e7c6d94f2453613868f53cf9e7e992271596b834f3470583e359c6a46c297f0af0d370d6c80a6065aae37705d1e9d0ca24c9d00a229837f3ea9d64a83766bf50091b4cffd4e982b4444137a5039b50f18d876800" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d83c2995621a6c2f35b462983e72395fb18655d02bd82e4259996cb23323b129", + "proof": "ceb98940d2a4442111c590fb46c37ebc0ffcd5cb1cdf50e0900f46ce3c671267dadf2b907237d8ac38c19f573e936bbe9b8b531ce9884634a0cf7e021874aa73eed34d305bee2426051c83d7d854d3cb2d1ff2fcd1b99f763d4ab956eabc1063dca67748d50560cc9f0ec8dc7971bb617c595a8210de7e98029f99ed0cae3842b216286258388929a488fa5a2b91bb54657169821b8c8bbf53344c3a7cd378013fcaddf95ad544bf02b07569da1dabf7910776f6d1eeb131d1cb512fc3fcaa0df47efc5a21914a20d8c5ae00a0b06b24d2c47d3cd7d83254aa9989b690ba170416a50c4c2aefc397992f8fbb68fe2ebf9f06712a34d8b07cafc3c65451150d03ec7803e53c394ddaa1819010035efaebf25ad2a34955a2873f2fa049ca6b6471048f5de14e0dcd21769f58e50764bef341808167788b04b8b96431030333ac0ca04b0a9aebd2bf28084532444ec4d7345fdb8a855f52c61be2d4660859252928aaefe669179f666b7e51540d1c75f95fe8d7d08ff63c74caecdc23b8b8dbab3924cd86e78e99b3e40b42de79bc88b76428b850f1f9767f74aab9cc80e3abb42512b254736a5d0468337689f42641a089782e18de3217f0b858334d5df2e03b2ab6bbcc302f6638977d74596e6cfcd1ffa0c346d12fefcff1c15a3597e4877d3b38ef2445fa34c98b492940107430bbfa11a0573226ed16fe01f424c2a6f43d0bd4726af0295ffcc629c5783e22360fc0382d889ae3ad1b7819d177306ed737022ed3c99256b211af293ab8c596196a3d402158e2739f0d50f954fdac734d000a5e7c665645b1439b7e41537999e7a647170655c138a84cf0da2eae21e107ca42f06e9ff92b349f51f9853730185c7e95bd89e51214342dfed662b3c2a6764b0a1896147f4cea84ebc426e900987deebb2120979cd4316e90633722c4d2f6990b" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 14 + }, + "commitment": "405bf2c2f984f24bfac6b242f9b51560b1605c2a110ec907799a3882aa670e39", + "proof": "b26ff0694ce0f9c1810d078bafc67aa21494cf61f067333ee00bb785c27e2f19407112e976d95584d2e09c64a0e10abeae8baa35dcfcf882e3d124232c103669627a3a4d42276f300c983bd52dfe78ad3f04ac71fc067f6347d12f711f08bd0ad26df64690405f41704328e7a8299663c6805a222d0a6098aefe7d507f6db84f6fa7b022533103de6154ce9d7b8b9fe30b505542e2e4fbaec1ee5771499db802126993e508d6377e38f027f6789d43f8adc587cd9ed2c1e67eeae06a86ab8c0fa771dde26c6e66e64bda1eb8dbc12252360863474b84b23292e0b5179be25c0ace778f20b2a416c817a2b0886f6843560eceb22fc75dcb1ff32a96b15160bb1388bca60c6ba990e4e7192ac86b46d66d82c29a13482de1936589d65c31101a46600927f4b6560715d7864f2b638288e92e7ff20e74f475d1026a093c242b2d75825db0761ef495ffeb540fffb79a18fb55c14e6373598bfcd25e3c80fbfde11e5e24cebc9b5247d2d9522e05ce9dbbc30fb8fa4d988828f8db7b0e185a27ee6976d416e86623f27bebcffaad4f98867059b8c876eaf3bf563973bb4920af9d77b29fe62475a529e890edba27a0697283a1abcffac312f1e5dc4d4e594e2c182ee08e0eb53c28558688e0dd8bff8509b113df01628cb49529b47f3c2cb470a8508ea5e6390793dddfb8f5649d4571a017d872644b3be510855aaab5f58a21d710e202b4ca8f71b75c4829a7bd180cedce1e4c60033f99e84dd3d4a04bdf6fb242283dfc92bc0f51c660f94970a74780a048a1d5327ae755b287731653e120c526041abee79ddce478c0fe93a1de1639cf96334c53a9c2da2ab07a303d7c096473d96e0196c312e334b71cfb6547bd3bf70807d0f9b769439dec0d14b0754ab504e2e1a50d4f5087f269678199e051fd78685d4602ef3a57e8a8ce43d98689f305" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "10a62f6e2c9e1802cdc80557c5e7ee2f6aa2369a121715fa17cec2798d197f50", + "excess_sig": { + "public_nonce": "54bbaee97b9f55ec23983a3316772468595b268d177248f59563e2eca4a08349", + "signature": "8293db0c43d72654a55ac9b4a4dc2f8a2c7d722d91caeacf60f2ad1a059eb609" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1404ddbf161abaa6439113e10399a1e8c86a59b67df25d26a5ddd53581e0ca52", + "excess_sig": { + "public_nonce": "3034d5a3d0e617b693e270eb4bceb1d7d9091eac4c27809fb4275968b0d8c740", + "signature": "3116b51607ec65afe5ce361d6f9523ef6e08dca8c29d114c6cf00e087d1f2c06" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "826c1a48acedbf07dde15e67fc5d89a6c24a251bd08f734231e8c92d449cbe2e", + "excess_sig": { + "public_nonce": "b212829d3bf6c2a1a6fa48b0a3a21c835b751857ed72dea3e1e72cc1ecd0b03a", + "signature": "fad4c1cb1c2563beeb918dbb12db960bfc2ea89040de99ab5921ec664569640d" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "8c6141e86f9a2ca1d0f019edbc98aee99acb927ec374aaa3fd10995b0b86b667", + "excess_sig": { + "public_nonce": "b6ca0bfa2fc6eeedbe57d0582095497b42587d2bf0368625e03a6949b99f4941", + "signature": "3236fd13e31483e7465067779e283dc7bc3a5ce4b63c6cda7ee585620764530e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "9e68a01f3d36c58a2f7e2e9d9bd1531d3aa978f011b5f697f111c08ce92eb203", + "excess_sig": { + "public_nonce": "e0bd4d2206889f537633ef3e91e377dd7fc4b9ed921a351247857ebdacd05d69", + "signature": "e7e3a1f6e825bab3e671475104af918f833c5701a3d702ee522454c530777209" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "ba3d3155f2a30b97a6eb970aa2f61332e204a08be302eae3a0ee8edf274e7e30", + "excess_sig": { + "public_nonce": "005da323f50f80dc1c2fcfbd5729163c52fa40f878e5a66b2f67ccfea5bc5a7a", + "signature": "5ea9361587f853a3ec4bb59e5af92ea71b2f2a8841964e0baf03777262848d05" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 14, + "prev_hash": "ba66a8079cf2f6f537ce09a6801b3f31074ddf0bdd0d7fe9559118a39a270611", + "timestamp": "2000-01-01T01:15:01Z", + "output_mr": "550254cb374d0591dffc9b2cf46427055095aa20233f86a0c9df0228fabd2c2e", + "range_proof_mr": "a5efdf52f28030363ec10365cf637221ffcdcfcf06a419bacb4f8cf63d720e28", + "kernel_mr": "5eeac9d60c8257ab410494d2a7657fe4c4bd061f0c769107044099b0e0f4f032", + "total_kernel_offset": "99d5cfd0364fa240c306cf88e539f94e5f4a31c1b0cb5151c40147e6d1f78309", + "pow": { + "work": 14 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "26f0822eaa9e43a5817fd1bfde541b6a1f49c0840a43183f72cb0be61288cd23" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8e5c5e7993047ddd1de118aa006a9154964ed173146d5799d8b7b22fd459ba79" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "90af391bb151b71e2a2716054f29e6a0b436360364c20dc44bb5a4ce7f8c2d5c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a456ecf3b718edefd05278f9c68dc17c91ed649538a732f257bef0d65f995a01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b000abd87d78211387fa2e41b2d14038ada3f869e5839358a74e09e16bfa8f70" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "08871a8ec9634adcc2193bb575e3727ca7f3d25483aba1da04711a2a338ee543", + "proof": "4a397b16ba407adc853cf42a5c1f31ba430021d9968dd0417aa190c812d51404a8f539c07ce08db0882f889412149a4ecdf74af34c5181ff0da815cd1a505c03f62f9f33d2e0321c9f64a31817566eb5cd02597e73ed5ed58098720cb6ae0f5390e11f57b802aa54844bda0df592160f9f16c7a5ce3517913bd948935341700a54f425c03a83a0bed87afc3f3bd6d81a4dca3de29368c2f9006d125f1d36b100e3f96be76b01f6c7ff13930e48d1fd4293e008e71cefdcaeeb431817ea724b083f0ba3e63ce3f80b35b9a3b77c2223acce0cbfb18aa8303966a83fc74070cc005889d2b778d461ef3461f519b5678b7b49b4e65218d590fdca7dedd68f6d39363488ba89f99dc6b53215428703e19ad5532b1e6ab9b2e5b95593306396b8427a6ec9e07e1cbfa5c8912a4933842f741a8515f816353a95a88f69bcce51b8791ac873dda783b85cb1cc422ec22281ddc44bf1a699e9ddb0431d9a4203e05d3e6540c03505afcc0954b95adf85174dc47499f059da8c64cfa0746e20458d3cfe4110bd54e9f98b68330bfebd70c6aa30fca5eae1c91da4343f2ed8b3414782e52b42dc9c1c86c9aebe9b6b87774c871b5fd2dd0371224328f70e5b6306da0ab86c085dedefbccd0a1cd24475452ca14772d6e60d7cc60e9f33ca8a73428819382b40bd60c4dc1116013fc180592447764921ec36d18c62b528e614c5ed4dab123fa69164b525130b3662273cc9c789f6a795da9cb4946041305a6fdb074fbf7553aedfa71e0973e25865b86aefeca0c2389a712ae62c27c883a66c1c0c41aee21dd88041fe8a9ec4270578bbfffcce7a3e1e326c23898c15c812aab35181137e1f259cc7d51d5fb83434fc86ec6c4f2ae23b54701b4ecb51a693cf3575f87ec20952f8d1daed6c3c5901b31ccbaebbe53dce0e9dcf3279c2ca69068bf9baf60d0a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "207c3cb882ffa34150c6ce4aea0050891751898a5be647d8b23ae3c3414a1c0e", + "proof": "403de5ed22f4701047345ff2dd27d68faa41b77e4cc4bf2793b21993f90b635fd66d778f0f9451e2c13f8ea175553d786fd270adb0b5b796db7ad2ccfd904c42bc281a7409a3846e5ed88f3bd640017af7ab89f9fcc91fe0a353a97e4ec9e4362409f3ffc537cd1c41b1b417e34dbc5510c0d8e854e4fc6a72d1b75c03ea796de6c300622300ec286907a8d11096013bbce616db7d9383b927369aa1fe06c205a96e95c7d95523b1322a8b953376089ff0dff12bf561f66f3710d0356c23f90d542d847340b6c7fe42e7718270128426f3621f5b8f9da4182a14444d74f25c01427d6840a05568edd9099eed629d4d1ba7586e61101ca8c532c05bbc97580e3f5262c6a8ad7928080964d5d90d7b8871f7b4a0849946ebbade84c0b156656e3470e86a7083d98fddf342d9cb2ad472c0da04bc30a362794ae77e5e7c611dfa4626a62275db320989d72d5a1b4b82371914a1aa0be30f24a35cf6081b2e08196a5e32327bd8062146407c9b8079ab58beba47dc88832d400637a74390e1e759070247caf516b192c29d1851c6e67481821e9b77b02f32d4b22241691a9447b54638fbe5696c05edd11b18ad4ed3ffc9a0131a91bc912f315689f86b2bcfc2dc51e0c139f8bfaefcc5b4ae055e0519c4b42c877bc1f3a24b5d7196d50d9dd3480f8eeddaba08e710e86127441d941f9db943178b764d6739cacb5480946f614724a65f446da3aa5c3ea7cf0ef82e32fab4848dc415034c2f3cf90ca138245aec327c6250566a5ecd156231bbabdc32167ada0081bab9e7de01deb2cf6a88799a044ad4cd011d960e6ff989ec88d312a5e19c740c71e076b1e863c5b3c1b3668417860b535eabd7c2ae5fbe09a0864fbee493187807694b2685e9bc609ed7f66f07c6ff36ca68d9bebb7ca72f651f81ed368c9fd506b6fa02d86473bf06a4528701" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "70b8a8473a00ccc41c9e40364fc724be6c35a0c4ebaa9862a50f6d99fd2ed64f", + "proof": "8096025784f6cb5cc61629db616487e406419ed5aeab7578937cc84d6fcc95251a53df0b56e5f1779f83a52c002280e0908aa93ac6eaf65cae9f5e91e84e3c3eca4719293278f2e112df39eef33be74137766c5f6e662066d34bcca0e4a9d50aa6acffcc0316f3d3e5eaebbf9e6759b3578b1e3023135dfc801834b9b3ee81092583161b54ac2ee0b52f35fb2d481d7a4e01e44c3bf84290ece1b3fde56efe09513ea78dd346fea8fab1178ba1ac4e3e7f5831cea00661c3d8e184877780050394fc0b7e81569688c4558aa59cae107d9bdb7e8e4df62908ffd5936276c13a0b5226994cd76b8acda86e699d74dbc5dad284c24a310bc9590ec8f1fd15635b37807e5dfb68019e7b8066c18c4a91c8968c47b724330541f9f2a4acf43c1d196e74c8488766cfc17d22cad24dcbf84176a54834e0a44827f7736ffc9c2da53a42b6549a59fd4603e1bf8f86a687ff713a532495cfab49ab1a231c484d8facc44ee8d211399fe5efc2b9b8ad565cb0cdc0b80682a86ed8859cd129bce74ef7e443849d0fe3e0ee3d76c2139a320f450ca5e8ec684187621d2aa464c6b6a021d92510348e049024c156cfeef89503b6ce7255ecb84a46f345e610438308b266d750e0cc1cc35766a6fac51d876fce23bdeda81b727ace79c868f3fd2578c59c9c29e6eef4a9e107c21e8cd3e667476c6cb4b8c8bf732115b6bbcadf63ab884bb375345e707e712a4d93a88710e7dbfa86b07ad8f645a1b510fb721dc0e5f42b4e5dea4722b6fa7e1a393ebb00e79bc4e5ee77dc0aa51a4e822048f87da3ef739b39d08258f25f94076ef3f6165037a5abb7545f6b5713928bba4bb99b2d28766e2e8237990d9a4ce9de3ed9384d20394ca17091b765e7d8bef20a066ae1ef44c6095421df410e8782712e622d1947f6f6c60f37fb180e1ed3441e3a50e95b06ba00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "74304d9eb9e04a1d1a4104b1aafc6cad51ea627d948561e1852113f74b83d374", + "proof": "7ec5a262ac0f387ce7c26e53938172c7defa1ab8cc5def9d539698f3f7f0cd7b3222af9109fd79f944e644a9313b72229763af3bdfc710f6996fa54cea556d781c53631f5481a2209e25715db3a6b8235c9f93ce34d93daea7b1d8656e8bb408de65ccca261ff3c06e2dd8b8d9d8cd242c65e385309a8c1e3e7de6f617c9331a3e3740183440d66c0b4187af9716c36ff23af4a31da8179fa9602490399eeb08e3e50a5a078f29ff78d62ae6cbeb72f119f305e4a94e47888aac0bfbe62ff90a90acc6bd63f5c8151dab416a088b19adf2e04b06e558cb7ccc6bbb9ebadf5a05a002ee69e107adc77f6b9e009b34b0839883931f4369c3681b6f797b3773367c463d4a2d1284fb43daa64ad20db210f3994b1941a803b3f7e1746a8666ed6a432e3bfdd410a7f0621b9615e30847318eaf5572f641bb46599bdc6a428ddd741260edf2f36eea6d5ca8001b9158cf45266271a42d7188c83fa110f85ec3427d32a243505cfd24470b5e9ea03650bda54bfb60c5949b64bbd43a337cd3c3b176324ac3dc62a4ea912685f9dae28517e04702ae27458dfdd0015b6bf1bc45608d24be2a1333815efdbe721e851844971d066db0d943f34cb7610987337ca973374aac8ef71ff128155e1bd5063d00dd77aef430b2a70e6f1f65bf9cbd7aa2aa692ce6649f9d1f6af0b32c2eede230f2cc4bf7c217e9de347d6c4f758074092b612262af0cd7ab0fecc3a78274c3a9793aec58bf013ef88d514ddb757a332b9736657cb523918ee35de3635634c64a273f6fce04e8e5f604aca097aa6b041052f11c20274e76fc3aaf9adbd622ec7cd93220051381a7ca3c9d53b5fdae88ef785755fa226b208fc1bb40ef52d3320a4ff0125aa57ec1f52032859e152606684ccd0c1847ad7d5cbcff0aafbd50aacf78fda58c8242d65cf0eaf9115474e5f4a58700" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "98a492c0e7a54a6b86c72ee5dbdb7803fc7ab035e82f8d37f161367dc62ffb2c", + "proof": "ca608c30e84ff44c78e74547a456f46f4b88b4e867b13d0ac7377af658e3b03c5a36f05a52a32bbd8ca45c64f1474bebb0bd62d4e6d7150059cff66af0f0790c20be9efa590161216c697efab33e3aa6f91b24e6fbc6f4983c7eda6cd07b3874ae6bcd49b71c158d7621c12c2fe2bf0b4e15d74124f1132dc209fd25d673f91c0d9501ec86edbfc73f1363269658c3870d6537730ce7745385b433c02f1efc025c7b67c8328744b17d3582e42e65b71e288a110e9c7280fe1794388f475c120098864db5b81bbdd63deb938bed09d978792e3a90309fed67557f460fcf281f0ba02c210efa6d8a3625f7fdb417f9d6d40ab54e3b323a128a3362dac0d2ee9d652edb16fac5f86357cf952aed79c7af35c6b95dbd9dd2c5921a61bb19d5bbbe0e3248d9081300510d3de669d15943a00ccd6a3ee3397b40b77329fc8e9fa808335a56640102d377e42be43dc06f82fed3df1c7ea944052c18ee9fa581601c79389296c722e799c774e67df97ce7bd1a78c4c8c9195c6aeaef0ea787a184472c0dba80b663b2d46f83cab707a7b56ad9b7bbde9d18539db40d80b171a97980194654e20c83b9a6ae126a6767e0559da9869ba11489bd565067b755bc10f1e5860c2ebcfbdf1ec0410e9ba5997a215f1dca59da8862446117156178502cd063d740f6a8cb4da63de395883d541e59d45f2ea71a9fcdadacd4e4936429530e120664f65479b09d994c2852d0f72ad695ddbda5b4e5daa31018387530b88b9590e9490c6f9fab742968331652905a532ebe457cd433dcb119cade97cfabc1bc15053b4e2b4d652cce8807d93135be00229a770b294f60f6cbdbceea38262ec7e29b11760f920654a37f6ee338dfb31b9b31cc652b6284b77e2869fa81e63dc6248706d42706ccddcc95c9fe11861a265ba61f28edfaca9f0ab46de8ac530edca3640f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a20a1dda16413388c715b669f4e4a9049ab3b7c62be2a981e833f0fe6365bd4d", + "proof": "ac91d4891ae3c52bc788e760285a06bdf5b98c743a38243ac95cbcb7af8720770e25589e51a953502a38bc1ee5f9eb120c541ae687487bf531ab0cec4f1cb32d2e08e2f29a5852a5e640ccb01e810be5dc1ab79eaa64febd331394ccdbab854f4cc6f52f84610625ffd6bd2cb58edcd0d16bc8effcfd4930195ec2b3832e67647c1f128223a20cffe6d6a70089dc6447595bddc44e444e3b87a6894efb836e0d85668f48a6a65fe7f5e75adf638622b61427ca3d54e3bca9bcf42c1f2bd8b90eb5b7386cb77c43505eba927c3756e767df442244c04b9eda790a5feb084ca00020e85a5cd25e0cf6094b90a8d871e49012bdad19fefa3d0d7808f216b00a6114166d6cce72011303b6d9339851748cb4a457ce1e6ac0092d0d8f9feb64f4cc0c005fa029e7ea77075881e430a40c4e719fcbe69cf7e1b4661bbf57f7e42ce550a09dc0bd739a9903f53cec0fd260cbfd6a07d9e162bb2541e09465ee3fad461e52dc739015fd0da145b000e8a4dda2794dae4e2ea9890aa86e32ac0188c75d503ec9b47f34a4a5876d57e32e1106d39f37055a16aa7026996d8132b03534c241884ea51851d097fd62f30d7d45b087d7b6ad1bb81a8fe089b0622c59981b706f064ebcaa5cb795629d4a032e21a574b05e306b9046d124fca811e4124e5e854f525e529554c72fce9dfa32d1959758992ebabe12b6692a99a15e6626f14d01254ce6056237129f98a9b2a620e6bf34c54f6062e66c35fc682e00037161c22679164cca5f0caf93be15d2853ad3bb3f7de5a34a5dff1234318943277acdee147c86eb2cafa4b0dab2f879f981e15ed9be50fef3882043e126e3430a99895fef18344ef7ca047374e271413ef6472f8dc46916ee972c3d1277ae8d38b634a6f60d5db7a720672da27466cd5bfbd1d2d4dfb992cb18cfc610eb91a8adbaca62cd03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a48fb8d187f8b9dc60efc67e1c975d50a31afabe88307705a4f16f513f714a6d", + "proof": "acf3fde9d61490f4b9aedbf3313a30c0754822bc45b0266418ceaf3c73e7d51be831d367e6e9da212b281be552d81d8506aa76217bf9dc869ec51283e5396536f688097521f801a06c9e1c172bb5ec3f1085ea1e594cc9980f186d14c7df9870e631c31fdf274f69c85dad0dff7d5b24acb9f804d5a0ca997e3ea0f77c683b3a5cf8468f9342e2076a876505be48243f0750fbcb2e2b44f319d764e2ca789a0ac0dae7fadbda95da1e650becdf3e04a7c9436593d7b5060587948d41de69f60b59d69bc41e4d0205d9dd7c1b9552d89d487e10c79315bb1995719e15988ff5006001b2aca52a01b853b01b7c8077a2e8203fa251ab3b5a85e57cc9128dd23c04142cec4ed73d7a09baff66147cdedaa158230a2f8be1e4b1ba4b554dacf67c17c09d4afcf982ad16375be1cd84912ff19d6b2e8425594ab0a4f4fcaaf78f877502aca5566b7733ba515bf26a418ed61d3bd6155e7c4eb5175b6f464e2a4a004dcc06cc5d0b8ac4481fbc572a0f2d3e1e2a1299498a8b6399cf8ee1be09206d2b968dfdf2d3295827806dd999add1056a6c2d699c60fae76f1f022916a6b1445f70c79fc8f33c53707254409de5c4cc3122dbcb799920957da8ea651b055e26442cce111c9492cc51805e3ebde41db262b88433bc2fb049aa738c4ca93411af19bae0f1da2ba3c07a3a86a8f72af13a37831db7f4484a09a75203768900ad884c90769208673847e64c1ef4ceeb643f8a2ebd2a955370309d5fe235732cc2c02b003ff3e811f7f09154901f7063dea31fc21ba2fdcf87ea3af5c93cb28bb20e2d440a61ff5b7447ef2938f5d1a0bbd1e64a0115a6f598f03a0ecec1d105bbfa59c0f762018045d1a365de9c07a4469afa2643369dff04b50366fc7deec5f30b0331a454c771baec0405b80585b1ed26f939e5814ab80afa503d9dc5c303ce7b0b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a8d9a0a92b51f5b7c026c14f09a9fed536e67a63a4bce868f1388aa4ea5ecb14", + "proof": "8ec81d0cfa943921d19d73336467e2bb404d823c82f58df419801e0055fb583714ba55d250a8e9d7e7830a9c50ec0a1d7910c0b207154fd6694733742e3ae71c705c6ff14ab37aaeb172deff1c33b3a550b4008e000262cfcc61c006d3fda90cdac4919d8a8f12b667566f1129ff53b8caeab382ab9a3fe3c2121c79ffc8107f2886cf35ee2f0590a1e7fa9a07f35252202d6974baf494df32e0fd9936d71405b92c7c281293e1d2d5c2f607d542edd706919f6d9bcefe7436b8b83aeb207f0fa2a38b0526f1274d84716af2023974f57972f6cfc0bba0c119a2e33fd71a4106925557e321d7ee4b1fbede64d5226f2f279c2de0ece72324e3dadd54042f8e3c78aeb1f2c27e561a94d395fce46d81e896698bdd8d0ddbe59b6c6f3f24480e66c84d9921e795b23a94907cae72dc69d342034a2446ea649d389dead384cf2e07c65095f37c64c551b764b4596553d7bb369cf55de944d66026a9884480199b7d143d25ff0645a57850487e2a6845de363ef0245865c0f320e663daff1f933568ce0c0bc794283d53d0f13deeadbcccff9f12952a7fd62bbba49e0d1dab1bd901b4fa785a126e949cb843a55138b1b2892ce4c083a63a0c00a6475559803c8754ec8ad72ffae043684934b7e3e9ea0301139ec2a959b36c1612c64a93ba3a9b610408bb8d554b4ffd4162153a411a1c405cc96a84223a70f965e0ca8be7ebdb02dc27bf46c425162fa1c6dfb7e0d9bfca4f17f35be1ac4b5a4680649446749435626e009318acdb176277a85d6582d0d2e1fc348a6b426166aeb1790b0d4f0c21fc0a2464e7f4bdb6796425c221d9e6f925381efdf1cce5a13c565f58d4f8493aca5edbfe864f94384ba5c8e8dca03ff1511e513de1067dfd70704fb29ec949079c7d8297217fe04208e2e7a8d3c0fa22bf0f552d81734c42697aff5a68e7500a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c26d4d6b27ad817ccb7dc3beecc0c20a572613b6f8ddb321c337c5d30a22a479", + "proof": "80ccde303157eef3e4e9eb9ff04c3464dad029e7cb1d2d5ebb6145f894906f679c4dae5f728c2e708ca196caa3eed6f45cb24cbabcee613c3195162ab981680df8aad420ae8180cad5d9009f4c95e8c83ddc745db1cb54374318c2c9f0a6d23e1c9add00f29b9f8e557cb691b448760fad7c412b61bab79fac47f667de1ea87036cf5bfe1de240de363da8794ba422f4a51d4621aa6d07fc9c086d9ad4daa10738049c737ed18d09ddf37274d6ddd3517bd80d74eaef2d2a8f0510630b556c0a2d0fff3de23608ab5438e43074119fc3125f608a6915999fadb0d9f1f5fefe0624c4254b2cf512845113fca9b8720a2b508c8592bc5329ee98f4125d0d7fdc0baa8a0080b4dc0d6779a6111a0bcbc78b6881f72ffb841910419ff5cb78074c7b7090b5a4dbf014561ef02767f4e4bd78c4243440419a9ba0d2104c50aa81e653ce7fee78521a537baeb7c49d30f49148aa241c644a4bfc8be6ef330fc1020d2f6e2c0d65251ccb8f67e467bb89ba01c706e0831ff8a48d637cbaff197f7652576809e0b311a143f22df1fae38a75ccaf343c51f4aa2767f615f94c70bdf58a138064f297d3c26666e138b42fffc323e4e9e52002c8a550efc3c98851148e04578007966150bd4ed000f0b98525ccd2dd2553586fb458c56a5705fb03869b321e7a1bc8f1b40a990726e5eeedd804234aece4a18215f819deeee9b30c0593776296f324ffa5cf9c2efe136fef175e79e4a538202e5df6358cf47fe6f415016241d6783365905d252a620e64a26324460d4bfad32d4d77cf07cf659501548d953268f5710c15e853c1ea621d71ece7688a13f946caaddc6e20ac3617bae148820f667b63844e9b15c5424c863933b84d2643f7abaf91ff934804d504f8ee53d80de176ecadb3960b7e405efca8789d3be2246da4a0770654ba4670512c81f41709" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "dcc72d4c6a12328805a9ac1475cb2a9d42839ea104ac3c8489ccbf866fb1ca2d", + "proof": "eaa0c212105e79507f42d0540f405a8085492893bf319d1a7447b4087dedcb32e418db387a3970a986bb89ce2bf69d9d054ccfb97ae8cc0c10f9aede8a86aa6f2c007274032afd14e37bfba5a862a75d8a6269019fe14903759e995764056e73a43b27abde5657c0f913799d57c9159ca2ca20ca95d46c076acf90cbd4149a5115af4a1b37f62854b0f80a92905d906e11694ccea373024f07df112f9ab9e900ae34733b6319f697ffd952138d3bb5a67af8caef4de14c84a9e50b3101ef30049fc0b1c1e7d1d4ba84ea98332ba0a7ce254c04a6b67e72777a1196231166e60afa14e632489819c454199ce2c2a21dd7e6d7baee380f195a760f00af8a114224b44c8cd2cd4bfb5b535a5619321afa3745a42f5621e7a6c2d16d0c322407a84e9a57815996eab3c68049b005225145a4d1cdb93633653e38212501fdedbb9e0b20b7e4324edf873342c60f6103211ea9a48b024238cccf18a9528bd8621ed741be72d852f335698a8b247f0a63a52c5ccd4b2091a309efadc2ed1e8b7ed9f56060cb4a6cf89168df6be44074d538045a0f5159c0e4e8fa16ff622b861f7ab713c29a46a2a0e199f758f6836bf6c76d20e8315912bb314d86aea183217c731b0b74219735620314fd5ab9e1783cf9dc9d2a8dbcb4dde21ca0536c14e85c28d1130617afac61aab50aa67b33ec1d4727ad42c88bbc99838d53491ee312c4e3a225066ee3096a94b7a67c91ac19bf4b7626067868d6eb265c9f4599ece3d7630c51302a2263282bcb74148bb3f6baa8fd1c07b2d6a621302618054ebb2b0aa3774d2cafcbc2be1a92749c298422019b86b04c7fc6f13d358742bdb6608c833a94209dc50a62d9e76a26aabeecf470e4919705d42de277ef5436930834485427910494c2372fcbc9a198bfe2e8805e75344b9338efa0560db8ea193e34bdf4bd790c" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 15 + }, + "commitment": "ee48d8caf578688003c654fdd2cb466ff90322923333a5d73c39411c7da1221a", + "proof": "207bd3ab886c5a04d54ab9fb97a2f6730f43e735252570402c588dcfd864be310051402374a18fdb78f4dd3a72405a378082a20147e3f8e9521d8caf74a3b5421a35588f1ae8ec18f2ab63700d72eafcfabcf71fa1a887b69e2a6b1c2f556c26b8a90107160cfbf02ff50ba91f347e5eb19e9a9aeb7b4c43228c2cc052daf46db1457af92750b5220917f54d08466076bd43b5db9689d2464c8253867c08d8026c1b9f65c980761184ba139bca1d40286aa5f99444aa792379a2e024f4b0660aea44f9b409123856129038d5c88d053133869dc3a9fad4ab7091b951c20b17086a3d9aa6cc1983952d4e84c75e6735000ac49064a0708066844156970f7ff83fb6ef54523df194325adc52c11eb09dc4f6ad67bc0fd0985c36a8f4706509717c82375e99baeb1d48665559a4b1bb2836d1bdb21dd941d2dd41a479393caf1636a22f269e4d62188f4e73017c7e5fa9f3374ac2edd0eaa35c82f93e6bbacf1d51d41f6daaab96253633bf72875abdd1034003de4d92e80684dfd6cb8d2373e26dac76a20fa00e3eac83398bf61815882c789bd8ac1af9d3f05183fd115622425c90eb38cfa9ca18b28f4c281c41d6fb4bc9eecb5f07d6a086f4c0ba8c2b6d0821d84b3870cf3b27ef583f294c3ef324fbde4d931ff8fba54676fcf3eb01b4ae207ac2773c088581b16ff5a8af87803560e2f41748915cbe668593fe32f0d5dc623a27173a5371dfa565c981db1ad35e8a6280bc8dc4a3de71993fbfcce738bd28e23054b8b1714e87700e669f2d362afbf055e93024bc64d05b76835b540b0a49ea382f1b12cf38f53237e313f85beb157de25d89ed83e0560c98b71219e6ab408bc1e391f8ed83fc71f637025a45149009c3bc07af25580484de55835b56220a975f240e596d043a9331e932e29b791dbd30e5a87e2a8a9c7565705277bbbd04" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "365d6c1e9b82e217f2653702f60c394e05ca369027246a2363cf6ce6cda8aa3b", + "excess_sig": { + "public_nonce": "365db2608dbe39da96408aacb3f2ff967ba746f7135df3ad3b8e2b9e8852fc44", + "signature": "5fce28a721ab80b7f3aedb57c75e72eb0687cf04d48a6f38f9a111b861518407" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "84ad73516da1dba2109fc78b992c79068f9a427f4c7c73b1a2fde157103d0346", + "excess_sig": { + "public_nonce": "e0bf78ad97ee49eb3cf303abda7235592d12dd80ca3cf75db4521d26c0aa4706", + "signature": "6447d5c08d19930a7aeb7ac665aa810921e90bf071fa287bfd8411747c3e5706" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "8cc6d666abfbb9c3125a1521ce8b3dc9f8bfc5d530bba788e2c69f63d0ada45a", + "excess_sig": { + "public_nonce": "828cb6072ea9f44741d46a7bbf1578e4e58bc48fa1ee81557a5c4228dcf0023a", + "signature": "558d66e1aed2868467d4e9c076b018f8ff60fa32ae6263a805e9b46c9e69ee02" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ae0f9ed6982880033119f34751958a3a68e35310bb88a6f0560df9f0cf798342", + "excess_sig": { + "public_nonce": "287c7ec0f8d26093361aa92bc488accedb11f7e14bb34a9278ab6839db1fb349", + "signature": "c6aa65a9dd91ebe7d21c0a691f1d1556c3259cd5a5db7be26a0c0981aef72e03" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "b2cf6ce07eea8ddfcba6c14e24b2999d504cdf2de75cffe0d89dfc42b65e0b62", + "excess_sig": { + "public_nonce": "2821497599e2d99bbb6fb706da3a7ff6f3fc6788514a55ad953d5141be42a178", + "signature": "8fc76cafc296576c4d651cc466d86977a90a5f7c50d9fc7b1556b7884adf3a05" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "fccb2f75f2fa67d4c7dd10c3c246120bcfaf5a8440e3ab236d499cbafc84b15a", + "excess_sig": { + "public_nonce": "b8ccf749f3743782df7bc3647b5df46812ee33c03c8855afd6383582747f934d", + "signature": "3d0783c22b2fc22bce34f4de8bae25406aba188f8f2707eb148038a93c63010d" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 15, + "prev_hash": "2c346e910ace6195884d0c4c40705e6eb7616fd646cb697337aa463807f487b7", + "timestamp": "2000-01-01T01:16:01Z", + "output_mr": "f4790f3965273ab0203cb8e84945349c1fdb4046e7bf8e40c80730f9d20a72b9", + "range_proof_mr": "156add0be4289c7c052ff773eb0b2a3da80cbcb7e964167b029f21401640e341", + "kernel_mr": "3a85a2479eb8722d3c06c341824ee6e6b033482967ee7bb880c4e1b396f25f32", + "total_kernel_offset": "d1e313d2626bc25653f07a6f428298f199dc9e332d5fd8532904452a14a7b50e", + "pow": { + "work": 15 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "08871a8ec9634adcc2193bb575e3727ca7f3d25483aba1da04711a2a338ee543" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "207c3cb882ffa34150c6ce4aea0050891751898a5be647d8b23ae3c3414a1c0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "74304d9eb9e04a1d1a4104b1aafc6cad51ea627d948561e1852113f74b83d374" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a8d9a0a92b51f5b7c026c14f09a9fed536e67a63a4bce868f1388aa4ea5ecb14" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 8 + }, + "commitment": "5001a29c380652d4c263ceebd4f1fd4329b34fe80a2fdeca378c695205f61535" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "028c063f7964a5fdf59988c52f8aefdb03376dea6359e390534fea4009659e71", + "proof": "aa1f29b52ef7c843695b6e780f59a5eeb7f0077d37c924f79fa71761908a34755484365590e5038c7852a121c375130113f04cc5d7949cc4a2118f48a8a8eb43b6f5edbf104b0898781b94d13eb150f192eb2884fe4b243abf3ea1f24610b14974518089f7bd7e41ac387fe694c66d7b869222062f3f53157cbd894e9982f351e09ff119359f325e22126fb6a0a9c88b90ba48317be43727833f9e367f17490dc570bc0f8db0f77aa809c4e7afd6eec977f3e27bf3a8c79fe8e52eabfee6980d321b59c7212e4f703300fc5460203d4a546c3a3aa4b46b9114f566514dd2630e6c78680b63731f5ea1ccd1e8968e627865b6f2cefeee52c20c48b03a015a2c49fef2e9b3a502a4e8ffa53742a567981c35ffb93b611fd09733efd9b9d068333e5ca031b308d9b8dd2b5d9ae1692d09d9cf4f00d61a37694d315a98950c3a01596eb0ede5cec6d722d7c9bba16a5923296a20a78e78c03f590057e51cebc97014f0c72c380ad66283b49fbde062acb2eaafcafdd7848918825faba6c93a1e9b7d8062e66814f7c02b979fef2e07a5a5f634252e3dce05d0f14196e6986cb3025c7256e02cdae484766a9d8bd04d57c9c2ea65bb4609ba6a418ea4fa7d1adafb338a5fe5f095f1ac5ce080c0701eb266be1d972733a4104ab2323618367f6326036e09047f0493d3a5ae28a01fe4878952d2e94db41cfc51b09dabe236c89af3460415a92d94c0362e8ccdef2e6835b74a13165be73a9b5ea7f6ee5c77cb74405a4245d6acb4b65906638cbe05216732eb20eed68721e9b0a2da8b6984b053b3055a1197a7787ad8027cf4cfdeffe035b063c2624073757ff604c691ef87547b2d4d5a99a58bd4a7730c820c3daf9ca4da1e9cd4c6674408684c0fae643b25c10b242a83618973dd536eb1614023f274b814ac20d943ce26534188e853209daf0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "088ccd635ca5109f6268aabd79a2db5b4df3a57954789a85cf5b1c6872d63a4b", + "proof": "6a260a8c1c5874e5e21f14b87acdaa57288ff2d6a930505f127a1d7e7ed8b31a3adc05888c63dbef9199290c08803518ad84ecac1389e3710a01c70742abc8666a9b293d03380cbfa8ab5f4feac1e43d86e14475af57202a83c4fc978549c544ac7576c8ebcc951ccd37e4f9bbcda36864450123142c1bb431693b02131435450e59cf29deec54f9436becbd3e9eda45ef8f9fb6aef71a6611af3664d4174102169aa18200bd556548c4a36fc6794d008186c6a7471ddb02237a3a605bb5ef06ddbfaa6976f1254f61ea3ad08048bd4d2f888520d2d672a01819b98d237ae40bf8fdae7e10c290d0ee90c162dc3fc4684ad37c23fe30faaddc38f359aae3b31d50882599a6bfd60a9b4e444549c20e25295efb3c5ec8fbbc08eae95be9da5e7a3a14e48ac379a4c53229f627ff5287a9dfccc3cb80da6ca85ea3a1eb251daa1f788cef6e53b2535ce05f3d75d15d190e506eaabbdae8b749d1636489003a451b5809bbb9e7180ef0ec3ab92df840eb4596e1c0df8cbcff4d29736a48fd4cff1c78099fd0b5ae245570ec9519f0c2888b59db1acd0b8704fc78484ad90a7d345b060aa554cac700cc248851b191163e7234978c09f92719b47360545362b3e359721bde622a58e62d5cc533dfccbba4b3fc1155b4469e13c417d415eb52a4c243508cb0b77bf6076237c5a400c4d20b0160a511c1f896a23639d48718bbf986163aa3aeb5b5f4fe873992265f329065c20c2a2562a413e9f19033b6094296f41056eab67002ab715482f23d31fe1fe907d6fde5f74a482f3563e03024d88e81732c7301fed5c750bde336a56bf221bcdb43309a88c9d2e13a670d9ff8906f0f1f55ef72d9a65acb715c8b970b4d5c9c620e15f958c8184461440ae0747c7dbf0d326971093deee668a673f552c50fa24ccc843e3977e1141cc5002321bd40ff0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "286cf3d668ed1670b12def6033b333518ed596d5818ee92d2933bfa9d5dcde6c", + "proof": "e0a06139535b7596f90c6f02005121bc33df9d7069b409549ec39eb84d128e4c58ffe24ff27643b96d0dce3614861e90a254e56b30dbf71a028e18a57fae9c4c54893176139c7d85cea3c12121e1cfd6868ee75b755dca63a0653b693634f872de869daf4d1d5d6338532489e1139d5715ee150af5135160a340001710e74934e6f92213ff2fc568d04f064d7e0fde87dc071e6a9aa0c22c1f449669d49028086b26aea4814fb738490bd4c452e17f932a08b6d34b28ebf3d7d087d3ad7fa703ca4e6d492cce8026f19fad303cec6aca00aee3b07f85a0193b9fff91efd0cf0084e339ae639402ef12973400631ca9ac781a5932952d5a0fbb0f5877324a183252f0ddbe5e38fc6ba1d217a53f2050fea35431b56db725bc4e52315e37c488543c7cd1fcbe8227c20a6ab7a56da84ce13c21bc818c68984a7cb5a0e7cd23406f9864c459b825eb99a5468b9b9559f44a8765f2314812f7c706a729a0b9dd396696fcff0303c9ae0471e78051455b90ecdccc29e2762b294ca224805ff5beac355c5c8a1ee226df1bbe5112c2b69e8b97a74a6b2abba526eefd7be42d3ec18922000e56011faa03da4dc38598b5ee996923e8f93eb1bd756cf131cb8b56d66c61da1b98c98250efe13fb5701ce3439cd2c8f9101fd027e2d7a3ca9321712dde27c8254578a28cecf8137ac1e8d96863de743317d1e10534ac23d5c02bc70a5d1aac31871ccbfd39555c85a8d11b03c445739c776425bb88751bee1a5c0034120572bbb140ef1d4e3aa97ac1a824bb6e07c7781af880649e37be12b68e774ea04614c0fe0c413b5639e8b934b66465145677eb31e8655683a345fa79c5a5ba674ad2ad082fc224bb3205c7af6d21c7eba676d3d592a6e50fef64d0978bd0302e0eb45b8bd3dc771efaf70df82b4cf91cd94f25f582a6caf485eb7c8635303f020d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4ee2fe3e51bcb325b049121d737643fff4be408e86f1e88886ad63f2f1d63f77", + "proof": "1a04c9a1ae2816f037042173c9cf9e8de4a4ca14332ecd4577bbe812d934fb453e210bf139143d1c59f6353f26838ad18fc0665d79d817ca89e144a850fe741738b7433b66196f63219d26c4f2a069a19df55630f2b3eb10104f9a27be746237640e0eea1cc44a9fdf147295d091a7011c0ef545eda769e544f3c0a55b759c22049ec0d853eda62d7bcbf7ff6944cb74c99e8625264bfd2a4d14ad07b7cd3809c4ef653265620c312d5f206e49fa0d6ec774e5fff781eaa60c526c8d5e68db0837c8aa7607e238a52fe6c24bceb3e084e54f0cace8afbab7a1e9f383cf269f0f08253870d052dbb9a852171f15df0ff25dee57e90ce1084f7373ac73ec21a8149c80736bf0b4ffcb1e521ddbea20de813c28a10db57de11f8c67f78f51f0143c768061b9652c4b38f7d67e49a3f1d710b805aaf8d9fd75c19549ec3c412aef21f0b28abb6c2ac055b59333bcca418bbcd0f0cea80bb4b8df168cb869f1a82b43ba2cfdbb022b6ed058c940249e966c515acc9ab1ed6db2fa4edcfad300ab705e62f0ef4760b389576eae350b97f2e9991f8f09b161811a450d593e3f5bf39f38fa0e43b79f092a9e0069f8d2dfe0c595c1a2689fc6402c1f7fa6ba2dd2d4a67fccbb903dc08e078df1d23265034e319cf09dd3e0935220e78a4df40659cceb2a9291eb9526bb0f8b8add3d6af0da118a8b0a485166fd91152b17c2f8ccedfe59f0af2ae80e70dc1f73565af1a8eeb5bce7b23826e8ff4b08a78b4ef8a3bdcb698c4697970cb89dd4421461f1057a8ed83fa7aaa22721325ec0156de228e0c30fbaf0e06b295ff0569a880a04c3c01e95e9f90aa9c9c3fc534e20a690eb412f51ae005bd3948455313bec85c5134475b50d1a4f248b390c2ae6a6517bade2400535e47b4f25afcadca9dd17a4a59666333adb2c8389b1b6a75f7070518f576509" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "58a10833d01ee9c64cb7fb55efd2b9535f3594f9f611f57ad8bea55c8c020671", + "proof": "d8ce651dd7e4adc0b0609f67b5151b081ffbd4679f795b76ad1c203eddf5cf3d764a4b98477044dd77fe4774d6537d55be8ed7e305589aba5a8510fd43a5b4155434fbb82024a5a0a371ddc46b4a081644ca2b3b7ff966d5f054cdb68327b624c85ec1dd0ebf2bad8e9813a54000219a6a19c18bd52e4131d1e61873fa6eda7235262b319bc61b58726934416eea729ff02dc514a0dd744a5f52fdd1a60bea0f70045bbc3f5860b1fd18ff133bdd706848c1c75c7b43f9ac96045efaf77de405bbb50ba781735456d194a2e33875cae5bc7cbc985bfc4a08adaa418e5374540a3a5b4637d142073c1e67891e3516a7ed294ab02211fff5997872a2f143055822b0853a9ad8d2c70beb46f4ccc7e2f26f5742879713a1293ad7ce2cf1b99448732e70c8f37be5e7cfebbc0ba6941af9ab3d2b3a0f753f3280da3b7a0eaf758372048c9389ec2daba6940142d25fb7bc0c792ebaeecb6edb5b18f51562845cf0637c4565803a2503cfdca9da279cdd8b3483c93af56b17af5d198e85d806a7f75cb024182f93e0d41c3ccd473430f1c6e049a2370c837cadc6df4c182b50449851ca7259806698df9b1e4cefb481edefa5ac78bd28760db1c557c6ce490b346118647176671ea94763589b425b039ffbfacbddac38d254d6b594390e565e5c5f49a88d5527aab9cb492e195fe23d6708c17e95ffae2f9202347778f93b9a06701df62f1105cb959256b8767143b4532d0f066daf15216a13a72524af848a740e46fa6ed0b875dcb663403e1f92c39593ce7758e0640e70e6e70a09693b28f09b1638b11371e44c45d1ee58e238841f5a7fee3fbbeb3790eb55ef5634d73a35d63b52844a8463c47ae091ee7847ab36edffc8f2e191c387d11ac301e9399a040a0e821c1bbe75e6e6133bca43e620480d6b677277ef3e705e79b51c1ab80b87070b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5e4d69e1cffc6785def7a7ac1d7476207321a776096c0efd685b2d66682f0739", + "proof": "90ae2614971ce68c2c0615d4eddb8d5661c7cda5d0dd416722e9b602dfbdb53288420f97b86126ea1259ad046ea1f087c2125e54c2befdbff53b9df46f8d6f300cfdc5df3fa22d7c5632d98c972e277c953a282f3cc41e5a3ad47e9332622770ec4eee2b4b15c23491992578d437a4801d89639f275acc41f75e354472c1cc1b7ca48bb236d57ffc878795bcb58069d9b71d62e3e642511968179ebb02a9cd00a6df070241fcdc85ee9876f53ee44b8b58074ad36b4c25542dbe95d917fbbb0d199927005b04c15fd1c64882ee680a2edf6abc91b651bd7b22d20b54fb15ea01c4e273e971a948b47e8ce7b2d7746d30c2c67524c2739b5b311241503d74921766da63bd6d2a1709cfcc9a0b30ff8247a01e7165346e857aadda23c92272915c68efeafc71c22df4364537dfb25ffec956c570b6387ab4ddad20a82412d96b7a76fa8525c7bf654d2bf0b0d6f5e07c0267ac48c2060e9a23563ba26771f0e34cf4c5aef9b9b81bb8623e6fdc48e5e1a25d01b55d4516b356c19a9942f7b15611da2df03a94118f5e6d0645db5e5ed2accfc10a41f7121f60632c8cb543b74f26d83e935fb99a1b1e7b51cb76d56829395e7d5ca646885bdfda2471ea663b176ed64761c5864d8fee51af264f5e1bf88d356f7b725da77f17f59eb7f756eb6408b4e89783273e972706f650e4712b017a9227567e461693d2190b88d68586873200f3abb545d6bfeba12b04ec137edb47f3ca3432db17281e3109874376dbaa597e14843d6dbcf7e1c96ed01613d2a2f0e2c1dc7ed20eda0117fdd0fe439e506a4c61be3d60335bdbb99ef24b547d1a67c4ee163605a600f39e48784f2cbde61fcabdaca685b0b8b3ff0368fb934bb1fbe3e5d27d85c70e7a30a5ea7627069e07191cc6c1141f80e8176fb7872054c4676cc8e806064626dc2758ffb1ab509d01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "621da901f6a0e819f7fbc2f29d60ae403f3a557fb6c99438b01b028f9180ca48", + "proof": "2c6b09f5a043e364673331ab346a05fcb0bb9fc01afdc80057c0e0ecb448624aac511c368e6be38c6bbfc49c6bd32d7943fafb15b19fd5313f6bfc237d3b2050f49c7fe47dbc46ff53b59b8cc3b8ed02b577dc72d04c22910b8fe216cfcb0f5b12dd51bd698a19d9ed1261641fe58129132dc80d5ca125ddb63ba1091d124840b4304b7102dcc5081ff50561b859e8be72cae6639247b844cd77c3316dfbf2066ec5267f0ccb82431d745d2ca29f5411bebde863072b24fc2e938de7e1672b092b9966de759ae51be2c725d66eb4bf3656a6d25d5e040da1dc846b12ba8b3d0a9c72ac84f805d6e8eb02baddfad2417a6e12b44d9bd3e6d5ec1e4006c713ec6d469e917afeb4f90f34d41e37b72967d1578671931eb1a0647149d88f9225ff105e96801347c2715a65c51febb036abc523fd0b82f7703849e2e8b4a883f85c22c45c99b282c6bc8acf2c33ff6e7feadba65cde30391d700236c1ea13b354d82294e7db28ca76f9410cd830f227532d475b7a7e97469bb7ad2bc1df8f5228d17398d6a680be6c482c26d999129e928f7bea518f690601ce588afee19566a133554c3eed25c65fe0470c7778587f241e6ed372fa3cd98aaf180f48f70e6bafa4381a4ab8b3e21047083706588c237ea0afe7386b9991e0259c4c966a5b2fbf8b6aa89e08c9ff64a1ed25498ee6556fb97842e5b69b5bd863b322676c075778b07504324f22bc94e87596bf279268cc702599244798d0af102710c2a6bf3dd6cc7beab8082064b1602fb8ebb6fc8061048ebe2e353f2b161dcf5e25414e7886f85f72e93bfbd4cd3bc2ed60e0419d4b3baf02b832280a49b95a064aa6ca7997cb3e881a3e5b131036227016c034e3d660167c6ca4e8903c9f94f557d9b622d2220b345f253bc5d330a632e1d8cfbce89da87567e8cadb195df1c13010d378a1090b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7428a2b68b73c767503b91d5e0ad69e40a1e509baafe2c72a903d9e1fd2bbd44", + "proof": "1c6018af99e53086e7648fd6ae626c7f366aecc3a045a89b660919dcc6fdf368f291ffcaca97099592fd9fb01fc525aaf0f5f74131e895af04d360c0d2a25c0f4261270ecf2e7f6f2ac891972d36e0d1c8ee167f7506c5a4b719b2010e10321558925d2b3865409452173e241b4cc25b6c9dae36a615c4b1c9a308413b2a2f46e39947e24a8673fa04104c44a3cd8f966ef84ef179bfcbf5d7bdacc9b696c90c5ce2a71605da34dd6829fa841af7963400401dff43a127b7d053e8dc2790bd0a52ad91537781cba4b592d07344ee191599ea03a49484d018d3f2c231025f9402ded1ea93229e9d8069c22e70e1bbee4290344593cf06f5c3e383bf9c7f60ab69988ced5c817cd158960eda3ed8038aee2756c9233171fc62bedd3d1829d0de0320e8e99172478b76fa770e7cd854f09f1e470dda9042590c6a02d255f1d6527f6aedc0792fc3c7b12a28622c7db2c7a84a54ecf5f5fa9441dc672f034c0aab1f4a2ab757f54d1c0db3a5025f31c5b25b371fe575c5eadbd1d48119167df17a06bc3d4fcd459079b93e2a2986d628c9c200684b5d29f4553290d523d0e5e5903ac03423ef2a116dea1c4d84d113e61022391c0f1e03f4e8d67309789e9c8336484a80c0321a2e739b2f3b2815faefa0aab2e157bca2d2aa980810f662090acd27cc8d6ee417099ee3552e9f2f57866ba17ef162b56f323e9508f2cb2bd3d3981d0c6d4fac879502aa5a5bbcc75265d968a80be08e19b7bb59ea73adfb14171e3bf20b90b72be8c3623333924f1f81d74f2c665e5c833b2027f71d827104672c611651e182093243a59263df42daa0d0daedef97c170214f33c3d35081a4f19c169c4cbb9a959e2667a93dceb06831641a4efa15f3b1a45c8264cd8e13b327c708afbc37376a2b20806df1bb4616f4035014fbde1251e5d7b054d8b8610a325c02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d2a3ebe1ab8c968aee412d7535edbb16fc8c104e122b932e408cbbbea945c606", + "proof": "a8e271541dd795cc417dc522bc6b9ba07f9b86b93eefb8b84f5d3b5c61c69460b05bd0f08890b05327553b0a39c48cea012910d3c79782aa34d33a9cda48ba438a8a0285ec20b0dbce2f2a7a15740a14d645628c4f1f9019d0faa32637b99b70cecac752ad0b3a94622ac7c98b0f098fcee487c17972d201bbf44c024fd969113f9a5588812f2fdd7699750f2863c57c04695632e08d2c1af2951fc437245a08749b6b82e2fccb333cf5c7769459d5d0bac81836de994bf058624c378430d5040e3112c38de069c9f4e55470cdadcc32fdce7fcb81882bd6ddcd12ea29346f0dfc31aee962f72544db81276f5415a59fc2fc59cff9e3ed51b7990ab605861d191ae6631783a47d2928241e40cf2a723bb714d8f3497cd4ce8d5a637119291644583259f6c2c034da87c1dd4de3c309dc289e5fdd48f89712d4b762ac0a41b5156c012ceb3834b9d72ba42125e6804da9edb704c9b10d194f5121c2a60551e85c142337916be90049e90be3a997f7391410a87c6d0ef429679e1e7e5b5c89515dac0956844da72f73635b6b033fe33a6226dfc7b15461e23e22de33f0d80e136022d1624cddc99f440ff723b061e8a92cb02ab33d618d6962a2e8c02a966cb64608aabf4775cc7d603772aa759354cedf3326f1d1a1536623264e08bb1a295128fefaf968ff73bb7bd6e8820c286874bee23f4503c7496344e11baa903ab3013d88667ff298364bfa3a6a5afead1bf810380656c3bfbb69a89839f53db9244d4dd03cc97fdd5be7c51e3e1d71ab0947e5c617511872d1a7ec94253e6f9baabb784ea050ca1f836a28cf77fca6ee900f1d3f8d64394186072706470d7fa6b9136aecadec615a992dd263cd91435a277d2b0429f9b367c950c19e09329636a15100a6524561ea26529230da6466824f0739d9b426bb49bb9452d9410820406d1801" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f2516ae3f213121e6ca722bd5b73b3096bca555bb3b08bcb81c88e9691f11345", + "proof": "44917c9060c7414975f2a691dceed4e51b2c1b93236405725ada0278f8d97a017ccbaa077e1ea054675e507bdacf7d9a13f0f85285d0bbead18e8858ce423c64fed20e6df6c2b5ba8d74241c231c0cd901358cfe7534ea09550dd8d0f456d935a820cea74086213a8b4498745556781903b7cfc70f0f3a635c5ed9ce9f32220aea81548c172f232d9c3518d639403e47f1b1ea94b7a340329e3ef0b5718ba70e401a3d4a2cb292effd7cd3617e2733cb39224b18dcfea9fbf12e50e30ac3a1010f6259872994030fedc56914470e2ef38f5d3b75d84ce1398bdec3def1e5fe06fc4589216c09cd1c841eae1299d1f780476d6bc6569b4e048a7dd16ccdddc03d80ee23c6dfa330ce88ecc393f0d0d99e29ff7502734f0588670c884f8302d74142b564d5d30855f958c3cfd3a98af9bb26c82650e55ef16708e4b426f826e2044059f16032c4b3f6f474db6ea54d7c971aa6548f4e802780dba341009aece53038a0a0eb9d12f346b5680600aa4ccbec875c2ac29cb7f4527a07afc306860e60f0768cadd16ebec9c0d9c4d5b8fac35e4b7c6cf77a0e856649eed0d4c8932414964b5d0baaca209df0dbacb89218001c424362baa3325f4a2dfbb657609da100b28c66f2e2d78d7952b9c5fae870c97658d0cd9ad2b23a455a231cb51dcbbb0dd87d4bfbbe8083aca97ee30fd43889dfb781cc6bd661bf6c5940340309d05079f48eae9d3da6a422fd94e9629bc2f534e7004a2a433cfacac35d6b863ab3ad53f0d7c67749ebf4a88c8d6bea97d4a98105e810f66a34b209cc6cb8f5d9a5a638960be9bcb897ab51acf198ca4c3ca37b9ac3bbafaa635910ae6cd6eb9581c866b10385104710e76bb073b9c81b0ca1622f3d5c56b9fa5de8c473f934ed40c601dbbf705d6a194b6d65fdd1d8850f88d7929809c7207713481ceff5ce2219350a" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 16 + }, + "commitment": "a016f012574e9afca414948e17435a0821a1bb87a5bd5d48a809109efe388d6b", + "proof": "ccc66718f2bf12ad9b1f6a52091feacf243c17c720926974700859896cede6638edd30b2a64c5ff6fe0cb0b088a61682dc0ac7129cf995c594e706c3d878c74a7e362344ca921547ed1a7b9c2fbd76eaa4363ca9863c417e84deefd16d2ed34504895b6c1beb4c4bc5033dd94e394b417b6707c5c6f2c9881b53ab424264ba5f4e31d6aff98ad609c0901a099a8d732e2fa847bcf01e8664317fd1145ef8c70c06ec48ffe284c4a1646ec3388247a2c96f1af2299334a2508749d9f2a0c41a049cdac0a94848044ea79c3f9925988550ca66800708ae5ea53c4361818c31f4020607850d85279e54199b05149a4b11b17b47d9bdd598df7211fa25d56fe7f12c542c6df2e1249461305cc17aae446d41c37604bf76ec703e8acdf2c09cc6d83d108c511db89a000370bac16cb90f437fac51570128676fa39e4f40ce25dcbb53b619f2fc94b0aa9fe3db62555f3318c86ac9342d3c110a694ddd7120e765c81ada0885a09a6e0ae91b0aff774641bc966decad0402b145b9caa476a9f9c1d0727c2a11a080e1398eeafaddd5947dfca40e5b884d7c2f0f3f1217c497baae104e34902d0c4e939a49fe9ddc1df3f1dc9bdc5b315b7a243ed12153a0a07dd24a7018c88183f47bf565290e07f5702d8b0da0491702ed6dbb4514aebd47bbe97d720ee5aedf43201d94f93e471c7e3fffb516f5e00622362d2f4d9b7da19eb6bb6cfeb98e4c675578ed79c4c237a49c8e6e7cdbf6e8a04caa44428d637b6d6b132a42c14f83434873dab18229ad6c8c62022ac479e0eca852cbb6273fd74331771b781ab52b1fad4d93329ee97e6ca81823a5be9908941aa2e5fa13371f9f79eb75cc1403f68bc0d92930889214663a03c52c3f4e1f0a38fb4794507316e7ea160a85523cccbdba3b2bb5d48785e8641550a25de0f9622c80ad77eaddaac665fd0b" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "36773f65ce799f6434a4276e5391613b7da0814ae2362ac2cf7041283cf3fe17", + "excess_sig": { + "public_nonce": "962ba59029fbaf1b1e6bbb3e6ff5d1aa88de5f1eee7715d19a52b134e3e97b51", + "signature": "932b98eb40e9fa369873f50cf75f2f419dad71115bd8ef203698eb43e8e92401" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "84ec8122fa15d1f67e34c601af1bfef8f80f07f3ebcd8203929fe9eb1f94f449", + "excess_sig": { + "public_nonce": "d482a00478353cc203a0b16c7df5a12d8ef058638a6af135fd068ecc25f85847", + "signature": "c61bca5ce5298ee46726881852650cbd615def72ff2509d893883f96781d2800" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "9e05555d79c4ac5e64150deecfe126b4dd6bf4059a908780173796b47509c20c", + "excess_sig": { + "public_nonce": "f817d63a3afc99a74d91358effc710bb36df3cfa5ee84dd3b13e1ffe2a115243", + "signature": "12e4d7be7974c6bd42ce962707ff485098733cffe69159195b363e7efc85f40a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "aac4c81f0ee5f9952e064c393d0cd16dff2ce1adff80fdaf6534da72daa19a25", + "excess_sig": { + "public_nonce": "f8c483e2bcb19df66acdf70787895d4935c65db62048b90a29db922bbc6dca34", + "signature": "c531343597ebd0503ac9e6e3f77f5b3d51ba8238dfcbf781c8c40580f1d8fb07" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "d20085aa933d82962f2306b1a1a1ac09e27b510b88c3c78a7e2c7eb72aebc44e", + "excess_sig": { + "public_nonce": "4631e542767ab30760b5c81877c843717752021517a83a19a0d4325c3b15e24c", + "signature": "0b9102513d3bc7169dc2ee45910ae61a1297aba69e58a36a470c39a145aa460e" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "fa8ef5b9ff13eacd450602388bf305d4e520d4bb9c21d77654234800e2e3e768", + "excess_sig": { + "public_nonce": "ca857db392dff20dabe0aeaa2f4f6b33eae6d64dd0da8a5c35edce81e7112953", + "signature": "bbb77ec93445da2fa3c25ee4fb9d14bae65dce40e592822c26e039cd96b5f306" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 16, + "prev_hash": "a05ff754ebc783d2ae9852ef97f566923c3cec1a9ca879cfd2f6e514d088db83", + "timestamp": "2000-01-01T01:17:01Z", + "output_mr": "a3b49b84d794db0e71abdc618416f50b48dccba7f1de62ae94cbc1b7e1cd2b71", + "range_proof_mr": "8928eb74439cee7aa06fe7e367319810ec306392e4b89f1a561927e3d4541ad4", + "kernel_mr": "76ccd027390485f5c3a2b617a38beb2fa6698bbf304362a55ca271e29c336efc", + "total_kernel_offset": "c9621a1814c7e2680ceed136d92313e3df09559dd34317660d56d4581008d506", + "pow": { + "work": 16 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "286cf3d668ed1670b12def6033b333518ed596d5818ee92d2933bfa9d5dcde6c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4ee2fe3e51bcb325b049121d737643fff4be408e86f1e88886ad63f2f1d63f77" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "58a10833d01ee9c64cb7fb55efd2b9535f3594f9f611f57ad8bea55c8c020671" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d2a3ebe1ab8c968aee412d7535edbb16fc8c104e122b932e408cbbbea945c606" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f2516ae3f213121e6ca722bd5b73b3096bca555bb3b08bcb81c88e9691f11345" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2cd2d525604a8fc904cbd0ba6470c7755e00c2e22a6c18e722abc1bd48a28f01", + "proof": "6430920c560a9575b61641e54203097f9c4e2b1107949d64f5a0ec11c39b2b05fa8a95742ccc9eb67d2fbb7e1a7f45ef84dcdbb40be3238ff5f39163f2d60d3476c69fd46ab51fe8794d12fc8cf9395ec43daeb7229102c830259f55182ce24844bd9dc344fdda7854b0adf867f38f771e7b2c232ef0082cb3d7d26a1265bf6937e4f7eef34d649cc7dcec7c9c7daed7c23237e51e717acea4a4edcb8a31dc0bb475977f9e7a51094c62ee332b3ec5a7c486a388cd244c5ee8ceb9bdefdbc306e44ad2724f96c84670133adcbb629f9b384208f425b1cd83d96cc5815a6d480ca89287a892371812aa61f37b50bec1c9fb9db3b78a26832b9d8a5f3364aae9298eff2c4e36b482873079eefc8e12b86c39dd31bd7ace293f9686b4c4942cac368e57ceca30b3d0854cc619fb3ad0d28119521bc2370a646663ae76bdde618f13d6c9e631bf1089eef3d9c619fc2edd27e81eabd71d759344dc91f6c17e97d979663f88bdec97f2db352eabc5c9f9609443e4190138c876428c01f0317ed8fe628a1194ff58f5ba6002dcaab4e2824d1686c402dd7225d8f8a8db0100e400ca4778de4d742e79c66a91dbbeb3aadc08ae4e2edfbf928f955bdb9ae5c28243c4523265cdbab54e1f9070bfe9743e2be66420a9e7651469d36d429e84d309fd273388bf1e08ba64433fe72edeab9dd4e643c164f655315f10c365b5b8717a7af32036cc19cc8ce45fa8a3efc40a8bb924c258696ab4bc69ef216ed87c167268515b66eddb35942bf479212327039fd5ac027301bddc75300cf41586f16e5cd33610d00501117eff8150053a84a5993771cb1364f4eb3dfc711440589a4c95850e281f806b1bce1d2606428897078d8ad8deb301feae6b0b1e656969caf930cabd01e688ccd18baa6ec43a28c94f74cb1d805adacf0262966f600a06f5f0e58c3f00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2cef17f0a21f931c2e27a19ae5c09d950ba9f82bb6402b45c7a410836615494f", + "proof": "faa3e07f5b33e31f25346a2060c7cf09bd1662654baa1928835ed6c9218bf41d82482705bd701d52fcd9157470725b8be714887be2199e1933954bf3ad35d018d278188522e8eaac03f0879c51fbc24515851a8237ef292251bb75c250811a106caf4a578aadc5a797d4b2ac1d85e63c29ed22e068e6af86a7b0489e088fde6a2443326a06a80274cfa86c3adcd6bf21a80def059fe6f5a325d430f4c1c8910dabb735f3badc9e162cc661e9a9d41048777cb33f7890e7d2e7534489c9c39f049223cb998e6c8cfc951701b357a5696444a9269a28b502de77cd0bae2c661c0b8c631fab9526c2eb67ea227acc623676c4281795cc71aa9dd556a15e52107e1f469de17752691bfb889d62a46032cd769696f5458d9b12abbf133ebf918ff47e2a7101353fac8bb06341dce589c31c569d382aa7f3742bcf581b45909ef49264de55f7901d250bf377e37ed6e2964e15d96167c276ee89cfa8f9d4e26984b67a52aede96014f0c493416a7e450d20468dd8572c1403317d85469f4eb1fbc3113aa03bad0e98309f3be9db54dd613c55a5524a144ecef0c07572f99a7364eda24dc778323dd32d707dfc7449e3f5b3f3317f14390e810061633b0d731680b9c4594f471cf563f738f5495140d5601da7591c907d1204b2146ca80f1459443e82bec40efeedae7f6555c9f5a90566685de135d8926799e6830f617ba0eea1891339c7a2cd77ba435efa20b29addc4df2190bd0e5b195a5446e5bc9c4eeb1aa880fd81a806cfdda98ca3874631de859d429dbb8e55ca023c79b21748b318e97f861fa182ee3de3c917da979404b178a603b4f2ead6b6ae788bd678779bb625de035cef6cfc42a45fbdc966bc048c90081241193dd5645b93ee6738ceb315aa77b0d0ac6b740c3d024a05c68ff5b401371cbf1fd9e92b64b7eebb2d34827d412cc0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "54703b0a114c3a1f8a20d33450c5bbae01d5ece2340ffb731b4f486d23f26a1c", + "proof": "507a67d05546f7eab4177b850003a8424b243339abecad4514a44a14627c8602644e56a664eefb76fe658198f9dd8bb8793e75c9859ff87ac6bc10767b66f2403ed822d94454abd1da8906e523e2b024f717e95fdea0c45681829d263c89304b2447f44b81cfcff71786397c5ac39423a2eb2d18216b7afe87b8aa677df6c06ae1b5e885ba35a87bb877e5296ad006a7836c1ff6bd97c5d95c968497f1602906b8c562c8bd4264d7468ae60ce0541e0a889b2d2b455a93263fd1398f8fb48b0caac27817bd422ce182a1b1a11021f56dc1ac36cab960e9cb0ce6292cccadbe07fe0d318cdf2eb6e1cd6c8bc98a3d9ce1b5d3340dbe750423852fa414000e4973d0f8686562c7b05b7b92af8739150a674210500659a0e861dede9824ecd8c51662c1cb1308e08eac02a9b8eaf5af4767c39e1612ac25a58f4130880cf69c83267c7de713f04efbdf7aacc584b15e75f1f957e1aefdca8e4bac8281493cb1e5087a2b137fba007f71eafb6ad91f5202285ee550176568b03791b70f4a43ac2248b0f67cdcd8e612ae628f52770a03cbde6981d8e5c59fc2c96ca5db9f82add108e07908e5d06e0aebe5e48c4725aae0eb16ede4684e4d0477af7d5342a15817418ccddc146183b6c9f79de10e36ac4700c2e5a98ce9df9c4d931b1d2533739e5f4497e1a9ef2cd9db2d317fb2867d1bddd9e10ee45bbada3233e89d5bb9a8c33116a506ac14fa7e79708e24248db436469fc6fcffbc9a09a38b99086cd0a3ff3b666d48ea751df834eea79ee3ae5b293fff6805428c1ff928244e215be4e00b7866494f04129f42099ef4c679cb4960f6251d178758d895baea01f8563f8dfd52842b0098292ccf39703299b68aa0326fffab7bbea04f6277c0a5e92c6a2a4509651a320af1cc8cde20916b6c46d78f7d97edf3ac66196f5ef3320273a012610b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "548d395123185e9418b9b789cdcf49381e338fa00988ccdac76ec497895a4c09", + "proof": "d8568c0272dc9d4710fa29bbe1303df1c99f26bf081f7ed748ac85777b2cf5789c2df3d2d298a5f1222d2bf6a20e01f6342ff4584f71f7208eb29ac79bf16713d69cf65b343e574e66294f734bd438969468b6fe6cc39f8c6ebe1d327d9d5e25e43e14a52b1562e451f208efc6ea9fc6a7be710d7201351086113edf3529b51249b4b2e81b88504a68e0eaf0fbcf1e8db66f5b5af9cf60f26e53a734c0ddde02b8cba46734b99bfda9cdb1a5eff809052efa4a6f2a9f22022fc40e21eb75cd0fdbec19dcd504b573095eec1a9bee120a3acb403f859b7e4ba8aa26b8056a5b0e1636288d0c5b1241338dc27626bfb991b7c2186479bb39bf7e97a6e2514c5e4b7cad9383e5023582520e96d1a8725e3f4efdbf1f6bc1e4fb6a9b98017fbf9f443e31e7cba8400c9cdf107ea15edf7c7bcf2a8581d72645b5cb5799242a99895436196acaa395491a05b6d0441bc06f13b37f8145d245087d645af078997c2a603694657e484806c0db338aff2386cea994b19df0a80d5ed22426c137cbeeb160ac79dd877066ee82d3098d9dcadc2db2cae205c496f974c431351817a63c6f4e1a2e4792ff59a0dc37a74ea7a921830baeae818059735b5e507442689668e756fe3a49eedd924c50441c681af22e98f397a81b34c6f92be3c03798ca12fb177ee8fcf746736aaa172fcebdc93c2522a7b983a764ac4cf85c0514db5942828200046180f214e0051df7a65b010f5422e6bcbbb7d69cb8b91bbe33dd47f3c6ad0c58882b7c75682a1afd1405cc2367a4cf5036dab04b7767d46ec9aa81c6588148584ad546253e1f6faf634b603f8d0ebaa27b77c81319c231046ccda474c4f5218b558685f10ccd92c0c41cbfbc3c635fc4096973a6f4cb16795aaf910392f00070399a1276e392ed4fb678de0b0e6e3f3530edbbd6c797610ef368230cc1f108" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5c2211aad963956a5dc418bfa73cac3a14ddfeaf5ce07d796d438cf15e65d00f", + "proof": "3200516aa6b8816453cd757b70f6b3c57df03ba9257f77845095ecadb1e7576f6435eb2d266939385dba6f629e494f6abc5e9ee4029067a837edffa99dd4690bf66c1056de4d712c974ff9b29c530c1bc9a0426f2485720abfe3eed6d2e3cc54da3da5e37edd10df6aaf9456da15579f92dfdd0ae41c7103a11dcae32ffed243e2507f0f56a4252827612b5cfcec0381f21d7f605c7aefc90c1fdc72ef257c051293d1b68efff97cdb44e75b1ad961fdf09794ed0438fe10c9d71f9a5db6d106af0a8c2773ce7a86d7519ee666ff9a882b27e3dcb1546fa1a591a3ea74b3ea0fee9db7d100944fe5a60b36e683defc4761f559ddf610e785d96509b073ba381056643cf0aab9843d1fe6158461696b1599dd9cfc4d836090d8dad198d5a87e1026883b054448e4ac18d60bfccd18f7ad9fd4360e36578c641c1936592ed9eb777cdabbae80a80a92e1ddead13d2430c753d27fa23588c5e2c09d9893c443084aa4545f9c20a81431783821273a7d06d30a6c6ccd5adf11541b6ace9b0319fd379027b2a3bc928695c184cbd2bc375869d84e2b7a5cc0114e81755322167ca50698b834e20d0693c270aca7f555d6df6284fa47820539d4590588ed96135e4a6c186b9357c05553454d7d58c87bfb0f46d3acd46c2fb2a5d0a940a9cbedaf5a291ae8fbab8da88bfbc368bcaa6b39b5f72638618b6597ac7c2c27833a5a2c7337c48e0745708f67442410a1cb005f5bc3ddcfcc3b365654703abdefc2f808503e4098a561d78bf1800dc564f11ec116942ffc9e006dd4799e921a7a6cecfb6458a26d84d868f973301c51dd6a6deec6e32e9051945aa893a6e1ac55a3d260bd5d3e3f858e47e960be6f62507d35cda0208bae80c65043fc4dd40822a9f171410c026667f5eaa863b85b8b39a84671ca52910f38afffc6ec6322a8ca2ad5edb009" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5cc33a12b9cee033fa33833a2eccd2972eaa56a8da4db022642955026d707627", + "proof": "24bb36bbed58325027f9ec9ca9a9fcd5b199be296b90decbfa702e215fa4cb243635b74aabc6acd9d81208584831eb04828bd46ed15c3b07a5562852d8183d13f6c054c78c2ac3927f6a7a4d6828c9049b6783b1b90fbc2b33d32d400d034a39322e7cd82d9503ffe094ca2d701c25522639aa28c99d5efaf41bb830dc090a0d1e8686024f80f12b806cffad8d58a9303e505f71141ac5341b706c4ef019ff0261a8973b2333bd4d9c9a9d668a87672069f57352a1d2390a466708301acd4a090da95ac6a6b1e686061dcad5f0c7003641d19061ef2ad5a7ce69f6c06b3b1702461552a7a88177900c2a2fa81e4f6fac2d15f3bff704551468d62902a1b53d53e852a5b84313a7a07ca2cee8270f1fd9d413425c782120738b2a57f39e30673e8e22772ce6fd35b80af7058f45fb464d4d3f09e95b4cf8a7526f9fd211799e2b5a777c6780b89de070701c67cf18faf7926bef89b7a9af65aee22a6414c5bf2fd87cf7e4fc3e7975ba7b108b6a109e1c2afefb3b57f400e1eb5509e21747c90202849fea0f2c417995d6e6039951a830a27cc10bbf6f7d10c78317dc348e44540a19df73efd6a93caec759651d0dd17ccf4e7f9dd7e9479b4bdf2f6217cec740b60c1b143400780ec99ab4d782a6c50bd720456b662c77c9e9c92f7f38f70547ca2fee00df13b24032b65075c14dc92b05324352d433f17acf274366a25914502c371b29bf6fbd8fa9da74846571afad6e3173efde1b1f4864af2d2acf160b4294bca06879f1e7fac44edcad8b68b39ffc16872a17a2f18cf45aaa3b7fa9aa55fae1ae1f8b534087ab019864925d412728341facafdaa68631271c0063859536f5dfc4ee292e96d1510092e9ecdb1e5baa03e874e5124a22522b6dc35e7ce80328ac33175f996227cda731d302d110125f18980d6f664a5106e42535bde7de0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8015cfb69f90eaa2cabbb9895cd7df600dd709ce672ccf9241cbb7290b562778", + "proof": "7ebc5653b4c69ff86169660d655366f84c2c5be38a01058dc01177646e6539530e88d25e4d679134256b68af0bc00d79b6376022e7becd5d2ca703198da5690e9e31435334ac20d14c922e9cf03db3f612ed1f8e6afcbd5f85040f29bd62ab0fda97797b08f6d71cdddde2ea29e0bf1d5d912fb800b6d190333083fa56a51318e2d7ba6bb48fdc8ed06d043dc219471daf93080a824b445353d8be50681b06019189a28b0e443d47e292a485d489435eae209682eb37fcec0896443aa5754709f4b09031b206e9e8e7402dba91ab71098ac7eb7651e4d2d0ec0321b64dcfde0ad0d7de4321c82d51b25331e28a986a189abd11774b67bb362ba734a6a48f882d3e25662a692df8a4027f8e6ccda623f8a1fba252c9d119d4fbf4261f18eb1b6d6cfdf17b8be654841173a7397c5cefab9e6b770d10d4f3eac94f3514b43bba1148a1f7a0e7b92d32a83c718f351ba550289273a2f4bffc0cc36cf2fd161c320422f7b26d7233a4c45a9e70ebbd70ecf3674d7939e4da012d074583ed14da164ce4f069e8ea5c592b5f4859eec092c21a458d0cfa36821d904015964895606a1744c5bdeaedfb45d89bf063b114350c139dc5093922597a63a8afb221dc07b91dd03644f9589dc91b1a3965c02f4bdf64d7740b65ff362726cffa5442aa9279141c2262b7db0e8a88de302effa32662e2526b320afbaf554fc38ca200d4387052e68841a461fe34d2f750d28bf3935882658c9d552e0a02a7b2aeeb2d6ab6610b2cd08ee7b6ec157d5990996f4b9b693cf5bc2309b146102a45e0771d17a8b6490a09c27c15af1d14a63ad31936bd8e039f2ebd223d8b8ac45595df716ba5db0b2bc908c9edb1a00380a084495cb405f435614d3d1468988ae928d02c801a6406f94a73d3a8b0ba3d7addbb1f6b4c6185e4ea2554d9ce4e4e387de6a1097a1f0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9c677d2512cf478ed4ec3603b4abe96db7a08f7a0c77d5000300bd684d385e24", + "proof": "f02f85dea3090f54ef19e9a78f21f8306b0a5cff2e473665bd1e5e1c6334cd1fe40977724c35baf7e9be7a6bd6cbfe9c8a3ba2400aedded3d1c611ad306e176eba0582ac4fa112711dd571ee7f029bcfbc77f7a9f8d25c4cb89cd4f6d7709c2ec27b48a2cee4b660bfc2309fbfb728bc98f1e132f087efa9b0ec43da852aa9270df2d584ba47f6051e31486043e422cc78f6448ca6e6b618415d0850b8d53c0196cb8b20e2da51c4ac74a5e99eb775a338f2c84f68c0e2f8459c09696513880eb2024ac51755ea9198f0dcc9f817af405ba10203d69c7b15e96f3154d2fef80040c51f7e11a6ff2d9879f0cdda09a75c805bbf3e978f6e270b130aeb1996f15040d632c4215f162d064b466adf955f281ce9d23642f5215422aa12a53b52c34d0e3e1bbd58d9e3ebe11f049364d7a65d89502adbb0db3f3daa8da5b3b603c73fe4241287f9c4d7b3c49f8efa6dd4edac740178d4fd1e1c931ca4d1333298571d0e6192d7a3ed7cb70e975d6f8049a342ae34b1c80cab60e8da949651ebf686223ebc23d49c54fd8d8f2edd77251021d26a66124450239a9ee85233b10f7a8a675ad4849a3ed9f20e655229e57d694f574250149b994376d3f6c895c258341f1e14c7f5025504ffe113249fda08a08bc12cb6ea0cfc3674e49468a874fa4c6e619458ea74109ab03a5420699cbcd17ec172f6e048944f4277ca54f4d95444ae759ef48840d404fcb9d2ee709f1ae8242fa51d7026b74818a6a36459cc237fcd6a4c40b622bc5f92f3ecf0a7036bdb16cd5feb28c447533e059edfb9dbf3a2c5069aaf887e6ee0b57fb92226caa4232319f955516f78d116881ae5466dfceb88267574ec35d461f78c6d2b2d02316cd810ee3a040edb775e2d1a51ae106ab3a303cf5b280f12a0bebfb46b663f898ab593fd70a452040edff37fbf04d5b04e0806" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a2d502025270ac4059401dd1ad492c978f6329ea5b30dcc905853b6bbf3f7a31", + "proof": "4e2e872f2ae61c2c7892ef9c934cd3401a5889be43819800dadfc65da9694229f0cf090c09db4d149ccff7e578761962e4f806c6f2ef8c428d7dd5345adffa3c34b106dd18e67badb13d3c7d6b46f9f7fa46a28c68c90b6d27ab36152fe63d3b7402b5f155f6fe28b051792cfa624e41ca08c86aa20cd7e660ed5ee53f37f03deb91c33b07062e522b371c1840debd01701115218c3ba8b75f42ec3c0bcbf908c76b472ce2519ced554105bdae28bea9f2e10d4b43e105ff3b2eda4cc6dafc0e231084b021b72e29b054d364addf91afa1544db0fcd458579df171acfaaaed02f86944762da1480e41b0023b75cb81c66154c19a8a06b50140305e06fa1e1955964dc0dc973d4607cb39da1c27467529c0d819cd1791931999a62a459c7c1442c0ed8455d9413f96a44cefdf0e6a1e0d37b4ca33ed5b2e03947fe6bdd6383f43f86104ba896ae1588a815f24d32010717d777b1d6510c5000491207985c0ef44ea07bb3b091587dc0d1d721fcf4e8b30e6d7ff8c6e6d43207d5bee8be85d2347f6d6e57091e871eedea2231175742ec688f829ba57b14caf79f6a55228495d54905af96df89dc8d3c925f40e6a1ff70a7d0e2f4847bc2e1f93aaba59b209f9702432f34041e982903c5479fc9d5e1e1d4c22013a233311f790369cf2298d801e804e870357a0ad71209ff6a9607b18e0b9cab2763cfa775ef2be958a243d7741468995f282a00fdf09c8d32b00aede28f27e433456f5e13ff538c862bd22e1256c88851c79d275ad3e5197d4c444f27555727c2977f42f16a085e06606430231045783416dade184124a0604ce580ff8b2a49283c5478afdbf88c1b7446d6e72c24decd987374aff734245e247618861a1b5265abc01b42d1808d4f22643b5015c57759de1d11c4bfe0f012c34f2112026a8a0a10392396c4ef9efd24fc3f40a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ce0990eeb775f239d837bfcbb9db0a963fc528fc09665b9b8d27952f3f1def63", + "proof": "26ebb4606de701d153d0d9dcfbbc71850788db0dad6886d192090a79090ddc677a2d180339188c79e32e9ae91afa10439c1e289b895f0e329ce47243d561cc07a26a484c4e1475bcadf752482753eef66e21c840851857975b6981aa0d0fc2226ad14b4b169eecd49643d65663e0d156b4fc321a58125e17728193b792f6ad2c04730bbb3148a6a0ced346e03f83e12e1ed7e078b4114a0e833501eb98bfcb08ea76e93b7b959f91203c36bff5b75dab114027a03121dea22907574fb15abf0238ccbfc8e86a9059e856f4654711ce5de2782d66c8970a08dc1e6a32338f0c0a24f97d68e4bc242061bacb1a7e0600c1d8d8cb6c0a9580166b0dc21e451c0128926311c4720c695e9b84715133c61e85b76961f50d84e24ab1d4df87306c7359c6c32d113c349fff9f44ba1bac8873021454065d058a768216432656d725bd6e1c1b3cc1ccb7215d8135c1ca5453bbda27de07fa8809747860f7fb7b17a36e0bb4476f5a11a2cc3f2844ec6aa34f9817017e7286065f21a2f2cceee033b2fa27668f4c580b7ca8c75d318ae1bbfaf0fbc5b4e88ad7132066e6eef4f1d8e6c66a68c3df13ec198ce334f6b50346512b9560dac02a946271c19054c1c355eaa3090c476beee09c82a2c2e0a8ed8e417c1917d0f6b0fc064f4e3fbc807285848d2c72881a60f3b3061e7f1be6d83b90281e08a6bfc5ba100ef4c3766a97c25c676b1cbe9655066a4700be8d6c21577b02d784fbfef965a591e55e369594e6778a576af946629271b700ed5b54f3207a5ddb113d7b44bbd94f1e5be276485dd9613d9059f837251c85e64e333adeb472b273140071194bec17ac5191c70125cd0b79d764cfa88ebc5561c8c9925ebc218f86ad4c2cc67fef7855dc0c8044257ad9099b4ff9fae4580da245f87ca5f13e0cf298c7df063ece0ab8cf66aabb6aa5e609" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 17 + }, + "commitment": "62fd4d95c4bed3b7c245e4bd25d243fba909551253245886172fc2ada34d6f38", + "proof": "28b655b9ffd4c2c2ed9f8af864d0bc109606850e9d45d895c9a8d6ac4b783650ac7acc0ac50bb7f08d2b585fbd5749d0c29874ec5ce167a12797f14340c73a7af806046d9de3f23ce4aaf923c951639e53fad8e2dd52c125dd6905a18dc39b0476e32d819e9c92b01a05f6fbdf2d4596cfeea2e6125722e9232525aacaf70c74a49abfc9a53b65c6cbfd8fae275e58564346bad39c48b19ec1019ad9ceb4e50a56dd69fad7891abffe0e8a7a336b4637d3a62b1ff09a11597de210cea5d18902237f02afe5559a59bd4709319ae0e48b1e41589f94f5cc8c319c9bd0700470073e877118680c9dba8325c6e37f8d9eda786618eed8762401a47d5f1bbc9d943bf87dfedd1c5c0c9f4f22e26e3dc1a5e0f4e1ebee7fcbb3c0ee36d49d923c0865e45a0d77238015471292248cb73aa7aa97567423036cf2976ccd17e0b8a6f144d2baabd00b6990768940b747fc3522026df7d9dca0aae6cd662748c14b089f1a9e952c297aab9f528414c483cc626219fb20d810abb61e75c90c01c0bba7d44eea9b8bd9b4993f8276ab0eadfb4cae2de54847b42dfdd0d31aee7a627ce46111824bcce1488f5f25bea0bce46a1f76d89dd2c1e3fb18943774f8ee3da61b3c1530d1d15615a81b372c1e4440ece37f6519c2fbdea21b8b8355bf24fc28372a0c2826824e67352d7143073d3767fdd5a752bd7c1efd0ed594750560da963e05020cc31911f925267777c32854ec4962542f7d0f4a58fd48b9a23e53187cbb7179d8764d75366d6612657c54e677490c686e7ed6cba70793c823f38d36d912873f882f64de6870c26b1b4c9aac318896cdc4db53ea88f69e799907230519e34e4d7f46e6deba61ba5c863ef6f6256c39febbaf6fb3f104dd0064e4dd4a17ebc0091ddbcb878628ae7a4d672b8e4dc050846bff81a6ebd72d5cf9a189d315ea3c01" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ae7bf62d03441fb8e3d4cb6f5096bdbd74983a903d96876e87f9a98126ddc441", + "excess_sig": { + "public_nonce": "524f5224e615441da679722d4cf08e9f163c617decf52cfb5bba2abe5f892905", + "signature": "00703dd145ddb03d628f4c760889a4183c518532200271497985e63168bf7b04" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "c035058275b96b11a2052d07f21975e551970b50488e2475ef7be663c99c026f", + "excess_sig": { + "public_nonce": "bece6cf9d7fe54f717f98a3f3fd39d5e85d214de544fe708a2759cd91363c862", + "signature": "e2f73425fc950ef45b86d120f5d0d1ab73b77781b0898f79ddcb3e394cee5b02" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ce300631ca6d66e1752889e8a47de28b65665e0f61d5950be7a262bd73ad6307", + "excess_sig": { + "public_nonce": "3ab10c91c976b9a6ec8135b97431cc5bdee6cabf544039afabe9d28e0010982b", + "signature": "0c577af78e616bb9631385a7630f06ab7f0d5649d7b01e6ff5a6d907265aa306" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "dce15b442106399a029f4f4f28aa12b2da342e2342baee4faa576ddbe9168530", + "excess_sig": { + "public_nonce": "1c4d4b5eb3ad747c76b72d20fac33e7596d68d32bc0ca2d7128f6ffb6f9c410a", + "signature": "dca42b1449f5d825745a3c3427698ec8d0b07006809b5b4bf8bd3d18c0b59f09" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "e6c485fcad1813e8c4b414a0a471d37d3883538f7ff47f1e08d2fc02b5f6f66a", + "excess_sig": { + "public_nonce": "8895c48113ed643b5432071718c9e5581ada6dcfd99549c77eb7adb8074a6b32", + "signature": "9ee1eaea2344c63cc96d7242939aa2a7ceb1e9a91b2349489e7ec635fd68c401" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "76c10e4ad0580aa22a83232223169b84e431637ea688b333de7790ade5e13d16", + "excess_sig": { + "public_nonce": "fe145d465e6b21f4e3bbfb4d8791d2de45e47b33faf72ef7255dc5c46339c758", + "signature": "419c527a53ef6d968cae2ab89f80eb0af6178e9826424acbef8d247066f3c200" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 17, + "prev_hash": "19c746d67cb8887633d575b4d27cde761180581db290b1abc552c31d8d04e12f", + "timestamp": "2000-01-01T01:18:01Z", + "output_mr": "d2516e31037120df976bd17078a15e89759fd0c95acf345260f6a6f5ca92bcb5", + "range_proof_mr": "6ee56645bb35e6fbaf4ff7ca2a249c589d31a4bf22f41a34bcc251d34db9adf5", + "kernel_mr": "0ab71ed8f743de718bd52915a9877b5b26bc145e9f7fd3f32d4b669ce1070b5f", + "total_kernel_offset": "c8b01d6ac625c3a1b97093743fbfd414312115a76c8c4d50004dbba555284e04", + "pow": { + "work": 17 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2cd2d525604a8fc904cbd0ba6470c7755e00c2e22a6c18e722abc1bd48a28f01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2cef17f0a21f931c2e27a19ae5c09d950ba9f82bb6402b45c7a410836615494f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5c2211aad963956a5dc418bfa73cac3a14ddfeaf5ce07d796d438cf15e65d00f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8015cfb69f90eaa2cabbb9895cd7df600dd709ce672ccf9241cbb7290b562778" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 9 + }, + "commitment": "2601c6458faf644e71550b28a6d3fbf262c650fb145d4b9864abb9c59e3f497b" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0a18c7c98fdadcccbffcbfd919f619e71c8a344f0b4b237d032433da47a4e86c", + "proof": "541e7c45cb7d1e45cd37b454a13acf440cf4d0fff6ab9abf2693396e723eff2e26f2460985d30a134ed5afad51b098cfb6017b3da9b75885029184e814757d675a4c5e8d4a0903149ee8a60630495229646ca11a7318c0d769b9f7119dce7f717c2c54d392f32e5136c999e3514a9692e04a9db3957e56fbc5a01f2b2853ef7cd08e4bd959ace1c4382413b23254b90d31fd73add00d96b876319f9f2e98770fcb6b0dfb730e49906d1818d0a2a246e22bb6f4aa93ab1c84e814292f62d45f0beab6208540591555f981e9b47471f43aa8b12ebf9b1acdd82f8e4a523e1aa208e61de8bba19bd3b63d9c149ce187ecac3ad3896ddaa18f4b7fc81e0cb0020c7bf6975cad1bf3784e30bc8b7dd2724c4b04c5fd03506ed68f1cabc885acf07c2acc17ae80a96859674f0ad467403987b5bd8696739eb7aaa851d5d9c759806a400efe3d0faefceee8e4c8c82830ef804b086d47cb1f84a5f5e3fd439709b9b375bc980d27b8d2eeb07aebe09380ace845f9ab5f6276f28622213190171c93937eea81928ea3a08c1995e68a2c34b93340c7d43f8703014bb7a39a7caadfc370615e60e6a39c5f25e6f17d994576304b7a1ca28a59463471fc9450636abcae01099c0a3f304ba5035286d546f9c3c33cdb10f3c05900040889c6b5964eaab8c538f4afb53785b2c0bd568e05db2fddd5426cf286751f146ed2de18a83169b51543029e5853f6a7dd4c7f78160b82d0b1f7c5017656af44e8c99fa8139f5ccb685db4d9b1f1f41d8c6e86668c6767c48ff7bd3477416ae30aea7662ca1ad9eb1a72fc500ed2d97af2a9dbb96e3d6ed15850642a5e0f8c07a5958a8f145e0e4251117e7a9bbe1548440582d001a4a4baac20d9044205bb1fa808953263b80b7d310e3fedfd05b7393c8eeb043c8c38fe6c433288fd2301734e08a8d61a0cd19bfc0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1035affde7b5dcc6023e23a0633e706b861be19573b836b940b2329d93e06d66", + "proof": "16cc7903e9b817593cab3cfcb66bdb050a91b3d4f88f3ecebea9f3fbf68b637da4c0dd14841620be190ce22ce12d656573703161ad7724efc50365e340d0db01049e10969822f811c1ceb59d9cac4cca78df19656aa1c40ae5ab44753afe5d351e71975b716986891495ac866fe185f29df9be63212418c63dae8ecc5446761401c1e70acf46c2d5339bbd80584eccb73f40e7b97b2313fe6eb289f73e960e00e03f021b0f6b711fbd6dade3ad1338b2d44d12379ac150e5780e37ea4b887e0ba453316575ad75ef04a5929368499a25dabceb385d2ceff17a9439715f13410b0c953013be848c167325c111c3da2431fff9047d52e0dc252223c285d189ef7f4cf83258ca112dc3be902c603030cfef4b56acb368d3e4111daa973ad143ea50fecacb554d374c972d43a10ebb799afba056400303aa14d4cf6d9c14c2ea114f804d4b067a66d52d51224ee883e33ef3e129a38fb3c250d3459e252a2f81d21016e776413fd7cb07f5f1f7a83a57c9c53b1ff7fc3ce8f3060320767cdabf3c7ec0850f385962c1c965d0435f8885de606bd9b456f35af571dd9fd39b0ce8f104c288c02fefd82d69cc28f192c153d4c1c95a122b99da62d074c400b23313e57f0c86f4098a99796becaefee833e72f99984d2c1fbb73a358eddd8748e2b0b972defe20fff4e5c4cb25e2eb5568437bf6b519682a96d58f542aa5aee5bb919525147610a4d4578dd84880fdecc4db7593c8a3ebc6eb79b8aaa7caa0cdd99b2f21c04a72d4b139ce0769742b0a83eac9a397932e9e4fc073404fb35b12578c331d48483678f129e7a9bd1733e27d3cbdea45e380e7714e789ebc6d6ddce4b2f2783794c1e38697aeec6b7145a721e68884244d515d5adc7d13b7d6cad518e9bf0d9ca92262ce07e2090b35b2ee7c0b98cb9d67c324f532100d6d3651708aa34406" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1043fbb90156152c8f56513ca230ca06150a19d6e7bf696855acca7ac8337248", + "proof": "dcb39b2f5544222fee9be2136968e8daa0f3a88095b295c222655f479a9316016e3f35fc3a65330e955a187d85c51a8676377796ff972ab7dc017ea5871e3c3a9ec2a80c5017ec5f146e8e4d3c451a3f51525b2d82773e8beba2cec287e21f74085be747cfce224c084144532cdb2f883085fd2dca6b83e1d72ea5a6a6ce7f05ffb38d02bb751bd219d880e193ad0eca1ac597d77987efae72ed2ece03a7160feb4ae978c7ea38392f0e41f32a35b10d28682613e1ed57c7edc2645cb309890c3508a253658e028aab7dcd536f2b977d610cd9bc88177431c62b4860394c410892e324803b4a73a7168765fd767f1058dc25df4acf542e813144afb177a4dc370219f96371d7da1dfa0a9c56e0451aa1b827dd86f2134cc463422a8f7b963d5360d0388718d537f3fc9c96770edf91ded0e4bcb762dc075b19b62b3dafd5460d5ae62fb45b3bb80eb1c87c2531063eff426712cce89595c4ad77d80d9850810d16656b463d0b585cd8c06ad489583dced03a04796798178e3446b02140492c5ae01c76fd3b049b377676368b7d5e131f9ae363f611cee833716ba1408e36d02f1eb3614efb0a08016408bee634966ca39830d736c98aed6307e7ea4720de4c019420b6ded30e0b065680e923acc8aa7f7b520c4b72836bfb0e1112369ae7f057ec6159066d111b9ccc35fcdc9315200f116a2c2dde0c9c85a86c789a287f087d7e6c7809eb621e33f7eb52a4bbfebff3d39940401b1b54397d8d16435f8e88511224bb4c3d267a8fadc447824a3e7b0d0f9a49a13a1e79a765868f1e10fa30175ac1c0737d54883629921a5b194e78047a998ebbf1e562a7ee9acd03bceb69760976ba0c8f52a3fd1985816087ca92f0a9d04d26420045498bc3a1dbbc8ea7032cf8090964476640b2140aba5f0bda3352ad9f87ae0ac20190641c500d5ad207" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "12377053a059a00c08de8000654addc8aec8d880805350dd4af1c5a898d2a262", + "proof": "365ccb49a1a91a32b02e3e63be814b433f28d3b2dde875e1c6471db3b6f036579e906939f03baec81d08490d35c95de451d8022885cbbb299c70199fabd03c663287f953c5837ccd492d68fcc710929d52d20fb97bdecf5a9e3bd9cb56c46527fee55dd0a2fcb46b295b90c475f108f50bc09de36dacfc1bc27a59d164911f49c1641d61882ccf6bca4ea8d044fcd7aaba567703c8588d8ce340fee7edb4d607b389a11c29a0e49869de780c1b2cf5d29a6b84a3de83a2de32357f611038c20ad2386ce9625530d8ca0812c4dea36fc32f65f4b1ee0a8505874624df45aaed0bbc450b847ffcfe6f7edcb903c4b84b791537f4d95d19c8562aa451281a7910507adec3dd1cc0f73263c9cb506780e18def5b464d2a9cb4ebe8d676731f3cba455c091dc841b0232e397547c4b0f5197b4d7dae4109505ccd18ab6007df47a431fc8e24d251ce3cffa9a3cf4a04a99bbe8805d20511066560648d4712dc6911124638e64538beec90046c16c5022e9d4edf9129915d67bc37e76f187ff06dc555fc74d0cf3b6aa08a4163018d10d4c5e4c74aa7373a37b7906c328538f118f800daa69456a0e0b2b9d6e5d66c505571992f74692bf367e65c718418aacaddca466a3ac04ea78e0b9154c84f6c6fa0d1b4effaa8762603299c3c30e51f171ab0484a535d2d6ef9c9a71aeed49683a426c4a1aa657fdef56e00dd99011c7ff05b3eaa4d05e9190edfea509aaf0127350ca0756f4cb8dd752c17d275a525aeb3e90e765e4f67c1f8fc16b228c01f54217dd52fe9ba52f0c58f1b4f3b05abb0a1193cc8dca645d6f1fadb9fa7837a0fb351a060aeb45d8ec5469aa842428291eecf5c1a15a952a895268ec0b4aa8523367d659d2082a5e31ae832ebb0525eae1a130d99f12ab28a52b0091c2bdfe11b00e1ffa02c5a88ef1a2a9e1b6915894b9d1904" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4a48629ea400ba9e2116fc183525f8496f06d8cfdc018ba54d2ee82e77a59402", + "proof": "d65672c1e42d44803bc6c743186a5b66f10df5bff6feb80b6ee3c4a326fcae19909c8bfdd8b6e41b3fbe1487fc7cc38f63aaabe59e6d39edb5eccaabea6e214f5c88eb30738879e28875aaa4f969e8ed6f4b3bb94c0ba3d3006dc144d92b0735428b6b0fc42ddf9f647f98b385de7f7f43b9c1eaaebb66b0fa882efe6e11897ff9c1e7800e54f016b1c33b3516db3f5b7ffd4564287ae9a9dc340308e2a9f00a498eca860d377bb71776f34cc336f623edb5b4754df7122b76c4045252d33705b0298c0fb149ac62f89d829bf9dceb36a8444939a9bf6c45fa9c2174e98a060adc35612e28109428ded6556ee18e636794389a344dedd9aa8647cef3e6a9e56f185d2e1e72d04a33788a5f3995542199506e69ca962f025b1d955c4d6213f623a6d211f6c1b30db5b9b0298a0c77f14b5a045e539a951c6eb440a67e4ebaae42163899582624683937876d318b6ce7211e09d93b4db24f4a3d344c244ad1104c52b2f10ea12761c487fc51b5724c6f3dabf441216d48e309c8063c4b11d41f1f60bfc80113dc9d764c9374da3549d4087bed2f36b40df7890bf76ebf73837e1a4826989e75c03545971ac4227f21c4f6a16a6a212c98a3c7a1056fedaec34e3bb02988f0a4e38cb197a9ff6811d47e3ae7b36498a47338f5e867ad1606e1e05cd09cd56b446dea8d06f936cf2073223a4ca44806c267f7ce4c1e6cf54e1631413e573bdd8d2e3044f779b1582b13d706aadd88bc2e3e49d48da520e2dfa3e61adedfc333b0209f26dd69b77a0e44551d3b15a1e6ac98540e052bc5eb98435d175e8f2e94dd12fb72dab78af01abc26e54c6a3a1c5f613f462eb11f096b86a16661628a8a134c69ebea31397bd28ad4e59e38423203b56ceb433ccfcb36b231010155bc17318c9b4f05211ecc6db0fb454e43868080f9cedb7223bf26bf5b390a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4e98bff77dffabc8ffbe6347fa75ad37f17637c56ad6bc5f1c2b2ad678ab585f", + "proof": "628c8b33785661d8e97ae52e96d322966d68e0cd34acc3c39fa2626bc76b87149e6f895cec069245febf76078375141de65c54b52f3ca53c7660d8021589386726ed1fb0ce32c5172948002cc92152b15e0a072b99887bec0409bff6873e7b48f29ebdfebdfca66af7dc923828a0855285bfb258a05cbd85caa5955998530a463c73f28ec3fb3724a44496679ac8a8a177ee146557f79df4756012fc66dd450f71c91c6a762d9848ec902f3355e17ececced20dfd5aef00d88b0928d7ff6950b1c36e19ec5423dc78d62816bc0afc32608496f22fd7015bbe07722eebb8a7d04ec014f8fa509ee7a657bb29741f868835fe1fba63bf97bf01d2963782c85067aa6660b58d854f40a73873618d0ceb6a0744af6d565f8fe2a75d857f99458691c8cf069eecc5150570fd099754e78d8baddad105d51058c92780aaf6efa4793323836858bf1aa94e50b7d2963b293a2acec4d6a03081428a2016df8b9ce5c8378165c3af19b193d4a942d23b033fc1a7757865f315177f86ff357653e7bde3126d804c4805315192e8269bf0467be89a98579d998945c2bf93f5d9d047163c9030c888d8ca90f60041d6930cb9a37cd6158f27a18f5d046397cf02cd8518c151f5896c516143d3ecd7cf41606468f909d16c950c2254b7f5018259b3d8b5703454c6a10b60b8d4c3b04ed73a312f19010ec65da861980a8388ac1b7d76a73b213b8da2de67a4ef2f5fe4b4d368b4f4155a2c181d1178538aaa68faf78beada22f2ce9e9ab70ea683be7a3ff3d737c5b44f3879baf2737d9ebafa7b54f80e4be4c2e3dcd6fe31b73244aa0609aee93bef773abf2a9fe1f17eb420edfcc8a930c2b8f4cdb317730493a7eb966f4f7c491b7292de4ebf8c97f68a39e5dca21fa750da3f3bc71f8412369446fcb0d927cceac7490fec227e0100eceb05ac78e991e0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "663756f12444261636c700b3c00e7ee95e35884f3cfac1a6ae1fd6032a8add58", + "proof": "548219c332319e9c0704a373e5cf7d0128b3fcffd6bcad943b9fa0cf248043275cd2df49d6a572197688787526a4a93d6fbc2d20cd95c77c0da3ad252d06de0a1c555a7d4e7e522eaccdfc5356dc6b4d89c61923584076be8fa78dd751b33767d897434903c75fbce2d121168193abce446107390e2f7a6a53395ded8d86fa5dff59e72361338916b45f401c19f3e671f8c7835d55a9229dc6bcabef33a7e80a06ab47656b4372a84cabc4f27779a5a1b482dbb26e85ffd854ed31264133ea0a73e8c1b4f6d0653ceccff3b7895b3484d6364233d44416a638b6ba135a5b4502327a3c0779524144d0501431a5a184348820db89a543b1ae1e015d61a9c6cc57380fb2b4dcec9b2e791056ac62e0a2e8494edaed77b8f6cf548c3b0ba7e0d428e438f5a75d9e4668c9d09b5a440a84e76b5a01b93b30d318ddb92a2d5c05e152466ba7b78176973b5b6130e1943def15e0b1813497b26a6338bc33d8857b461c4ce79940eecc86f9aac49d5de88b33afa88eff93a6592ae23f01b401ed6d9133b2094a168ebb5169b061fdd26b149598242eb2128e89476e9a80a257935a60305486ebf3f11499a234b4095db910888993c5ab41e8bf85c87d81cdffe177d516c8e372538dc561828c429e1b89fc792c4e235fec73b5ef3fc98c16189ead4a6ce681a0487ed9a252c909b6d4ba9ca9bbadedc143e725a45691761350dddc7d46d0fa4ccedea07efd4bfa2920d631bed510da6360ea51def2dce7610ed660fa22ce95ffa0c6a528416b24b883119703c4ac8806425fa8a70c28259427788da33a8a6a0af4874e5c13d29d054bd8f96a850afa612e573625007b107b77a74dd42ef4c70473abeb24a108f3ece242f934f6c8fdfa3c594525d40b9fcb515dfb9d0063ebfbe3ee683ec79afe1227b7ae363325c9a91f7f8fc0170f14d1f20f435200" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "741a7cd6263d15d1e95c430699c2234fb53c3b3771b7ae4752d7e09b17b4b235", + "proof": "e6d3ad65f57dbaa41629bbb44bf01b9383fc6898f41f74da80d7d66c69bd64029c52e1028d6565d954245999ff6cf5dfc76a24569fce0d8e2dbdd9938ec74108624d3298f0f61b7eaec9b600fed0196f709d9eedc273bddd406f40a93bd56b563ebddfed5331c2ef76c556f50e01b167b5e618810ff0d70c9c2276591a2ee83865c6cb7909df7b303922e8a2ceadc22cd491aa0adf0a2c0768890d8bacba1d0f9a2af6cff98048298f0c61d19844a46cf0a2436950b1f7d05714f0f7e013c7005758c3716103678ecb2a20d106612e1ee6e69b490e6350847aeb6cb280ac780d9a07d4ad6782912b59b0d7b8ee165f41a166c607b31187a583f0482851ebe75bb022b480c2eb35657a7e462ef16bc4b33f54c829ae855e2c460f0bcca7bb97559e0b08ac8ea5953372d034100c39fb3ce98019a0175f4cd8a74fed3c7edf521d3acfe2b360feda7e04ce65bf159330a449cad13fc1e593fcedda54a9a0ea0001ca862a2848b7a37fe86c581023734c088930b79d9731e884d4145728a682914474b76e2510dd6c94beb5955f607fe66334a4c3b696c1513e1876655d75b64d2af05711554f86639940f15669e721cba8aecfec3c5fed7f05218550459fe87a002e3a19a4ed2bdf8cdef764df66f3094c136718041301aae9c51f803a3115754e8a42442a9812d44971addc4993912879d50a92e1c625c4f34b07cc3e887ff46d2428dfc294cd174400b17a9cf591eb372738ff0d867aec040ecb5606aba6d4746ce5c88b407b14c36f6c5c46c583923454d655468365610121dcb76476bb3f305e4a4a47a52419de3ab4912552f4c9910d47ed2a5411dab6ea3fabd85e36bb59e3a7fab4ba91455173e34bae69040ea7526856decd8b2c4b928006a23f11f10855ac0846bc7b0392c8500130a7f1c1c99b180aa73b8a24a207a4c2e4d927e70b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "eea33f7f66f2d5d228184c7865f2072671a2a8388e10a30357435c24c4894b1d", + "proof": "288cf448930e3258eb42084b68964997520559f0ac46661c07eaba4a72720776b461f80027e06aaa8ef69ad4b1573769659caea81cb45f9c4d1f93293c994d336094cbefc7b5b44d101f192a0fe3768e3dab9ea9d74181238e59980a054f6e0e7e005e9fdc4ca3737d52410b23cdbc172498320fe90c0925a0437de790f70119d82ebc76d9cf4e3dfa147a836ec51f86a7bc9578f81445148a1d8225387cb60e72caeae2028a90c6880c17a6f568dddb89654ee7fb3b62eb0a3c4d2c268107097c1f17c6ced045bec68e7c0a0647f9837e17b0b0f95fba609d8229061dad3a0c363b3a5d1effab0b790f63a690edd8553c5e378a70a5962707579683b44ff42cdec8e85947592cbb41d4331f0aa646636387feb9532c7870c3e7b00f24a2c97d8ef6cd3105e4439a3eb286578a36cc94fb13552b30502cb18a65e410b7d1677fbe81b633549395a4d6b7cd6a8f25104392dff17cfb14d016e963c71e3712a23c722a76f93b75b840b0ca0228303e4166751f271453e2a87d5941350dfc48854a16d7a5614e8d382dce86edebe9bb59565c81f7cf05f48d1cbda8d76e90cc290ebe3da2fc7c40dcd1e3b406b0629ebb59430418b258545ca0f4b5dcbb6db1bd08c43c72283a4b4f134c154db056f562d86c8c484c6ca2445d3e9e4245d5b1c5023ed6353cf97fcb05df99df2e725b324dacda9e2ca8545a8c4e9fa5b008dcce11f04c306b422d68b3d0f0c6e89c4bb48636279d7399c692f9dd609b33e60f6626f693bb1c0f59d6bcff9cc4c83790efe3d83b9889883982f3b04b10512cecdf6aca31e48e84b1e4624a94b0dc36315cdd3ac867b15401226d3dea55c4ac96cd713b7703dd6780389dec2181f22ebc12070a401f32242a415de297fea61f845b00d748e2d63c755ec7ccec58eeb74a78f8891ee64e60956b88d0fe6aad39ea8704" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f07589866df759234e481ae8c64d7428bfcfdfd34f3416b3345198bc52e5e70b", + "proof": "7636e0553d84b29e8f0705c843ab9a75e4b0fedbe1f97242bcdb9aa9acdff1124a2642ef47dc3490ca7187994b27806b328bcec7121619d5bd7d07c533ab96161e7c2692f59b79aed3a1c0af90649a4a711e7e4b1ffeb8da4801dc23d060f76180a3389927835a9c4aa44e338d4383d7cc30a9b49543e5b91bd85023403db80d1f22fef81155a48861e79911f6da9c5eca958532661a69a2e2d2b2165a5bae0575cdd4cf404a3a395b1db3c8be87c355a23cc164f2d2d6dcc5468530f5a0df0b90c0651bbd04882459ef41612d09fdf05267343f2c3d1b05ccc3e2c7e7e8610ce6fc5e4fca81e8f19a66362600d72ec9494ab7fa7da1c317494992407b155d5d3443f35ad91c9e1f16cb10d063be1bdaa71a9de8d9dceee62c15050211ef574224341c3834923993802581115f8a7851f5758c272870237f5f928897e802c25b34b500cd2487ea06934c42195a8e650f3291e8c6b945e5baa663fd618e0d463c4c0add4eb547a6f04b1ead2384aed0dbf52c4dc398dd83c1ec4c94572d680e02024715200855c36ff58b9f30abb1fb255077c68e3be1e6da7ffadecedf95c46482fd121c44ba0979eaaeca5178d6cfbeab590bc17840dd9b09b640d11a27a930ba9ca3902e398a90ec0b60db754a5ec8fa3e6af73384abfc3fd885b1ca98f130b069b65af744f0d093e218a0e73512558ca9d18d32f928cda31ab4eee2fb1417d4a91eb8beb995fa2b5480761b1cdaa54fd738a851f482928b34f93f624abd6f3e95ccbf439e28d9dedd177c952058e859987eb83bb9e0f2af6a891b8bb596595a318eee1e01927e83298a3517c0a07174631c510b2ca836e397120ed852a13692c55b5403ef1223e70ae7b19f2c5cbeb35e47791094134a1c0b0ecbc78a270c49bdb57d909a2ccfbe4bb450cc24883656423b47384fcffe9775539333e6510c" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 18 + }, + "commitment": "3480e58d181643d932004bf4722f4f8ecca015b2ab1239b521655a49b225344a", + "proof": "b445e205c6c494f15d3c7edd7e832f3c4144c77ecacb43ba81313a4c002b35185ecd92cb4715386851308516b57f7d0d17661673627156c8da669f7ba9dc9f1d8414d5ac30561cabcfc92bad137a9b297e160486b0e982fd5cfab300c81417515ecd1099cb70c1ee8ae7397805695ccd218f590392f146e2f6d63633a39e472e56486b16261c97da7dd775a49b6e0940e122cbb9dce66abf45fce9925648220aedbfc5fc23f331cec986c599b151a14897602f0c4c4a8ab01fab51d72384850cdfc9221f08228c40fbf42c577b02325561ee5a8c3ead9361471e5db1b389b00d40b02b784621809da67ac101e6bd529cd9dc94ed65c1d5b3e3ee128c16b673184ccf3f161bdc348fc85e3f463f5b48fd3be2d79e49445b48f6c20fcf25e80e32001e3f2152563a9114e10254ae0ab1ae1bd96a53c6a850d615e0908d9b4c321b16e95cd4d834787876fa6763d08f183421318cef1fe47f3f7022196d2674da50a69feaad4caf28d6d7039c3ad0f81f2fbde1e9d93b219afa9482a8fa07ca79311a6d3254642ce326c1cf8b80cc7155152c9df33b130b15c433a397dcee04a53bf46064533c22acff505eb4e9f1e7cf88af94c76c13565342613ec54aab3bf23dde4069bc7d10a141dc4ca4048d4d5e59999767ecba4f256bcec993c3df10453e08eb8a2eb25fa81fb7bcecdfc12f6b8e8b7ddf757685649bfed7a628fd30aa33f600f6709c967d3d3a381dacb0a583f1111aa5231a4d2e9030b53d3d8a033e6c5659a54fbb7f25ebdeb7b1f71e278263403e59615e8a76a9239dcf445aba6467ce9813c94b8907123b17daf0286097d5e9a1a4b1c8303f4b1651b78b909ed36752af4b696078b2c2b1619002d7038790aa213ca3c38d496a9b96b5d549dfe3017c2c625f8013d1dec28b8198ad645c83f48602c7ca5f1bea05d55b2da6314606" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7830f933909bf94137416a6d7d9adf08dbaa52a76bb3a83b94a8517a13e1cf59", + "excess_sig": { + "public_nonce": "88fac490335e448d27b4d4f8f09e6a97dc9a53c38d5d371fc3b36a52292f8d6c", + "signature": "0d9a8776f5b479f22b76e6ba78d3ea5222103b31a4dac2e6d91498dded72e907" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7aac7ff8d0eb039c2827c48a1e054d95880be25e1a714c3daa197140070cce03", + "excess_sig": { + "public_nonce": "202e72faca784ce8cf16a284c8e9795ce482e5772ede7de53e6ebb0ef26ec71f", + "signature": "3fff83fc8995d7ba276d1f3e9d1b83c2c847de64b4edb844b334298af0904f0e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "9af1419d1150a419fbc9185fe6d8b67e3ed7aca375ead9edccf8b0d18b9cba74", + "excess_sig": { + "public_nonce": "f66b133f8eed065f382d495089c9c015b7c1f7ccc3db083d114af1b7e7f3417c", + "signature": "a8d737e8eaef8435a92c8cce98f7889733c2b2e2845e4fbe248ad3a7d30c1f04" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "d68edf40a23f299feecae75defb5666cefa7316a6270da453b3a328119364850", + "excess_sig": { + "public_nonce": "285675614ea6dd211f65396e29f04f12db6ae3e1a2ac985bf979cd6c39a14771", + "signature": "c46893e152180b37e48489a3556dc59b37d6ef2eb009274cb7120c472355e709" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "de62e93506f42417c0df9a92ded57d6f9d0e98af42b2b72d35cbb4cd4a8e9a50", + "excess_sig": { + "public_nonce": "d831d2a530c0156cc75ef58412cd12fb1877eee23f25ab408572f233275b4159", + "signature": "8e96f840eba6b544092a61b2f819cac6d8366005214f05cb238e80f3f4e05904" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "ccc964ce5d9635c0046262e9d5639f7402f64469a07d5165139085e8c5089673", + "excess_sig": { + "public_nonce": "08070a9ec7cebaf9d1645141a55055215454f8779eabba754ed706bd2a417512", + "signature": "31a2d27d4570fd67c3144aa8ae9f80f1f84ae456c2970cdec1a1682ae5270c0d" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 18, + "prev_hash": "b4750871c973ffd34556da68cbc8c905f304b307f3d193fa862ac44de861b126", + "timestamp": "2000-01-01T01:19:01Z", + "output_mr": "09fc8894d15c239322913a45e0765d366c3627d797388543c601f6be5c6620bf", + "range_proof_mr": "791fbde81c4d77ea6b8d4ac34c0915c00f0c03a45081a01b38e40af0d1c65241", + "kernel_mr": "30c8ddb59391c11e5937ac7b07e95c5ba4d054e071c0c7baf6650d48902d6975", + "total_kernel_offset": "b58028119de5c0b6189453482204586947f564a9679083c03d484883b79f0807", + "pow": { + "work": 18 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0a18c7c98fdadcccbffcbfd919f619e71c8a344f0b4b237d032433da47a4e86c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1043fbb90156152c8f56513ca230ca06150a19d6e7bf696855acca7ac8337248" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "12377053a059a00c08de8000654addc8aec8d880805350dd4af1c5a898d2a262" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4e98bff77dffabc8ffbe6347fa75ad37f17637c56ad6bc5f1c2b2ad678ab585f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f07589866df759234e481ae8c64d7428bfcfdfd34f3416b3345198bc52e5e70b" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0e35b288a15dcc18f2b3026ac1e922dcab3976c8f91e1b7096fb0107fe32b972", + "proof": "f25f1ca81a4594684ea07333c347cf250c466f6e3c6a8132e6a875faa65f416226d3544bbb4f5e967b9457824b1f2e01194914fb9669f9b82db90dadfda66657686e8e7f361b39f3e35f2df0491cbef396da6fb0426a0d7d2b613ed5309e7a00fa22a3efde273a58db6eb899d98a2018094d88e49e988e2df7fb8e59aa13687ecade3b436e0d05721786fa1460d5ed89aea718a7c57aff09f0a6ea7529ff5001b0cd7c0fcdda91588308baa27af1377b6c679d769098083728b8c92536e1a401ff657004a2c261f70d5ed91d4f57f498df1f75d3857b39654976b3d06e2d690902e4b0a7f7a4570ce9175793fe27048aba7e0bf979370e4a43b03bf4e877cc21400a7172419984056f2f781bf5c74d209788769b0bb267694da38929f1285a4b746074ccd840d1599c5eb8fc0d46c3a2df0ae6de66b18b3dde2ba5d3de1b4f27c41c8503fe7d6f524f254fcb44cf8f950cf7ceb7ba5f90b9277eee22d49c7703e6a68990134ba64c3b040473b426c982189592697ab125c77dba56643852ce474a035cec2f79ba6177b629768688ea34b338b733a8a150edd5228b6c8b6e5e0f1e63df37977772b393dfeec19a17f773381ec6a8f4bb1eb41a78c3bc309833695cff129cf413b17e54a48a1721afd5f9fc0929f9c4fdf34620f4d4dc65a7ec4f48757d52591c92f291503c5c2f1e4fa148c6fc3dd13074d08b1ab4eddc064e0ceabc7a89ea1b38bf75fda137be8504970944c4804fc25cbcb6030e1a00818011ae7b238e1a3a37e388bbea09a6b0f9cec641974d365a2063c69a375c7218944a1013e680463c364fe542f5cfc1d650553246b1ff4822c81975098cfa39b33051ba415823b13c29fb7da1a2c805075262018c995f912a03226403df0580129f0e03bb0da6529f927b05602cf188c42f7b965b0d20d5090c589382bcb21d0c0400" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "385acf45d71770f8bb73c097ee81cbf9798501fd119d71e09359b149473b823f", + "proof": "b4d72abc4526a7501a9805f1923a3655b8b3b3773b67ec153503b2097a98fb7ef87d820c1c6729b0a643f03bea0eccb55f8d24e1fab3e6507c6411967aa85c46982fce771cda9bdba14080a235a45f03982b52646bb1679d734c905b8a5ea67df84f716de1c78a1d8152446f2d36071a350aff67053647d05a041d7dc9985e39adf77e205ff0949e25f146d0b84ee8086cfa72659af37a09f769a80b61f9e10e74a782161fc7eb015513894147b9c7c441701e03c260508146cc9d4cf26ac901492963b56f67e237715c109093330f789abf87fdd0d692e31ad3820a8b2775028ae4fc099aaa0034fd0c3ef27bcfca2af87fe850b47a8a4c33c8d9fcb902e00d528f94089f56a406d3a6c7048beb1eb0e88cd45be1417c2864570687ad82d74bf280f88498f130498ed5d348c39d360abc62a69b88aa6129617e27ca0cae5e6f8c86a35992441701315f5b2486201fd81ae35c9a85c06fbaf4f24f28cb11fa7988c5b96d1bf52671571c3b202450986b4c5702912b295c12570c5a12985d5d315e5a79eb5365cbc4933e852d26cc691d01f8158aec88f0e07da81627f577a823f60fd95de93b0efd84f9f0603d483bf70657fb9cb0d19940bafbe8abf52dc12612bdc819ad5222966f0564e0e6092ed88c690ed156dbd3e1cd905f40a83a6e35cc93ecf89bc6deea2041cdd737c543a4805a2341f42ae3a6bc0d54042927bd5b480974a03263cd1a810347586d5ac1364564e99aff45024c5507fdbf1c05dc3dd40a316b7840357114d4e683505b7a37f6115bbb7cf396df325e2ecc80c9a96f8859cc07c9dbb6ae5a06e883fa52fa03941e824261fac461ee55ba5ad953ed17f7aad2855257e3519f567b79cf7bcac652cf20060682c25f0890e3404a120707e3ab445952f6b80baef3e80170918444c783941bdfa91d04fa00a00972fdb300" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3e0fda1c1f5b3cd254f3c48d414c57ae42362ffc902a79a32f6eebfdb0882256", + "proof": "68bae9fa512be5bb4991c5e1ee2afd8e9e23dd0714142e1e9226feae6acb6f5e62cfc5a91ec9cde05cf916ee9a8d68f6ef129680cb0f2737e1e5ffb2ed49386d767532654a994c597c03cc0607e4420a33cfd41b317fe96bb47a644acb19162aeab01d576e20cb2c0547469b9eb0402303fc2f6fd7b7fe6e522b0c56b164823f99ea6939ba2dc11825f6b68e40111c8c79345727747763ab6da52915c20de204521b129d5c94bb3115d0aa52b9d050abb3a0ba902b589103a2c133a24c64c50e55c4f5e8e0efce2e2caa0544757d84e74b26324623761fe908c416980d73810880a629fa0435f6a5f532e0fa8abd92dce7619dae3edc907fc9753204f49e5d7aa263319b68e05e655bc11dd89fdaed1acc49c385ae9e4ffbd6ea4a28d5ac2f5cb8493bda3fc959da74f637e05df27fdc3d1ca1a32b979eacba14a47a8b1de238de59ea83a805132c514c9059e9bd56c3ff3f79d524f766c45d6d270fbe5188035ea63fc87b0ba1b2da56efd41f777331d481275d4d3701e5da86712528881a087e00851686766095a1368f32da6708ff6c11fd54f1f92887678749790da22c0cfc09bdd7bdb5a8e56da9e8ee32c31e9f633d4ce0883d58ad8731b66440c09e4d0e3eb871ac66cb91977a5f1ca788b609322a925a9aae53de2c418fd688a8ef7f6cadeb26a1a4f476e0d0544b3d766febcf78a956469a6a0a13b3ebce39442359669f143c316f09535a1bda5ce2938d6cfffd92f33fcdb663e78ac55b98816204e86d4a3629924f7252a59c24c729c8d0f6899a2dd0e037e7035fde66f2bbcd7d7c087e3272c8da1a78d0586d943eca3ed5228ca695e772447ab01aa48e2c177f9be3ba0209cb47762e353e4b3bd0376feb58ac88dace0ffa1c1c7459fe3e4b04ed3d33191b44a41d2caeef39471d110173ccb8a16ca85a8e3f1d3a79a4436002" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "824df6a2ee378c11677fc31b92c2e25d195b1dfda9fb0cedc15bddbcb6358d5b", + "proof": "8efb770f2384620fc62dc0f92aeac2ef47740f1c80cc263b188d65d3207b772bc000b3017f0519210d16e8fb7590e90f740f36b8db4592e429c953936c5c2c34c440370cfdc1ee7fbf45d1dc7d376e0e565d97e5a850abd83573a0cafdba1d77b2b055889c316e0efce554eea0c9ca52e5395192de44d4d787c7239a020e9c2f6d65345f1ccb32e9ab62d5fee88f23edf9e47b9bd057488447b68fe67875e6020878192369f5a32f9c582022af7e5e891f6912ab870d61effad6094f55a5d003aba753cb31cff775224c625158edfe287b03ea8300037e7015c46fcc49b23308e2b63f888a9ce8ad292801650083b330e6106546620f4897d7c5de127ef24660843c1f69482b25993a68fd9533eb60759e0dacce9c162a71b048063e7d1a5754d6f8900452bb5e8b009a3ded130e1dfe4abf7711d4933ee50daba91d120ad534e8be320d950e309ba7a961a1f89f6ec3be315947bfcde88b00b88549f994001e0aff4a1bfc446f56c2a18b0c7a9d64fd6d5d1c1c81b194e8a2110bc46bfc1a4a4292a7c713de704796c113df936958b56e26b51099907b9861b36e09474124518035e8493ccc16e36d67e823deacaa10228c1e7a8ebe2fe2aff2be266456582b66f8f3a94d73c827b2102c33f781e6f9125858f56d5ab29b4275a6500c5606375c0b27594ae08316bf143b077f5093a98da162571f54046de1171c74f8cfd462ae9534ba9d0001b52e56c3f35c7d74ad3c55ef1463564ee684ddfc4a51b3b224ec56b28eda15ea2f07eefcb884b6562cbf0c204de39b345b2cc8347032bc456cde62e275cd5d645294b11eb2d61f0aeb94c37e405d234b39acb2f441841dad351706c8ecb895cf4a6c99ba3269146d6ea423e9475627cd528e80df42b75d42035f7ce29b4d2e9471fa66c37ad91c1c3f8b1a347885d615a8596b51b03a8bc600" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8ab9aac580fd2a26ec0642328b2e83ad943f9a9baebf146639edf7b66ea06c54", + "proof": "069d3eafa6413a7660304b157b5b45704f70ed90593546bba228985bb626bf132edec0d63c76bb37a8acfcbbb88fb6d8d3f08bea3c1393613bb6dc49e555bb656efe3dc395b88a0ea8dd61819a3892eaae64a2d51901d41ef12680af302bec484616c078965f5592f312aa95ef3d8281c5cc7835386e4155fddcf32a168c634b80ce973874d910af35305d7ace11f65bcdaf6d951e7b8ed1fde8ecfd8cbf000c0544836d1aa791570a0f5df21ced0999322a3038f44095aca1dab6bf6e71430f186ccb882bd79911aba59e37503d0990ae32c00889e2abab8e1dae181989bc082a2d82ededf3f624c367fc12ffe0371cf1212c0155690df4317ad289822ff133ee15da555f58b677072157d0e638963e5e3624c68fb2deaceec63f8490db3d1c34b8ea52a445e7a9409068e869d66e44eed627b52f4a69a713fec0c70cfe5c221808437dba52f09b173476f79b0896d4f58e80b22ba848d1b2ecfb1f86a52c2980f8abab1c708684692f05cfbd0a3c69bc34bcd895ffa9d3c6bf63dfe590a678f8bebc5295adcc70d405f8f082b53de73add7de61f79fa767aadd683cb6fc6472e616eaf77cd7a949b6965f43cf4c20c4ad730fb567a578c787952f7dc4b4e03ec301518e5f2294b780ce6082de36026bdec7c9c709cd1ddfba7eafcd36f7737d6d7ae36eae20c0f6ebedf741893cc6a20af374b0cf34281b463fb1b8c6b4423ec461e5b1b6e14bd9f1c44d716cec56933d6f684099f7056fa24db7d08c8085f32004602f09e38444f8d4ba0c670c9dfbf1a8165011b96de289af8670b690e3ac455ca0458b270f2e2842b55f3e783fa5c377ebc2e47974edf69bb0e5993dd6869d295d89497c32dfe060c9a66a20f3a1f374cf8a6e84ad518466c71f158bb0a4656cc8e793722875f439856f7ce459512989b6ca435bfe5b8fba39c2fa82b00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9440f3f8cecfd82703d08ec39c95b9503797d07465233e156815c5707592681b", + "proof": "0e68d91c4cd5b9ceda79e5e71baf85d2d9d7c7ec491ec8f5b84f21d8392f1f6900f17f3bae44d11b1dfcef6f4a2afa5cf02e99ead7539bfd5931b5f1522d5c5272c7acdc707a026e82edecf3ca32b7a101ab3f372b8997a1ced918e75614cf4f042aea1520aae0b48ce6f045708df364b19779f1afdc20b71be033060b49d3210bd802632876021b367ecefd52dc69f911308c0c1176451a482d5d4fe79576069af442e6354e57f143cb65da5207213fb060101d65f8cf84667723158f3ed0028e05ea9a1ec7f66ea949582fa2b2e21c280e7b59ea2bb2b25d2f995b63a41d0604d6af164e655e295d36710c8ea7a30eca6727b84725c1dc58c8abb781ec1c7a00c47f12827d826930ee8dba414d474c6fb65d992657b2a610cd074be3d97f158c2fbd064fede2103cce6255bf3cee7eb05208908f633bfd9dd8f5fc204c987cc29dc94068fd3974f170b5d44977839bceb73bfd1d48219feb7c0b61817e797ee274b143edf77b0acb1f363b9dc1ee1ab01ca5d986509a91a121574e8b594b7e463075f65f346c26112531a8cf58967316a4143e9a70f1910ae7a6c1e6f0ac2e0e29eba9a6fa2393376f94e382fdaa192ccac0edee0d861f028007b4c08d2026760273020d6991d93faea06f02c358de4dbd29bb201707d474ce9c9f28250c0ad8f1d09e9ef9dc0248fadd8bf5364794b20973aeeb286f01cc8f225f783b4e50c89f6b72927da908593b8e5da3529fbda7bbb5c35308306f3fbc453627a70a337293b19809c2e1352843847115db5ce8a73240335dd0112d829408392c41b64028aca93e84c3fe87e787daa0bd16dcf79ba15e537e3f2b88fbc03f388668ca4b2e0ed667315514a6e48ed1b1a9eb4571163340945ba5f4edd435391f3248a90076705d9e34c2f601bbd3bda2d54ae30f0c9afdf207b8d21884d7bcd1fc599f0a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bccc2c14e510159a5ff8f58f9e33fb35cbe6e3501ad69abc5aef7386334c5054", + "proof": "64b7b78c4ef4029a81ce43e62e8da1ffbbd8947990b36cb2b375cc1982179e76404c748708f2bfe54b1f1d164087e26426496cea9703fb0c847e2fdc9f22fc75b25fdb99791ec706487d3b6801d3ccd6649f17aa88285f50be91be290bd98832b6f47a720676aba156656b709d4b472c7fbd593858a5f178e228dd7aae6c1b35a0692d20fab62330c58b02d625d93280c596a366bdb5e03814f34cea0cb62f0a7be0683ad179ef732eda74c64956875eed9d20f10839d5790cb2038614cd2108abc0a91347111b47a30b88b689f89d4ed5c254d449eb0ee7b90b59ac9929780cf840349d1d181f6f716c7a8549032361db66c7e93c28528dcf89275cf88fca455494c5dd5b253bd8af74dbe3a022bd420764bb3054852042e2bb2b4fefcccf383e4c9a159a4660b3e1affd5f142b1ad6a65857c8753325d737ab4af20ce8884240903238527284f3e7460cdacc6c4f923237c49646c92b5228ddf1f862331203f04d6d8c99a2179c5caa35aa164f19d035d56e5c82e88a14ae6f100640ad6702c6bd281079069e853eba45ca836dde3bf733130bcb448f6b522fb9a22052f85e36daedd9cc4d1ce1622596923c305cab0d0e9ba29f924221dd500698e11872236659053f9b28306737b573ab6f0a278b97dab639ea730f4226cff74524f08a2d6800eecf2cea6f137d4fdd1892aec5835313f4c12a29a34e42791dcd3a030579621e1a9121d46529e05f4bc0c28c322d67b619af445f345695dcc47cb2c59a191424d73e08efaf8a07ba5d1256ab26b3c2b630b4cae8bb1d191b04d355a6433b5e4c205d8a1bd79605f8dfecec19d54961db8562310eb9ee48388fd357884b327d15e4b43c84e32a97196fbdb4645694d3b0643eda49b6c7787e1c3384a20903bc6108d272b1e3f10f07cb9dccca5a0987627640bc9eb5c8079aad288c3b0c03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e28243f2f7f203098d6bf480ebf1fee4bfa6fb5a5e7d7bdecd0618a0a48ef21f", + "proof": "9a67f8c37d3a09b6f371ad53c57232d72ef5d086405751b0e84194e31cb34d4b18db4c8276bb526115404399e8bee1b0f8258ecedd63dab2838fc04b329aa935685e3929b1e9ef4e9c3915f41ff135992b083ce0f1ec9053506d105b156aef7e6adb70119100ec388e46527017f45637fcf376a11cce228daf765e8c28a2612f481fa6978e080d466bd84b4a32bea8232bd53fa9a1944cb4bfb6b5c774e6c10df4cd94f0b283c529522ec15c783f03d3035616a88188532a8034eda7475eb80b75222714c714f45f2eaff6a368b8d4bb0b11f0dea7bb0bf9cdb37e6acd04a3049470f8a25d839d1105c5a402c8fd9db47f6ed204878580f7aaff06249f2da64390985d1bc3fa8066966f46bc2457ac4d694d9def8b7ece48305bae192fb8696280bb7e781e5a7e1779c0fb987a16171573cbbdbbd1747bfa131e842a7eb588370ed62daff6a5548b75d1647cdca3b38747e8b59a94c160afa68744203cc85b0f0a3ae6abc7e8ee3fd4519ad2187837186ccf05c4ac9c6b08237e2c98badb8a7f5616fbb738b63a1965bb68e937544387abb7fdd7c2eabfad1f79b817d29e85433258e45f6823545ffe6f3d61e09f7621366d0459e9e808fd4346b029239f816f40c995f2ae77a86d954d41a1b75982f06d41179afa93cc2fac2b9939d418464f2e800d252e23b0e839a21f7c98792cda6ad579d746e9ae35453904796637305c7cebf70fc762f15f1fe3d7691cb3ba2f024e541dd446e0a007450fc0238f9835ccbe2874489fcb7c282cd0a13b22f2fc92498439a40fa014376d2f70fa08883396899149653c425181fa60e3f6b6835b993499e348c2197d9f68ecaf8cd622765e5ae379ad78b48010772de5906c516ea68779d888834c0fbbc829323f537808c02493a689a2313ed6b8750697c6fc98a9947ff9c55cce3744e424b109e1050f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f22797b6b285f479fb620499d87265d4ef80283eb3ae719eebfca59bf697ac3a", + "proof": "da8110198f98ee80943f7176335ad4fafe3824f321f0053bf56a3598a92a6b468033ea766f44b32ff938c51039c5fd47610c9b927c51b4377e60a54321b55d13d6754cfde6a541aa25becfbb09c9910cebad3d23d55eb49e953592a5b72682455483f952511393a100d1190a99269f51011178ed3488228ebcf408646e88114e6bef3d44bdbb07c97ad1b12948e2834d109b85cfc56647706fac9b4d758a27035b719f139519d5c7a6bf57548ebd53170b5a55822af6f8492ef8c4d6aa3659018d701ed7159999cb955985543c9e4565ff90bc8fa0b02410d65ea449a9883200ac7f26904aaa498cb94ae217ef30887013916a1d4fb5cecfddfce48cc42a047d8ae77705cf7ed20bf03e498b41b94cfc26a8f12932de58eaac3ace5c0c67084c1a13c2eb3f5c9960d3d029c7b63db580a9e76d7b936e514f62aa02d0f889560f282d7f836849921a5fa595b7e013aaa344677c2b5a975e859ff31586a658d468b84014af950299076b762f63064b3090fdf2d1576130b1c5978671062cb8487f6214af303161b91ebb5cca309d6d1183be3afbfec759f9afdd81eb55b7d5e05fa2049edc055c5fabdc0edd358df56bb1cc8614b82635472d347fad5aa48af919885f9c4784e814f42687a317c3af3c959778b7e76d4092cff6856f40f725fa41f4f0bcee7ffef82c2314da258b37841a2c98ba331f225ee3cc022e8c95573c65aa5db471892f7e3e3494deb9b7b92a4acbc1254275b21fc2055df99f2c532d3498917b52507396097d860d7ac7b714610fcaa1b337b92418b52b74ede43c7a2d34495b20eb21bbea3ec5a7d3660da88d907ca56a9cba69942fb01762b1c58c2180e06d414806b7b9b86215ff0ec47bdb482966c76f1cc829f3432fb233e15a06010be55afbae24d4b464596ea96b68b03cc260b45d7d92441a46aa1854444703" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fa4335acdab316e258302b3ad5e63edf24db6d365b7e02a9f1422e6f1b474328", + "proof": "fe83ea62b687a70876198b901ca294c6910b33c355b8605a040432832503d60ece0089b505c3afe7c452a399131e092616cce68bcf901c34b6d4969b9f28cb193c0c096130b98dc37fcca328b820a0a014cb2ae6f0eac9d068939fa52228243cd06ce31597410300e787481bd23ad2003b7013893a4c51127a0011c86aa31a59992988d65dedf63cddfe0238d666634b1c44385029f6fa16c79e1a1653d511049e89d67f4e12065f6e446c5b0cdeb7d9dbd5275327e252cfd39b310e15b25a070e832e15e36e485a8cd96cd5b83c561224ec29e71bd619660b3921cde6f68f08206133ffb8b8d31ca8b9ebcb5c1980dff4f82a9eb859a7c67b20b75b5e152c2f3e5b92c19fc601bc66887b5ce136c8a5155d0b65a8ef76234240f4bfbecc77370606040020475a0275b3391912c247540633f22a31a08696e3bcfa9962c3fb4820f320a065f384a8267e716ff622dbdbf165a9c170618a924f18192b51dd5434e8159609c74b6f21aea24925381c280ad8049841cd097ccaa1fcaf45ab27ad1e02efee8a810a725e81758bba78f07e356ea0d8100aed5b1f770f2d6e2b423c62c218ad7a0710d5295aece365ccdcb9da23115c193ef8607accf84f23f9df4577a8ab7cab06dca657c3c12f437bb62de967d6e015e564b73a7107462cdcab2d1dfecff231c3793a8a47690ccf1b7ec2acd37884273b144c857619a87599f4a912101153a96222e92a3969a49b5d379fdcc8a6a83e0b5aa034bf961cd00034060f8a2603031b23732f8766b4eb53a4eb5f57cdb5ce23464e81241086307404e40db47d07a7be182680a4596053bbe5f362d6e45be402ed7b5d0704759fb0ef3707875d02c46b7241ac4279a3882be22c25ed1ed1d758772374f0856907754b7d05d01ad979cd87d29964f69c046733aac1c4c901dac2506dcd8c54766b17424e02" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 19 + }, + "commitment": "68b067c55997156efef9d3f223669d5a4e922511f261b41abf4e3f99d9a0322c", + "proof": "840da093c0ce2dd0e02a54aa71759da9addf3f8b8fa8ced1b8eaa6f2ff5c192784cfc7738a6c3a8bd826b4d16fec1ac410eb50da06c743dbee3d37f79b614108bc6abe8b7d5890d8447faea05af2247f34ce1fd2144e28cba8952afb9beb120a36fed527e8dab00cfdf18031e1a7fa4d48f0ac2891e65459d6be29a36eb3e77a0257362cdfcd704e7e188611d7fb0bdf97ae4858240334cf57e43a7700a08f0c7b7f1f41af22849071e9f91a422e3e06010fa0db9343ddf712fd3185a9e1c6019142eacec33e01ba9669f794996c743f36e76ce19d9235ca719817d1845541053cd01c8acf57f888eb562959239a11949ed406d3323341da0bc4383b61735829d2ec14d66f73ffd2785b68681615db7c469b66576cde298cc46ff05b0f68744c9a5e26cf9df7c9ef5d0c6d6d7692fedfd7ef1b5e3b2126d8eb8e076a095a9a6556a629f736f3cd1565d7c4b2faf3dfab89c9ef2c66fc8a83101137d88166373f381b664324df0c69c5de4ff8e66de92a6cb7abf10e2fb60fe8d186ecd900352a2ab1bc4f7d27fcbe14937926dc9c859379fbdba63601385a234d10ff8ec60e3e2c6c205d8e83bc06f33877f4811611fdf5210e79b37e3d97b4518be44c4866058a765c9809b66fc1d42bf710a8d380612c71757faf40e8e778e6170cb00b6e56263729feb0d6fd711b026f078f4f181e8b82ad39dbc878a91e27abff0d5f8d48ca3522d3d49f0e9dec9e1c208a83759df7f4baf3c6f974c364f3b45332ce55358413b7e05cdbed4e24e7c916cf378a9add3aeb7486251d56ef1d3798798248071e3518f8b3fde81eb0fa88559e2e7c7c15162e7f32fa342079e8fae642f8911a9d4c9090d132fefe48174b89c667cd5182f65f2960413c094e9a2c2cfec0f701d6b14f162542676dc2b59a8d0658751f7cbe8f1a635e2abf1ecb745d5263a006" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "52835c37239a22b3edf6d46dccffe993d1fe2946e04494edc0014786346beb7a", + "excess_sig": { + "public_nonce": "76b238a1b0068c74c0181e1e6efbdf3c9a45f0db00934d01df28671864a66122", + "signature": "6fd44de6e9e87ec9ed8fcb4652d6d416059272a6d44dd42b5a1caabf6e7daa0d" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "5a85574c538df9890fa1a6985bed07ccefce6b5a7adf37124dd95fd393277931", + "excess_sig": { + "public_nonce": "18b6513421f28a45438dab33362c227a2418670cb3b0af87eabc8deed2c6df07", + "signature": "d86f3e18f265e943879fcd03525b3d63c134a8eb286ac3d421a8c772a713b608" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "681b20d0920f8876e11f20a9cbe0099b057ad30db30c4ccf2c241ba727525477", + "excess_sig": { + "public_nonce": "ce7ad9ec8afb23d3a31e373cb2c398c5910b7cc3e61ff5c1f33eda92dc138a6e", + "signature": "2769db482fa087c2e8c38e92af57b4ae0844d1dc5cd922a61acaf33b3c56ce06" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "e2acba5bc573b519ff3b614651780b4ba66b2e0a82f9e311afc09143866d0f05", + "excess_sig": { + "public_nonce": "58321214cfce151e24354557ff974e8a30785c998012c4aa45212cd748b1d159", + "signature": "b3c357ffd73a43a958bf38882a73b25876658425e4f505fe36c505c7a50f9202" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "e65c1e284a72d8372614bfd5e49061d587aa44c025f2fdb3424e321c8c81ce48", + "excess_sig": { + "public_nonce": "aa88d37fa65e879682a7ef36ec9128313cc5ed629aa8db8685382cc96289c501", + "signature": "177f18779a90973fee6021f2386180b1f95514f6998c28e126268ada86bac703" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "d85d5fac0f6ee005126b081c57046a084da8213b4055c89a9da591ec40424306", + "excess_sig": { + "public_nonce": "18eaa70ca04a9d60ba1c91776800feaed6125e3abc912d14138ab01db3fb4b49", + "signature": "39cdf6d71f6e19092cdac22283ba43a31c6ab3eb8db56a5689d3dae7987cfb03" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 19, + "prev_hash": "fd793451fbd14b11aca5d3745519018d086a3ca7913fed12368212b4ba8f5f4e", + "timestamp": "2000-01-01T01:20:01Z", + "output_mr": "7b82d02a76b54f06a6f45fda0045d34c5962ded69c68510f039ec21cafb98bfd", + "range_proof_mr": "767a32d7e7dc14d2fdf66208fe13d103d3137e436f1f4726cb60cd16880e2aa6", + "kernel_mr": "9c32fe4819f62108370bb80e343897ff640b3686628601e357600115c9ce5f5f", + "total_kernel_offset": "b0e761038061ec9ae5e5483890c76133fbf7caba361213f0223fb4932c0c5801", + "pow": { + "work": 19 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3e0fda1c1f5b3cd254f3c48d414c57ae42362ffc902a79a32f6eebfdb0882256" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bccc2c14e510159a5ff8f58f9e33fb35cbe6e3501ad69abc5aef7386334c5054" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e28243f2f7f203098d6bf480ebf1fee4bfa6fb5a5e7d7bdecd0618a0a48ef21f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f22797b6b285f479fb620499d87265d4ef80283eb3ae719eebfca59bf697ac3a" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 10 + }, + "commitment": "9ef8501388da66549aff0bb1cae35fe8a4ecfc3c79595726fe96dc21017e6a75" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "06d1f7c3717b89e4f1492afe682aa5363d2d65bd566b467a99822312dcf13a5b", + "proof": "72cd4f546cb5f06b8b3073d22e4c985e666e099df3478bcbe64f06b46ae098158c782aaecb6c90c4dc58fcd7931678529528fea9b9230bb26f7b45f8ea7e580d5430336cada4b67e79b39c6b6caad9d31e8394cf0f68912cb4be2753abd88c631aa8e7e36c533e7a49b838955d2218c5681b809fa96f4439c85e132c8ac62167c204a684f8ab70d0e26600663fd3cb8948cac967ad5117b10df6cb46213d04069898317f6f468bae9a2befc836e64afbec8d1232c6b44ffd740998d7dbfb4e0923906b979ca567c10d03ce80274138a200d18d098fdf654d9b168bf638c1c9005ec2000cb9966e2c756a9323aef77f034c7b775a76064c3c6651c19499c7490acad0bafe9d37741af7f3703aca63d3d05d5ae18a289d096c3f3f28308d2ae646c29891e7527e05223f9ed0ffdb340f18843eef066db5a79590ebf9eda114a70c7eb23965b2006474d15164267e34cf45b092b0fceaa026f9baa2bbae65285250e0ad407005aeb7a3077548aa15387e927e187fce172d740867f83c192b5f3e102ce9d58e1ee76a00a302db7eb3fcc36a84ffc7069114405c575f9e3ae96f7415f4d7ee2a0af518c639a1873469c14af6474d70372ae7e16debcd24e2fad5e45638fb1165128e0c28223372a73bf7515f1a5f938c495f70034515e6b0a9da4c6c5a337a541f5779d7bf9dd6f087ece8f99d4c1e8f1a1c56269f366a922b621922da4815c30f3e4cfd455efe4d87cfd47d63170d7da9206bb296ef95e54f9e802dfc9ca65acb37a65ac41ca75cc5b407515cf23c3fc1dbcd462f6ed2e73883df67047180ede3d43f2417679b8b588aaf5f2948f81202f33b2bbf5ce9e02259f16d6d2b249f579b37896eb18bc5a545d19e5879f85acfc8b672a6365af7828383087fb1f8d7d0ea1436b367f59633dd30dfa08b6e9f1496797ddd95ed030d37a103" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "08c699e1369b582e67b00208131fd8d557cef52561dd6de858f076334722ad14", + "proof": "023e002d813ae728d8b85554e0c0062ccda490b349c732e3aaa354670c84fb2ffe886534f85f75de1da5ee99a1c8c02729eb04a7faf9ac3a7b5fdc75b744ad3eb4ebe8b28eb217bff450ec0d02e9ae9430a981fd1cc6435dc40e28d0c12c81554a6d491d03c3f980ad5a4064860e1be1f61a92e047045317cfc6c5b5a372a800e3cdb630d0c031713b8b0bc70f1c40ec477d75a1038d33893d17312393541308e47c09b8d14b74ec280106c7e7bd68533354ab72752c163b2952a7d02da05909d3ab1b2f5c0a485b3e6829f12c69d294d0bb9b85d7d568110985c0dbb891d60bd4fe44a82b6d4b5f4d0566771fd5529eec654bd401e43a7a4cb8817cbb690a32eac3d292c542e30803bed44d9c51f6a24fe1fb2431dbd2af3622a11e48be367b14e6d826e2e2eaf115595f4dbaf0dd33483ec004a6c189fc490ffbb012b40d6d18c24a5c715720baf943621f5e779eac29f31fa02bd364b17d6af40c3d48561936aa36f6acd0a7feb65737ccfe26bd4b116cd29a59fef7511113366b7ed6e63c508449650c7854c61283b9dca2a30c79ae3868d1741ff208cc7c77a10e07073ff8f483ceddbedf175657f17eb9f5ba9cd3db7e8079bfa50823d5f005760b4a49e2d6029d052c6ad35a0c1571a7f2945fa73a37edd609595c65467738b48cc7065a75245e3e77a885f8d40c57ee278ad8c015efcf3fb0ecbf06798c4c0adef437f631af667cc8e277bd405af4a05b761c189050ece726e52aa809560960ea405b963695a051e95cc2f6ca99b2574e1a17d40ae52bfb37b3bc62bfcbe5f05c762594c1a33542f266b28715efe9b6ea3f712cc5aebcf99cb48fc5b528ee6d24a22aab36140b7fcdfb5a0090256253d6780dd36a11d129dca2aa3c4b4a1d7c5e9701cbb12b044d64d3b7a006729a2f2e5357fb2472fcd77c065e765973ab4427070f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0cefa777eed5c35973bcf0ad5aa7bb66c72e8bc324389b6f930c22bcc061ac0a", + "proof": "2e9eb026d5410c1fe52b357f12dcd7783d8f08c5647ab6c8a8ceaa730c948823e20da4714117acaa9b3dc400b0eb84671b006d61ee8a2277d3e76b11f9772935b828607c1bab80645ce5d626f01854191e475a80973ec667d2bb89ff29751905366fd11f686b9657ebbf3a122928a12f46f75d33eb8f384873f5358b34e7940267544e425216722cc3d566533d91c8b14d2abcaf3d1913c5fd2507e91e1c8a0db5baf32001a0ac02b205dc4e6a8325267eaa6049545cfd3b9aab4c92af1ab502c5a8cbce0694b119df58addb1299704f02850370845c044d91ce09a762de9307a6fc821dfd973e989b3e8d61902855344fb3102a9f13e879d3d4795f6c82655fc04c298d20d6de0c0c3a849dacbaca32f7bb366888dc493af7dfaa0f1c81eb6efc46c283345f2dfe3fe7f98ad5d660dd087bb6ff18068dffdc691bb2b70fab17129d787869e2d46d3ab6d073adf38e6fd7c61a62de75b59fb645d5d95ff5796a865d4456e5b6f8333cfd3e0c040f43d1fc11cf0599f70f4fa079cdfc59e0935ab049299cad0aa1993f4066e4af9868d012d0ce6b0d17d9af697d44696834395ee0d9015ab0c28f8dd5cea988296e1ae48f472164819c481951b8d8592609e655962fdb01b0fbe6123d2218c18917dbbf60ba564d665bcbf00036ae2c72a1465c0ad9026bd145a4f592c61a4fe46686a9deb405f1bfb35bba472a4c8b6b66142038b12930cbc53106b0ad424cba4b73e09edcfdf45c8dfcbc2521e3918104572a46a449ff400111eca4f0f4584d387862a045bd7a4baed2009042574c1d89ab78f86d1ad55e0a24321624eaa5f8ade987d9d8adfd768570b1d4160a873c74406b91788188e6c238d43c37974fd1a47a7313168c49b0ec29059bdddd89a6529209e7435ffd6cc2ceaec9c67927100f6df0cb9983005618af414e5f57120ff86208" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1a923c45c02f1d27784da4683bd476574ac6ef77110e8cfdb508fed9f69bef55", + "proof": "16eb14ff7b163d52fefedace9fa727610c9cee6261be65c3cfc5a2e13c09e5467af8aae91f04efa5c728b2a96c22070f5b155574a20a0761c47f7061f5c1fd687850a3e34755cae5cd3a0d8dae1107abbe17f03bbdcbe622d67ad2af24be542cdc3a76dc04c1e9aff9aaf09f2028c835e8f43828b3f0493ed311f5a26c5d05690d7c34d7c51eeb35062d26890d54eb3aa607c90bdaae5acf86e7b7fb67268d075501eca05001796c910230ac0cd917a6ee8a3b0280d8aaa83a7f133fec464704caa44b29a75437e6c9a393a14845187febc5e4920c5e14924b168f800bf5950c926c27ae1a8e51ef485d06267caf69747e0e99abb479f6214736753618b31146405a0735e5943c2df940d428e54610a6002f00512ce193cd0026c0055bc4322318865a3845d76f38c3eb724f6015de48b76b3aa00fd57b2b5aab9c06a03edb7b0e637eb1ceed6b64b95943a46bb0f3a12bed33108f2658845548651a54aac4088a657160e655704f1067927d218b67336de22a1970fee8098f248a1c6db2c51fe08e249142c6cfd4e478b289aedf73ae4cf5b7dc65a000cb776dfcc145b0ce644e0c1893515512c30ab5deca3eb9d335abbfd9fb3fa45438184fe457c1279c2ea6627a9c0b111f9d6cced100f881561d2cb315d99fe83de0cd4b7286b3b26776b8220077f3580bc17208483eaf5e6bde9a3c5508030b21b278cb867da9859d57301147e6ffdda6a5a61fb2077a13a55fa2e3efc32a46c33a14378dca40101c1adc28bdede3bc56d370ba178da0b3c9b6693af9acebb70d03fb82be938f2e803aa23cf46981391ac9fb738a597e29d861249a7b81adcb4b736955abc25c0ffb2df5902c22cf5ff3e9bada909a1bbc33470e480c38a20b27661b2ab4cd35265e0fc3f970a878f3572c83a007eddb3cba43d3c6ddd24c907aba0d1e4ca6649d3205" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "385e7f90a3b257d2d6718aa4d8da5be7d25ec0422884ddcc7c1f2fb8110d8166", + "proof": "1cf508629a8a765e39e66353eaaec1ae9aa4accdaf5288706f75589f9279eb26709fd09f497cb3f29b6ffd8aa7d6ffee17fb3a37f83f4950375511dc7a38fa5eaaebe8a09acd6b7ee74ec22666bfabe781d1cdedadc943d35ed2a5d17d96bc1a28624e00a30b0614f12063c7efd44d89e1eb28d45113f82d02ce25125f1100744dba89823da2a75f05758514fdfc6334f72ce38463e4e04ba85f412f858f3d087f09750611abae94ac9ddcfee2d5180a9135e7b64bf3003de836c3328beff706efb00389a90e2e311799924277df88160448345f9cea9a1b2d53816a18ec5607fce9e9ddcf6b32b44e73a1cc9ed5e6e436e1036af235d2de1269f38986e8d50bdce019cf81e67aa2e091872dd3898e0f0320d598eaf8d35038530784071c392f3ed7f13e62edc09f14fb0b679f2f4d1c4caf144506970d9f6796467fc77419489a58d14e5dd959b7e3ffc365996ace53e23186bc0b1f7c414115b84570e4573bfaae078264452b5be902d51a26df994ce8b70f09a74ee54758404311edd9db7daeb7fd9d944ce1f3a07b6cb310170e5b2fca88f8620e766d2227fcf13fc38f39f4ceedb47f65bdaebb26eed0a07af712622436503b1fd66b7e8a3ca1a81abf7ae48475effc83396189ec636e6d2c5fb7aef30d9b986062e07f7bd8a68063a66a36adc1f713fd43dc82bc8b9394904f1793787d03b2f543f99c78b391496cab563c968ff78d8058441db3001563da8e716a1228de511a029cc2cf241580860f70e8e7f60cb23f502c95497c820b076f2474e17238a9bc9528ad418baebc5c705024c16a5fde1e649ae0ff8e072ec39f4f2a3e71679a4a2986afa65b305ddc4f1f99282404acd94f32052ff124b16783aefb287af1e43bc53c61e89b139d53b90cb15f6c48d64cb345f1f6a86e549c56ae4ee84ddf226f60a47258d2561944c906" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8aa4ceac310a89becae665c96fcebb77cc8648abc013e1aef0e1483b4a37d342", + "proof": "94635dca1729ff6b9cf6fe7706458daaf8e09aff9516f9604e17cb2f9054cc46347fb8b22a3cbb0f214c627abaa07d7a9433a66668478c3b79bd7b4c4e53001dee06004059dfc2a8c210e3893f5ea5960bdcd0450bc2e5251a0c5187b33afe2648b5acc5c36d37c58dac9fd7df2ce94127f0b5af4498e15adfb37053d5dace7336a122813f2bf9b1e92c3245aac573fb1814a562ae95bb2f526ebf9f2270fd08e60547aa7416e4d63c7959ad0e29e301c01d7816049fe7e9119c3adcad99e10e68cb3a95f1d674d888f24ca6594779a456ee0dd5f65ed835106d9e583f0d8902a84cd8845a5d94f8fa5dc0c0d337db74a7d03bcdf966fb2bacb846c9035b776a2806f07670f4bba56b5331721bb038e142b46280c75d45dc040ebafcb8a27a3d508291d9c65198cb96a115f6a27332b27c04767cabaefbc5dfeb6364d5fe4321eac35ff11f8acb07b96fcd750f5a51bd69879f22134f36d9f76f4557a4045642cc51cdf2ced309ac5578da5c735df8d63bf103032468ce23212f89f5a3de5563920df877afd1aee1e1bd83cb14a5c03f503e8be10515f86dfedc3705cbff79688050d366e55389f25f154969d5edb1aca885c220ce47c67c05ea983cd7aa08590063cdfe1c47c37dde11db7ddcf5298333704619e6164df8336dfdeed861687a401d52e77f98d5c4965effbe715cf4fb55cda7472ae06b6badaf496787bc601fe4a701a8e0a6c710cda47d090facfc1001d8486d8134b2130491eb6562e9e6320a9b152b3d65490fd0bab951e48e4f2cba96f8278fe02a69d4fd5e500406e9227a24d727c2b5fc9049c15b9df518a77e09e40be63693327700286ae981fbf25fc6677a44f6511d37daa7ab716c51f07134919aa8ffd2bf38438149abefdd5d074f2df86fc85de80f6dbccf8d20f3c44642a90d07d303b92e9a9603f22d8d750f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9465161c3bd0283710fb2c109343d5cca3ae02391f4d7a7dcce9b7838148ce7d", + "proof": "3aec88e6396cff6408ed5d7a265b7658dfcaafb0cb40f3e08d7de03e983a6d0a2eaf719377fe651f21ed5e0b14646c3f945aa09a863b2dab8178f97136067d774a87214bb085c392548ecd57327dfebbcec774c8a4f2222aa1a9a1db9ab0e834e0e8e896d56dfc2ace2f264f7db62ded821def3ae08a90e6f60c3435f4e3c338e562c343b8ea40e369a2ead70b8621f51be55b2a4439154a6c248612f46f760f4866833aacfbfa5e3179528c2acb0e57a1e7da1b52a80e0b6561a444aa77a2003538cd83db362007c50d4de7c23252abbab5d5db8e90cca3d215201f1f85200af01a81be9bfe0afef57275db31bad66bb6f65763e461f08d8f8ccccb82db720c0e52135dac82b66ba490a34fba3ff14d2c3bf156d53819231d68227ee671e35f80990b6cacb50524d0d83dc8d2dbfd7b353591b3e0135b51ae05723e300e88653c7acd9a80c7703d56d0e7b300fd269f1c6227ce52d91241ebc173fe149a630b0ca316b0e8188e73de9fe22230951d22c639a769b3f7249339c2d8a1d06fd164e63f14803b916c87a1cd4e7019111028c1538b953abab92b267e1b621db52525268b8d3f1e35f3e9904e164fb05eea2b1a0c93287ae85617c1e026baac8c373b6486de59140f4af383de97e35856e4462cb58af33b1fa6ca0c290d3f1b67541f9e1de823f277617e7c4d8fed510bbc3f781b1923ca18358a80be6af9650f9b176ec6de3c3a9980748b71616f75a652088eef81d651abaf0cd7efedc86cce81161ed4db220b4cf1314ef7d8eb0c7a784ef085fa763c3c0113d19a14c8b640501d624bbbe6df6ed59ecde873748cbd1b98021dcb4e3869afc365c7c325134cb41709975ee6cfe0eb7e792c017a6856c5ea40940d3fc22fdd6a9c3f3646e854eb0c12df8f10a3dbb413a87528af5811e4a1ab75971c9cc9ea1ac4b44e026355730e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "be6becb9eaa4b043eacaf47495bf52f49e427292d49148d4c329c62bcfffbc22", + "proof": "50cf37c75248a4e8c6e6a481ac93f0daa6fcc9b22f1276ba796b3692cdf3d2250834a264fce0f655e320ff7bb45412e010d0890a5522231e24c7671164deda5e64c2e9d72aa7092a8f5bc0ad792a22f7173d1066399216fa069c69617cf0b7087e863305ee3c3df48999d8e581b564098d16789a8a9d2ff18447a96ce460353cc2b9e9901a0b6d943ef935202d328a54c79aeaa85b606b71088aaa0bf9f2850d78e248beb8df504834bc06d9bbd9afd287ec7255f368d405db48faf2e84ba904b41db967985aceffa822848b3079430f26125bd47308756b2d7c068eaf0a1d0750a9e9fa8becb6f503d4bcb92962870967aa6dc342d277338a32fc57a655382fc00e0e19a2f195464851ef7e6372fe476505c662ea1d52bfec3b433ebdd2b4549e1218d60d4ce95af940bf132e616571f06fa7e3ed4531a8636ca653c0543e5660b564e1b04e05a7488635d51078b7527220ef03b10e36506c8504e3192655617a01f457cdb54551d262026d8031f286598425d713d06a5546388b5badeef95166f8cb3ea47e6dda62bbbe58f1d1c38342eaccea3e6c2dcc172b380c939a29215ca23d835d18350b31e188d43347fbe4828b788685fc36db9340946ed6dc8a15f0796854c31eaac5f1c153c92b3baee00389dfa71028e8b2ff49b05299c87219c46da580911f705e66887f11d4b8f1f4da93e08469d1a292c4f204b95888ce77461bca05124475348ff6113a49730ed9c42dfa7594e43e587a0bd20a73ebd375ec91bc84ee1b3e75528873c4ce9a91003808f74a784e8cacbf13dceb567a3c096e688c7d6f5864cc78afab5ddb2ac936b1d75d12471f4d11406a9b103306bc3dfe98582d5c3d46088ffc765dc0926bdab4d336b48d7f4968ec370d43afeef108783ddfb74167589f29347417f088242c4a91fe7f2e3d5cd44d132a8ba116d103" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cadd560f2c9f8afb7234814d7a91f022458a198dd407bf2a3f6b74a1c70f8415", + "proof": "c4f0363a06aacc3faba0c2369c2183958b0e42a131803f3bafab541bbd7aa27a8ecc90339ed6171290c60bfefc4613bf1ce9b308eae672db47c4e122cfda0010e2593a9327309c6eef3c932b7595148e48f3abbef21fa47010f6b49153f0252d6632178b0e9769286e5a3c3edd5276fc7eadbda97106bfdae9e3041424ec9149c4ac9911e4a01de859988fc3fa9c9c41c2fdde334a4b5a95392e2060380a220b241d5c6425f23cf56de8cff1ac3d6e6a754cca38969f6fe2fce62250ef5e480cfe153f07fce85e8733dbfa04295b4c50b201a62fe51d1c5650632e941f4391043c8d645018b2fe59c1ec33c2c9954dfac4b0c64a303ecfcd770575194773596430dd1879d5ce888f8ae6850a2604daa74094a8caa51dd5798fa04ef5643eda23640ca99aaaffbbc355eb3153a067ce014ab2a11127400f32ca611651814d705d847ce8dcf5b373562ff08494edcf61e2a989e99ef595eeb42824b36447fee421e276398b39e7f8d2f56df44cde5e99eca1a19a0ec7e79f2005f67312ca1d1e52ea054e78da2d83f98a3665e11fb39ec6297143490c6bafdedbfa5a95a1eddd717e5122db77eaa387c43ab56da7d90ca93788ca8bb46c8f0a4828d84981d0e777202074fa1dc42e0f7ae4a467e4f4999728d97bd69f2548f6b3ff7d1ff8e7ae4fc6be00f727e1fc31609523164766ac0560e112f7726fee2e974c33711acb021034ca262aba2187b8226f13f1ecaa3f866fecc79e82dc7988f834a031d915727608a69d324b359a4e76b5ec8e0ba8b20ca37e919be9b0eb098fd0c962491a7b52c6180b90dcbf6eac9bba9bbbdcaacef093dbd20f39a46fed6b182b4d043bc339b35d1a91df836c3454a3fb01c14a43cf4adaa78b7caf8e9241f8da1fabe8af06df09b1df76c0f5cd8e6b5b0037ed07d05ce92dd7e541743709f6baed72141a0b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d0c6397d4e9d1f2ff4ea2aecd7539c7f469fe582b762cef067df3ff321dd0810", + "proof": "50c64b0989e9ec9443573967cfad76cf7ab3c1cbe508a6d666cb73af79d6e74714d675139869d46ee83deb1fc31167398f851dbe0ba94c5a345c8f2ee2b35a6d5016b9914f83dddf9a2a838fc01423f702f4c5e824e204859ac405909783b12fd6332bcf63e5fb8d5ee5b2cc61af3776683e27a60a1f8bfc1dcf66d47dde8571d0cd1b5aa38ba177713918cad8c28eab7b5d3af3de8fff69054be6aec44ac004b85f481936cbc642a57ada22f41db399464a4bf7d17ecaa04f9ea10ea42f590e742226839a8199fa8275950a5357955ecfa8752ab4cfd26315d0bcbccff73c0ad039c54926b5600e6b3a7a3079208224c96c9be8c242d115221aa0abfb0b412230905c744d53432e05bd97942859c5d240e86010ea8442a2ff51ae256423d42c5046141fe48dbbc4d89cd8587057f145257d6c65d2fcf4e8a93e006b08a58d54c47df62f1a04200785496ca3ad6a43326f665d4dee14da11a38deea04016316d16335f0fed41c25104b99395d75df4a221a746d6f8496619c6c8d0cd97581c3050dd09d3712601008a557de1c23eba2ed969b7016427a6b409ef25cc89311a7b1e4e20f087b50c3496ba1135d1bfd8d98a22e878077913eb541d1a329bccfd7c8c0d4484bb027df60a5cf331924e1fcf020824cf360b75b5be2ad657b4d51f2de4d2b450459617755da17f325d995f54efa11750c8f102f216fe813a86c69605f831b991ff3abbf0dcde6426410467d4ef624aecbec8058695a329e602b2cd194c1fe73b5972b7c8f9bee00dfff8759d1272547bce3fba54823d1c5db187b23d0624c0c303b64115ee534784e43283b47bf41e47e666b204ba67a07dd71f1d584d92512401ba97f0e452df9380318c26ee383c61c5f0fed3686f234c00bc7503d97b04500a38eed2b01fb6b1d8bd9b23166d380477dfa3d16b2a9bca48f6250f" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 20 + }, + "commitment": "eac6bbdc2c2be436e1df778ba20342b4366c81b5f1e450d199698bdc108aae1f", + "proof": "c23569dd65b3541e8681951f712c9e04df48e8985537ea8a4996817ec5e6c519d681cfadeb76625a31f496d0798ac523923e13515493716094fe6db19b327c6b720ffeb696d0f29cab44c7e0da176eae1e39dde709947b15197eba99dc3ad120ce21dc6f082a63552ba19e7a45974f2b14083319ddc56c348fdbfe3a8ab1841a8a251f25f601aeaa120700436bf22fd6e9f5538878a54f6a7d5fb12690c12d0584b707adbc6867afb752e7ea2d9e62cead9d9f54e31abe4329865c72cdeabe0ba16a7aafe2feb86e78d1d187989c88471829dd6ff3b85558668f003722154e03320ed2c799e10c7de59435213062aa6805caa1b7a94dc25709b4ba16275b01099ac8caf836f54eb205f370f7e88bf9536e2f9cae986ce2b2b6222cf7440e4a16b22f8040956e29aed7a3d8d98eb47e51850a2ba790e23db733c4a360cbe9710432bfe73d374163412b7b1bb6061735c7f845b0f5b61f7f8b7a73ac69e5472c06c6c07a7049e08e72c1d9e7f66c62e88b19242e872a77d0008105ba2c336b617186cca673b79d83764c931529c3dd201c4d32785efe7e50b6756059a784c992458e8ad5eb920ea99e8bad69c199eacc4c39d6a06da56af554136849251055a16dbaa0ecdff140caa30de851d2c36f56123a227e898dd80ef93187bde1bd9cd2258e4ea016568f7b354c2407d7a5a37f95034bda8a7db366e59fa04940e91d5d70f695983b45bcdd54f391522b4d4690fb572a419e3408088e25761d866560c92fb877d5aa3d447db24f62c5375ad32ee8cd988d0a6036612182e744a74b27973b548d9ad79dbedb45ac2deadd6d0426a9e5f23703a03ef6be6692cbe3bc4cb43f96cd1c9b46be37d0653c94aac724bf4340c7a97cea6b2eda286429f2119d000dd59cb35af32126d3555a2109c7b645c9ad6caa51bc563e024d9be4ada913db0c" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "141b9e531363b27cb30a98e32fd4ecb50b37ee309d98aa3f02329cf4de6eea46", + "excess_sig": { + "public_nonce": "26aba7fe9eefaa7af285922e5ab5c0f5472c068fd974e92f2df97a3022c1735a", + "signature": "fa7dec596b7e42c58d511265b3431854e2d630cac84ac135d4678c2c30809f0a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "6218f7170779d7924bec75978bc53798ab2b4acc7b674964ce184cb32a4d3643", + "excess_sig": { + "public_nonce": "2872a2df6d8e8b716983829cd7511a6a772b0eb007c0128a6f552dc16680bc4f", + "signature": "2097c3b3efb35bd5747b465bc8c7afa8f140921c8b6cc2f73eb17610c588df07" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "a22fcbca03f1fb4dca2ccedb089c3eb9a87438c99081fe98c3930881bac7c550", + "excess_sig": { + "public_nonce": "6062c7e94e14c1f2f669107c6faf8570472859f24a24b22ec93ecca6a7cfba03", + "signature": "b1d84e0d26be69e0e801c8b92277f334067498d638d7669fd7581c4ba5ada508" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "b02b819b9930764efe54f2020e46fc74f5127b6a4e278cec4307512c38e4554c", + "excess_sig": { + "public_nonce": "bed8ffe7aea700c42a5d5f178b114d7b1897aace2bf6d3430e532f997160e61f", + "signature": "18a2ac053fa5514d681f8a2b5e4221f812d1497781e2549dc298cdc19a90810e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "f0b52e5075360a76a5acd7cdc6e32e1ae9ae6b3d1db9087b0196612a1863da4d", + "excess_sig": { + "public_nonce": "c222faa81843e9fe67086d918e7e0a057b7a3b990a5a0150c6682c5fdaba8e3b", + "signature": "439bf822435a5865bcdff41a865da4d80a7178abce2aab346de4395628941e07" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "d81d28579003859a44726822f31aa39ffb18a8857ac51c963e81f69d7b944c11", + "excess_sig": { + "public_nonce": "56942abfb96f4186d7fd79eab93243a9b3cb374c8e8bbbf8c5a2d3dfe24c1b09", + "signature": "9e3f31f4dc5be7b61f61ab8927128fc748571134c742a92c3af1284c00ff9f0e" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 20, + "prev_hash": "06584a1bbea582d2c5763fc0c5040341d5f14569c3dfbc929802f61a83101925", + "timestamp": "2000-01-01T01:21:01Z", + "output_mr": "8c32b58c247f436b6bc617ccd9e3b78e9741a02043814850f03dd0e87e31c0f2", + "range_proof_mr": "5b9564c52fb43c0d833d94bb2c2f615f7d826745ab3f9d4b92cb037b124895e4", + "kernel_mr": "c9f32bd184f1842c6c27b6b4efd6eb0358b99d89b46b2fe8af5e062a5581e7e6", + "total_kernel_offset": "ef27870fe8ff8c83d2ba73248a0b3b9abcdbb6be4e878298c79fafad389ae90c", + "pow": { + "work": 20 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "08c699e1369b582e67b00208131fd8d557cef52561dd6de858f076334722ad14" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1a923c45c02f1d27784da4683bd476574ac6ef77110e8cfdb508fed9f69bef55" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9465161c3bd0283710fb2c109343d5cca3ae02391f4d7a7dcce9b7838148ce7d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "be6becb9eaa4b043eacaf47495bf52f49e427292d49148d4c329c62bcfffbc22" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cadd560f2c9f8afb7234814d7a91f022458a198dd407bf2a3f6b74a1c70f8415" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "06f73c3a41c5b84cd2030aa6d88818346c937ab47aa978d5dba60263eac03f0f", + "proof": "96c7f496a4f4d58c249ca8b6363238272d46670ea2130106c869df2ea090da3a028c2062ef1259431fddc5ed3bd22e27337304b7ccc0b9320c7ae136c82ee412b087f5ac20da692745d8b9e6c2c52a69b887eb3592c3361459e8aee36215506b90fa93791b536e366be1c34a163de6cb44d5bf6a7fcfd95479dcc8f7bd7ca30fa881bb22fd758c629253437def48a49982091163a174c8b2dd66b3f654cae0004f39f295414192addb887cbbfe2116fec4a3b9e68954bc3d4cba38ecebf87702d3504c5b7d486e4f8dcb34e61e472e09ef8543ff1907d78e6fd6541a3970ed0162fe2711dfea51b6f00207d36c1cff1d28890c3a63a05934b53b6fc1dcc70e397463d13cf87b947f97f9bec522ec770596de3dbe492c5edff7d609d35af69e0b047271de13f99831bfe0ee9825d719b9d94a83a3d4d4ad086567b77fe8e15b23745e067869b52b35e7f2de37f6501fdaaacbe452b168a06d28857e490810f754988b64d29a6da9e4c9f41b2f3b4c1b677d322c7f10c302bb564b77a78300fb5b72f883ca7326130bafbae1b9b4c76024069308adfc7f55d57f075804169c7033446ed57ff2fe348c51164dac2081189ba6b97f031439a5f4981779d445b80667a8c22e88c1ff94ae6b20b0b8d2947931e7defdf2b495a1b0aef70a3b9629ff5dded35361317a01cc786d3e0445080244a60c56fd06a7a9997f740dac4fa83470587e1d1dd35e27fc6ac4e6e798c612315fc13877c15cddafb3aff95d7e2ed805524e61f2887fd733182bcfbfc3f8b32be7076596fbf384d1332dc86f554c5d405000906e6497ee5f15aec3e95e46fe419f47de00d15a9dfd25c0dc07054b025260361574dbae11f0b9e647eee35cd7bf9d1b8a8ed67196b379f1a0a64a47f6018b7dd3ada28c740122bcfa775a6f75efeca773c427bcb11831e9b08fe6f34d05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0ac1766df93eb4da1a5fa9486f3c53f14fd0cb26fd853b55b0e9234228745f1e", + "proof": "5a0b16d6fc0cf11f8572bb3d0e8bb0869da208d75226cd2de00b045311885c1750a1655c7fa9f74bcf2041d238d8efbb23c90bf027499e0e27409be953a18d109a188d6a65c673fa29f09b5d62cc7c8a3c9c6087ed1876aebf5c09ba1edd583726c9f775142daf2110907023d9a415113c9ffbb67f6d574e7fd7c478d1c5765e24d21a7412522c81e1ce0e8e442df906de0bfcd0c1cc69fdd7fff55987e26d0997b85518ff069fcd7e331102fa3f1da3705e97a045743dee5ea2b9d12761bb086388bdb4e90e5d0ba3b56080736204d26d112ea88343a9c97296e3a5fb8d6b0bce978a31451d47508559fc4f66d6458db9a93ed4385de5df61c0ddb64501ca730cfa6fa15023a20546cb78abc4fd8e9b087fa087785f3df900459f0d276d5a7f5091d4becfd3c4f890740bc8227305b13ac087a845e6bc30c609c15023aa427c963dbe075911d837661d9b64007c3fcf8995b895ee514dd6e6398afa3fdba16e646bcd40c2e9a79128df76b3215261a158276a2cdc41a3f714a2e3fd5e438d14dea3e2b44076a975639995d03d0d9664f947ccc28f8313a2c3c4bdde1ba4f67034d0b43c27f89c3f231d22a2e36d4a90a04a268731b8403a32fa1b0723f0b4026461d99548cae265f143bc01083a8ebe2aa824bb77a99cda34ea61e024750d60b4579140440a0ce628d8d50ea35919dafdd0725db0f0df508e59b690b4300b05366e57792a226e938b4af6034ee10063cd981f9ec27dbe999d26e0691618031636e198d5f7579128b936d49a84bca6e02391891de5e382ea7f2a43a81cd7c009e43fe5c9ce2c5cf13f32c5d4fe3047d762e5dfd6d3d26e1dc6ae21602529c9776242bf5503c5006cf67b35b5f3c74b30fe7de5d93f0708f6e6bb04818d97540a9e01d91be2455820e3d899014bb71a7398e9ec01202b2e9b5f10de4fdd89b000" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0c660194fe16dce895abf8415a3b02dfd21bd332c93ee75c13f3e06f3e6c133a", + "proof": "c232254d71b77ddee9a3e2c0ec929cdc37ac50bcc61e708ed3551c14456037212e2bc9c6ae2d4a2e5f5c22e158b2d2b1efb9d2b5db339fae1abcaea2b127c56720b0d84f9a8986e9313c03cf1318933a2a59cf71bac0606d6276cc4f1870e26aa6ba592a7bfa3aa2f3a0958a585f181f3b2108168fa225e46371636dfcfd3c72826541d60f811d01b79c6006b994d8365767379930a9f6cbc0b79d7f008a960f5c522ed75511625dea71ea53550173627984e156d00c52b793cd0e08634726022bced2160e3fba9360f4ad507d4d7bbb9ae839251d0de29db7298e9380f8e203066c7309dcc7c87c90265090e98ee85551ad5b2658965b67bbda19e9a2440660de291fb95c948ea951e22ad58307c40e36bb33fb1ec40f702f181606c44a1e44b6a2ad39c0018454d5ad8fd3dc12916a7a9870039e4c0cb8a9b5ef200768406576e9c63e4b374be3a57ea21780f7bd8d0922c2a74b9ce8110887e7e01cc5f5720a555b1c8cf8981c8252a042e7e15109dd393ea6f5c75d4084ba0ff48e4f7a76f811a214cb21018dd8b2ded7309cb4dfccfef3090260ec52640321577b59b836ba5bb3bacbb353ad737faa9ab23cbbd2f6d58057c874ee58f86ffd16a8629e272e664ff17bf95f8fb49f8aaec897315085eee0ad6f46a62b1b7292988147d61252b19a36f66518e363c75a158a4cb09425651418756bfc272edd2117cc21da6a00a219e8b22508a68bc09fad5fdffbd7a4300bb41227d9702fd90f8d09d06b0d085bad6a6b10d7f85c903ad31a10dd37656fabca6f8a23d7fddc32cd5f372348666e74a48d07bf79edddac7b6ffa6f320daa3c9da60e6e0920e8fe17e2cae9673670fc1a5af0c79d3261159c6066a674496a0b0252ee69c4b856d6e3e6fbe709733a964c5050421c58ef634e8a724ee05e7f499ba0ca8d8c9f742bff0e5ff202" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "221b749d8ce8892c935d81fb9703964577914cfd1df14377a2b09a39afe4d458", + "proof": "ccc8196ed3cea3c3af7a4b6b4472639f5ec7cdc318c2bb5b816dd2391d1ac27d981aba51030716c68e3342c6f8b0969e263ad4b826b9d5636384042ff276f61d2ac9a72afb9f425efdb78c0433460ce8bf9752d4e82d0049aeecbc9c16c95a7c34b69bed299441c1042c79b8ef64e0855aad498f20e27c2d2bfbad7b088fd70efd360f8b648103da1006227974e969afa41fbfd131f6238fb310eb7382192609e4407444fb3c045c9700de3dee8e0f5b72f7502d43e50709692b0d9ea47bf802ba4f514017cebf036d438cd46566e2f4385cc422210feffd8d0d80f45e57850e643bf8c2267e6225dc9f343104b498e351bc1b6930b21e30aea675bc42c2e15d0eff6c2924f2f256d5081bd3a0f920d44f0e068925ab169a74d01f4d2abe592d00402c8be3c6f5bac6e18b68bd14758b281d2c09aa26add636e858c4b5f7dc0c0ab3ab4795a73f5a777600aaae0e99a2c3fcdea543b0352ef33572fa870bda0aec6c1919732cef7562d2af084074df77a2b9033df4e195f15dcf58956258e62b748dcc24602b20e8ede41506a5c9978a63b5efb6230173733f1d2beb8c5309753412c25b4372ce1d7acc919ef23d0434cf0f371f21f760cd8179b35887a0ef27c0e0341a8ae68162adfcc22850bf1ae6c045c11ef53c208150d5c886fc9d331946b2cb1c4ce9665729e63c53b3fdd1a1769cb8742c5857fad3f287efdbe28f4322ca916e9bc3a1d0a30804952dd3fdc0fbc16c1fcb75435267ebc3cdc1a3f8227c0598c838ce4558bc0a9b02acddf118c1fdbf84e4cdd69bda2c92c590876d2f32be1584b27291c658fd6f8b2b58afb9cc2c621b58b93e8670be81d3eb63d177d98a6f4d5b9d6e38d63136dc759f4b4aab239b205545c5e8b83a0bccdda5fa0499002d35b46d1f7e5ca152efbfdc2b053aadc64cc44b46e6981969e82e419a02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "22a3fcccb5f07c0bb20b804414baa0263638dd748c333c1696742903073bd433", + "proof": "049f33a59fa12b4cff319ac0989102ac4a0343cfe2f1ec2e34b88b4509d7aa2bd06af00768d84c4bc02cad6e445e06411052c208e843ce7060d36a491330880ffe6cb4e4135a040a738975913de17a940efaee82dc15f2bffec4f02e7feb9577da72a33f15238ccf6e4b34f630057226d8951ba42020f01f13220c974935ce48331cbe4802bddc918e12587a462ae66fe285a6a5afe347cd76bc021cec17ed0d0e17d1e195342d49988765b97a7ff8618bf76e5a0c79a6757da8ed4c4bb2a00a83bf2eec17b006a8fb6d57dc699ff6ff76fd72a2d366b9e11d44402e1bbb9d03981a5ba4d20a8c765eff620bcaf2c3f6b7cbbd4f8b8dcd998e5f7c5afc431317a2b8e51a3617cea469a8a485979ed2dfc192c1e966056f6e0a61759322d11475d8598e643446afefdad7563a25dda807481fcae19b47f5908d2301c20a13304df4b6c45d3cfd118182302d8b4e93dedd0a628f65cabff628c8de61f144b62e5b161a0744d34465ded47379ae4d02eb41f7fd80bd28bb1e60fe05d94b1cc52e55eaea69e0d8b48734825a938e1874035d6ed17e68df1dc1e70f58295ac07f6770841492a4281952310fc76e40276fb6940630728c5cfd9334ce597516e9f40f7d9621ae3399e638cf754d8b1407c9ecd6968b0c89d4eceaf0e897a82df7ae8c328ef56c4b53c4b10b1c37cbb23e3890e9ace27c8a1063061d396cdf14a5cd381f901b22866c52051a0781dbc90113cb8d76f7178c30512c512ca869284ce39266da48520caebffa614d193a1c1bb5a9abe8d153ea920b858ce08b61ea92abe2452a3f96ce3871864f2a1391a3d02105ba898b060acfaa0c3a141c14aab3d86f49ec0fc13fb6d1053ba466119c6f71c313a7f5b001acaf6c840dad52d48378aa0685173c25d506104178c47170c2ed587ed0996d4c9c68d7e3324757e37c8cf404" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "30b81cd728cb90fde4ec57be8c84f418175cb447736fe531a4d0d83bbd945c6f", + "proof": "729bc61729bc64ae5e9ec86a4df40db6b96ddd8a2cb55226e77f4b5ff57e5b7284946bae13957d63f040a50a24b3a2624436240a71b617c0060b5febf91fe96f92b2d42337fddaee5b53e9878143df563d387daba2669c9cdb09f91f63e3af45cc23cf5d55c5104bbaafcf4919a2adabbf6f399bb637a4a9432a00d05fda4e57c4354ce5c2c44f939058041aab5b31cf387f5b170fd74854af7f1190ebc565035964ae9ce6feeb2aed3fc99f35b050634308f34cae5b7ea3f13ff6af8917ce08e9fc1a38ff3afbfddc5f063811464578cf73569b14738fb5d76ca98b07ad720cb80e141af41369cf2b9247d5805030eacb1e7730fb480b52e3f61603d1cb94571613c204459934936c9929ec906ca5790bdfc3681461441d0611c67e81ff6e6d702d9b85129101bf87d347b1af31ab8c4a17f65e27f35b59b8b26f6c45e91d2d52c85b9f41c291f07ee2ba97e102678de03b9380ae857020e404353f1fec2e0786994ea749c3cc5feb99ec374650bfc83ea8f31279e30775150446e93de98d0b22aef4331c9e69744b36850034fbbc175ab6dba154b4bcc4fd42a76710f1bd75ce17e5da672fc68addc32638a7f9ae862f17cc9fd62f9b653908368ac99dd325e808c2e284d07c84849233ab5d49b345aaaaf306584972fff9d3889786a3764b4835bc12d7b7fcb78d23b7e5a97006f4f9b15d9b625231b4ece1d2b47550b558004deeeda71899d67f4e36c60a74090247ca002293e710c0eae7b6b6cf62175cac71026a7cb2ddbe5cc468483ab3271ad19cce200da7ae5dc2d4474088a4b25802307de25e48a04e1b8c57594f2b991c3952e48f9fb1a2a1c43e2f824a6657101bdbe6ad977239c632a3fbbd1cc742484aa94ed7cf9c74a1eb14d0bc6e58140911c6bab60a672878129abc0f2dfc7411d4ed8542d17e13707bb7b9e145d7b605" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "54f5fc6ad9d827799c59d8dd4e4e56b7a1bb6506e69574ec6363c694dd67077f", + "proof": "b61d69ab3a50fa5dc97d8b6555c9401d8c32b9686e10272414c523e2a4ad23686c1580ab9598a49865a9f125ab0b667200f6633431e2c7a98de943a38a6e7f6bc410822b70f93b5c655f312a2cf19fb3eebc459e0cd5db713fe9953f3e5b20567026a17e11700f7f7880bf88271e98c3828e0a46af88c6ce5f2b2130bd5015395981dbffac59a3500497241bf4bd1a7a3cba75cd5f80ae49b9caa28f8279530299cc86707b86e0cfe8c80077e2a0e0cff5c8bbecdaf75616e62ad10a4d20a10be7f8b0b70949af86707f92eb52e0437d3b5519908ac0b8eb2a69f2554cea1706ca44be791c423733fd536daef293a39a2235fb9e85d7753d7f283115aea8064444c8dcdee889b28263d7617ae5706fbe6a4e5ba0b395baf1d0aaa96769d14b2538f38c0e133b084076e064d1e42cb6af482ae6ab9fb6877de83d3c5059770e2f34a43857d86cb5e5f49bd602fe9e105b90c6d9f13559835582d1c06fda508853f4dcd36a011fdf534b595609dd564761c8d59462820b5d5614a7e628e85a0a3402e084faa83de70e8489cb0104e0e81745281cfef205fd9e8fd3e73e853f8f19dcaab5035b061431948a81c98640b727847a176032a82e1012725c5683924b31904e58f39508e13be0f758927c454e93212b4293ebb22a03caf212c6ea828931d237f22246e41a061e6328fa5cee11d565b604d379edebc6819be14938d8d42ada5407c92fac15de5bdea796e58b6351764d9e20142d9c2df5b72bd26ad78146fc01b7467a9498d357d3987630b6ce2b718adf45216a1f7ebbbc2a3b54cff313f056b6fc897e8babb3e19ad4555295be1836c2197457123657884367c5aaf979979ac4de9bcfc04f22f875791b646caa85a39f99b23edd123816823f917a9c04ab7dcb8d33e720d6c0080cc8e311e2b43926cd9ee29d54e4ac93502bd729c70e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6a3a5a405321566ffc5e1a0ea628b7fd5e0baf7c483713b19456c5513619b417", + "proof": "2023ba9e6774a16d6099c5c7ca9fe3efc7d544bdcfa9e717d3b27843d7951221c09bf67db15e0feb1e8599bf24f2d6a159e0ee8c5df5148472a08fec02aea1148cc39db0c72cf9aa9ce8de9cafae04a4f864ecaf4f6a5e302f6106ceee4b434c9ced9d0eeab42405da378107cb787bbf39c4dd253caed014a5605d12b9afa233c528f8cfa422c5d68f4f9fa490de23fe4cc6a3d4d748fb910b3868ab67d0dc00ecf54e757ebf7b0212776aad7656160997208274118ce2d13e3ff550a3ab350b769eec7edfccf668099644caf5d841638d2f64d505269bc0e99748db042ca70ae2c7880d3a2e706349a610480cc6019e1f11d1dae0b87434598a0df79f3e974564c86ef06d8ead38395221220baf0c00855e89d3882ef7d66d4f356944ce37123cc198016cef916291da7b6a3bfd6ca70a8cf0e054345cfb44a67bedef5fe21ab6e76fb77ef2e8afb7d234c54852795ecdd602fffa612000ee282a8128bfd13d82716ef3034363ed04f83355976f3bad91d9e1d567ca6b3ca5fe185a70bb8d681e03d92cdbaacf9309dfb82a7b621cd072689ae8b3283688d5ed5165777de070beced1e2afb3a96c4d644121e97ffa391d5227b2649cdef9cb1c2195f18f805874e5fb51422aabd88d97e87249fce68c14e1cc559386b568d5f867e330dc157976012c519d688063b4c87c8eeed6ef7652da6fc82af7754fe698434c4399184f220ef365cd0366e247b7c358c8ac30d288fd82b55b5a99be4728a6fb9419174df8075c6eaa7dd5cbe4e7e4da5c40db85fdb1c431aaf346303b3329232bd95f4bb6b437e50a06fd930f17ec03f56ea1306a2f0b4b38fe53a214163272f4bd0a6dc3e53ccde885ac6d7973566d3e84682f7e73d9948f8c3480d46c0b5db176db0ee3adb622bd6ad482236894a74b9efd3962079f50fc3461015366ecfb3580cf08" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "94cc3a87ae21b9fb99e2668bae97d6d51c3f6b4d7d32b97d12108fafc3523320", + "proof": "64cc5914f5431ca1af12d554f0c0e4021038660a45256c56d16cb6a15d34607dba89df0f8241dd0da298d952eff0e0924f13b54e5e3a983f80804c18ee6bb61e587bfafe10d47d2cd8889876b719a157c707ddd0852d91fd719fa5d39154435110f7db0ebf11509c5e8960c84f30faa59fafddb1c2f91393e1f3fb952249882600f50871dc28645457125497678b17ccb18f633eb882d79d4d873765c49b67057fe6390f5728d06cd9ea3965ae6d2703e564ef8e133b8e0a5e4009ecd0d8f800ad33997b238cb33caf7c249290bd795ff4a9ad84cff55928ba4d9530b965e2035839a494f03c9d42e5fe6bdc2f73eeae0c556d75f49d404c0898ca1656e0e54ec6a240444b7e7393d86459ba5caa50e98eecf626f95610897aa3ca9bcab6176508838e3526d894c57bfc2d3ca01b1825186f469b0224763f8c5912558a7a0d1c4cbadfbf400693b799f2762c5cc9c3caf872e18cdcd2363e5d7c3b1b7d79993068e612438494210a9d22cf5f4565c82a8b5f1686c62cb8d48883d2ad789a4d0982b6d3a21e84a5a45f4f341ce80cd7ae8d68b92e218cd66201653cba1cccfb50eee7dcbf3df81269bb7af7a96306980657af1e7e8ff72e6c3dd3224162f0ae1d560049e1f8bb36028737e1faf4244137e195f9f4f371aa428c4add95f801801a4e0a95b8a4fd0784d6136fd8c62877704e785c508bda7ae7f62f5253afb34e66aa638fc90f4b663c5eb706cbe30de8a5ef45e2d9559b8e55d161c029056c3e36ee25d7155871e4d60087e1ff86936d39f0df2bcb350f4869c84b1424b3d130584e3cd81a4314e24c06877c644b6beab95882938b87882f78507616809ca8503a0abfa890a44d7a82d36872e7ccd4aa1fb7faa46b9f3c6c40181e061f105f6b08b002c2bb7071f3b32835a3117abbf2ea4c5170635983bb034cce3c0b9ff38000" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fad50006bb82747fc1815dedf704670b4ffc38c8ab1c7c534bdbf979dfa3392f", + "proof": "7e50f3d0ecb4630bb7104c82feee401c87e567597184da30819190dd9aa8cc759ee6f6fb5bc87aadd7d274a471e8ef6ee6cadc27f1f117af0652fad1ee41633a7e42fdfde135f2e65e26600760f9d3abeaa6b38cb467ad2e86b1a119660a6a3bf475dea9e08dffb6357e476a6256376247f676f067349c6cae552fb06cca4b110544bdfb87de44cf865584a9cff7a235f84147a01b5b2611329d363a2e19dc05fe0d4cff235cbbe1cc131343529081eb988adf3f6e58f6ef2eb7a760f4b3da080b6f1b4541a9748c5f494cc5ea89c4f607b6e2febf43a18334fca3a19885390258fa7b5bf55fe29094a3840c8c1dbcd3c41b8367e359db16db5221f8f0ad2c4970a5cc60bf0102561fbb8bf384b158e163de5b797aef920f2207703b5d5d677050a3e68bbf58b03664188c9f8bdd132228e6c1f40eb9da515210501bd11ec82b2415f24ceed84816fe07a172d374d93bc735108f6f679c44d106a3a9a713066fdc17fb057e8958b4481c0c5ae7dd75448546d8884c10c6bfdef79c6ff27c49584c13a9f0cb4f5084dcf22b635691ef0ee76678ce702902fa26135a89db5a29599025c96c8eb06e2b05f2ff18b8f4a6095e63e3cf0bef894f2d2b25d4063f7f213e55fdc1e7712f870fbc446957560fadd33caa70b58d0e6259785d2519512b100407f56da1d458e86b4864a271c172ac772728d794c0e1ea15bb5785e59ecd523a2aec2176a99eab636f6eeccab6a815f2197af1e870a26342161f0704f47578309c2b0491a797523d863b334e370f9d9efe94b93b1c9ea0c9f678c30ecc5e5b4071dd83f31bf496c9e071d20b2c702bbdb555bbe5426c76caff80b66a8f8743e01b6480e80e6f676ec3305467fab27cd9134b5ff381aed6c4fd8638b30fd50cc4bcb605dee42c1c9d3fbb713b86c3f1fcef8ab43b1c4453a1e53b91316c9b0c" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 21 + }, + "commitment": "c0e32e53349cb25555485dc71b24fe3d6b5f146001ae7126c14e942944ed5b58", + "proof": "3c555a1abe15529d270bd9decdb513dbdbd6311a078f71aac5b228e19d7d0938c07579f843ecab384069e48739e8e8085c7a1a1ec5d5698534f787342004010a30ca77ebba2555abe9ba6d3cfe91c9463acfd9e84a924b30e68a25f1594f1657406816ccd21f6d6455a7602967ad547d1280b03971f73c0d954e82caae0b9f54c2b34a3c37005f9ece9c7edf8726a8c1a59ec2b5eba40e662851d3354cc1240e6a39ed124bad30fa315544bf31444b0366478a2430758fc7e380ae1490375e07fcb7bd26e9f3280ccae59395c4c30bacb7c853ef0f9ff30c4cb83cb025766107d6f29c9dbdb92c46f942a222c320334ab0a41fd7ddea24c19c328c430a0c90530ca0237b49f8952b5a0165397f5352affb54ce164196c33667006ab01a3e5a2b0e387613a9f2137f21e4e1a3b2f6ce16bc8af24c344267151e7435c9963dfc376a1b6e027a1550ad7460987d0f8cfc4aa5057f14a4480d9acec9f6ffb18cf3163ea9d540c4bd6e065187db9e2540b403adcf8a35968eef69676f2503bc295f04f2d66f2ac311ccd2e78f3592b650691b714a9c73f2c025efac22d6be2d37990a56cc6e53becf7384097680bf1972ebbdf550bbcc030a4478ff5e97a8ef2bce0942406526f3fceaf65e1bf0730ac9c23d0fc8d95d7ac5e47e94fe5f09332bc16136929395aff3afdb521b7bb3c1b304265345667f86b8f257a0b7ffa3c2cae344b6c52f3ecf8bb771a8403cacf73ba04fb27893720e807bcbf44386da94d64a532625ee5d9d8ef4008e3faddecbead4ba4e186adc036250833fb8e2c522419b6534e630806126a54a61dc2d617c45a8055f386d6191cfc9d3ffe518a0b522826bebdce518c3d2a95523f013e323dc3a6f5181680f1d5e41d3d67539203210370472b3ff29a45b5b2f17c2280bceba90b9d82ba6e877e5aff396c995a0f9427b09" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "32ad3912a8cc065976d112f7aae3ad118f04c0dba51fd1e057e862755ea3d35a", + "excess_sig": { + "public_nonce": "1a8da3d8acce88fc92d8ba1385a0ad3ce2953c32e8c28d9ab7545c54ccd81f36", + "signature": "1464f55ec25ed3ee521875b23d9d91ea1a309fb4d8617c1d70b81bf72404eb02" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "34e2b4b5009b0bef12731f18daa0002cf3d7743f962f03b2a8dd6d4e4915b74e", + "excess_sig": { + "public_nonce": "70488272e827c1f1a0b79ac83c066cfa67756d1f0ca809ecbe8bda6292fbbe05", + "signature": "5d23f041eb673bd7bdfec3722aa582bf0d4da8900461b61c3353244b8411c90e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "484fce7ba38f9d13d19fbf49227fbbc1a63d7d12226684c0ad2bed1efac38112", + "excess_sig": { + "public_nonce": "e8790765e8fe942456fbede9ffb173f9183a7a7c67a37dd2c60158d155b00f31", + "signature": "39dd69ad9400a9fdfd60b5db9e4b9005db39bf39f8f77bb4f509aa4afaa59c04" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "86de422f7f8bc2a2b23464a45430f7582d66354e62f0a0a1658d21b7dd69273f", + "excess_sig": { + "public_nonce": "6cc0dddf451f88f1bbaf827bc6af73e35e67fd50dcd202c2c6a82259e316386b", + "signature": "63d899fcd9c163416aec1761a81279f1620463a8f2a4dacb2750a39b164a8008" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "bc83c127f2468558d52a8c6b72176a668b8fdf21eb26abbe1b92c21e6a42c835", + "excess_sig": { + "public_nonce": "d2dfec63c645c89d30b6c67b621f6de41636c01c57d0447d70714917265b5762", + "signature": "b030333caf98c776d86dda4c5f49b59f611653a78fdbdef26119e8f4a0290301" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "d0ef71578040683fde5e4efbc0da11371cf278de6fdf99af2f7b318ec892c45c", + "excess_sig": { + "public_nonce": "8a8a0dd8dcea8628de78a758b750484523a2662adbe3bc6a2e3957c86c611b08", + "signature": "98f1506502763af2af81aef51e2e8a5cbb047115a188df979543729d381f5700" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 21, + "prev_hash": "9fc3d08973ac34155c28a03ff5bd6ecefa76c9862095325df63b66fd28ddcee1", + "timestamp": "2000-01-01T01:22:01Z", + "output_mr": "cfaa5b8e26a3021a78a6051950934ae4324d3e24c403b1902b160bb2fac4d57d", + "range_proof_mr": "bcd483a4e93235f8d03d44a0c568f3422170cc18878375ddc0c388a2990fc0c4", + "kernel_mr": "af3f6a334a6aaa334edc560a02af32f9e377df9fb053f81e20f5d37c37ba444e", + "total_kernel_offset": "b0e2a08799a6c31888f60d37ab2081d6e357b60711e623c01004a0b8e86ff00a", + "pow": { + "work": 21 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "06f73c3a41c5b84cd2030aa6d88818346c937ab47aa978d5dba60263eac03f0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "30b81cd728cb90fde4ec57be8c84f418175cb447736fe531a4d0d83bbd945c6f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "54f5fc6ad9d827799c59d8dd4e4e56b7a1bb6506e69574ec6363c694dd67077f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fad50006bb82747fc1815dedf704670b4ffc38c8ab1c7c534bdbf979dfa3392f" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 11 + }, + "commitment": "0675a8c4532ec8188903301f2b79de8db2db4275192f6c78d93fc282a2cadb59" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3855fca9cf1b9a0a5ead45cbf5e5bbe67f3e8cf5dccf98623cf1c2ef1bf31110", + "proof": "8c68331793623447e6c2718730f56c8912d0d64baacdb229a79ea99acd80b20bf2cf026ee3ce4e42de1da64f8918e4c9e6396ad13905c33458624c8c5b70475558da31f8913acf653f5c6e8e9e7192c75131178f88f112ea56c8636338f70b549496ce389507b23388870c4586ba281dc1c55a70ec3c28714ffd4832fa682b700984edd27670cc3bac9a08122241e84f26bff1ae0b55e0be1d7407078ef81c030dcb6e1d9a235945728ec3597381b135666284f36d9531c03f92fa70b9d6160a8f44305565f2a580ad3557eae5201537de4f77fa07153c0a4ec56f94998d420298ff4952ae7b8f3a2c65ccbfb21b0c488b8b50c30b0a5875ef841acbb37a70262883e328695373562442cbba7ddeab27d8ebcf150af4ae601215b296e3b0f60b96f038a6a409c47667ebbcc9b6e8ff0c3ba00832c9099a5a0f1d7812bab61e0256ad45a2f15a8ddceaaab497cdf456bee2bc822e3fd4d5c67c573fee51fe7850a6b2659976282e5c09a83be4ee6a7a74db216ad19f89bfddad0ee50fd30c1a4a1a7d9bcc94b78527ad1c8f2e3be55cee269a05e5846bbd068ac6eb67d54830405a51e4ed53ffee2df60f25ecd7fda71fc079c800c4589991f35ac5a33d602c2cfab26dcd803ae8d968957244cdf303c837652125f3d52d6fa278c6d1b110a5592a6292c32b8e71bd645a18844687eaf16f596010477947196ef76022bbda7b6302ff91177410d70531df9f2ab05bc9ef00e82ab974d7325c6b40409b1830507f5257c0678c5b8ebe811efff03af0b1ce5632aee6736930fc218b1fa0b2d1843d346a364ba408a715774e80d0a0488c63733a68931247e3b1939cd4eca75b5e6a016800c53dc465f31d44537f3a43982a56b465f6a466887a97da31145ea2a10c4a895a59ec6caf8db8a34d8ebc3c43110ba39b2229e4e43a8f186dbcf5171708" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "40e8a102cd20dc2e604dbe92a6762ce0b2ebad138747c8feb1d5577e34ba421b", + "proof": "c2e42a4ccf85410f4d22c35405ab3dac017127d2bf3f0aa6dac57c02d2d9a9042e901c939796f56160ad4855d29226fd01a825f76a35a2dd416b2d95b10b077cfe107f932e6ba64f642123152112c4f175699937fda062d16f7f0c7192af9b556cdc9ef3f9fd4ece1ed5b3c623d2868d461b0dec476f1075e868211dc61018144fc26af428a41e952404cef882a4e9199265603fd2db86aa54d1e1debdcd9407e115f090f60a2b16714e387a971c8b2601431a1f4aaf473b61365cf6e83f1f08c2f531875b049acc17e710641d7ba4f01f8220d081037d26514620e789494901aad27c6d7626e8f105a0377b901fae47f714cbdea62708fa0cec337a2b49f77eb028e34db38831414e0fbc9f1b7db0fcd1b01e1ce584244c01077118cc44ab187ee25561226ad426f7bc1eb7cf650c4db48dbb256c73f9748fd08614e9cdd462809a3470c5fd39e4b4beaaa46a48afd2307d935f981490f9de1837f7cb14374e825dfea04f343215c7b096793d8ae055f1ceb676199684aa4a90cfa2cfb9c650fc1cdecd75c41874f2969f0c921e3b7ced6bb967882abb3f426abd69a4138f20da01506d8e51846c5492a67d86449060ca7f30af2c6996f34f805e8a14f808796aea829792c255fb45900d3397672956135e1a3a874a9ed6955c43e22a04f875b0c4f883d69bc5373e22d160c94a8493bc1afe992600cd70d3296afb40dbda3fec64bff451aedfe78a8aed170bf57a2cb8509270738c57595a7d33af3531ab3df8d4cb112ece4891cc747b3b97b8c73dfb931c87b9c7467a088922d578e03a7f90a0f278382fbc16442ee659e75bf87d5e8a07307573c51119e28954cb40ac584d4e8ccac00fb021f04fb5cf0c0583925e3e6cc6deae66d22dc37a13d2c3a4039eef5a1a589c26143c012d82e272ba0d97e234a3062817f2f7ab964bca9b9808" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "481a5c636cee5fca261a0de0c76ccfbcad298c2366ceda4b229aada8efc7b659", + "proof": "3a77aad63b5a1eceb55ef6c98bd9ae9f6b7610a2b1765f5c0756d01476f06476d6b7e0c342c3a187564061c211722017d2355f7072f52c3e8d5d0e3d5b62451f8c7a77907539f846a0cb008008a8440470fa891d8f107fa5ae9bb97e92a41e54dcc21d76b78be2f0888a0b74ad598269d477eb7f370a08e04a19f2af7a97dc6867996fd96692b572b356ab247a61969eabecf7daf695c75315828547f0b7ba01e03a5350286e2875e41353fd4eb09a3e26b4393f899b105ca2747af243cf6205ddca7ca19cc4133de6e3a0d8c35b42a96093439012234ca63baa0cf4be0fea0c4807994b1e7f3eee631b4e13e8960c61b8a1722415a35457ae7a5afbd8f9926b7a464922bd2ca93e6dbc36791160d7588da963cdafdd1bae6d3c2418021dd4301664a907b2a648044e5012f0c0922868410684dad51e62abcc7d8d87d3a562675ec075792d0703d51f505d868295c36976483a03af424ba0341564a05bf38a0b5a69d15c632f90c9ace6b4bd4d88ca6a7bfbe3ac40a5c360312be5e6ec50a665d0ac53ad2f562a6ab4645be911fc0abf275300b43cec787f7cff8689705fd9408ea1aab149069dbe6d0304e9d56d3f8fb7c1c9e7684fcb74fc7484989c1be367aa5d7e75a8053bdc92583e9feedc2c661b6907fb94b4f0c91bcbdc24ab21d33f6c7957d0bf2bf6afc1f0733dd3a5d358a9a5866f315cb0a24a0c2fb48fdec4165a07b7d4a60ad06ca9458b407ab35a41994eec83539ead5b0934f3c6d0e65461ec808b9094a8368cb6ae297c9bc68c53161e97702d6de218ac882cb6aeea3c0c3473c53033e7902c9ea520ac72cf9f1a315f771a2fae3e91a384cf32dd21c2650bd6bf9bc1893c0fab8f70b5112fd9a748272787ac47b6d73468e1cf4074f30e0fea424e7d8e35c4242f703234ba3c781cd374f39c3e77c3a54f25f80ea0d303" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "80b96dd31892bffa86ae769314dca4cf5ab5c8897fbc96bcb8864cac55db0d7d", + "proof": "e46f466a6ad2385ec7ad4b197675000788b3181da4c5d6f137c4b28c6469ca09e21df5722f7e02c6fa65624813c465a5998f451e99c3acffab7062013dbf832df2ad67fa3e46f9e2f8e4d72d48f78c5efdaafa3d4e0d6810cd77f5b3ede4ad2678d2ecad03a669732b580ce06f03b7793d44df91cde46e2b852651e9454bc544e3b4fea4a9515e4ff8bca5d7c51b550ae4cfdf443618907c49f5e37d99e1560b1a9f55c0a737dd0349fc1a17a1195b32f005177154e523a1ed67129d35b2a4064918f5b18c7418f93ed178852938354fbb46d71cacf6eb929bb52fd95523970dbab27d25884dddd7631f330cd04e99ec7d7a3add5fbd5d356fdba810029a7772a81d9cba38ed2308fe6929a671822907a94b52c46cf8f10be364f065488ace5e2c8b87de91a10e3422434651a1f47af569e7c47dadf88f6617bcdc2a21e964282efaf9d166701abab6abed23a6294c38223d44bc7e7f7bf73266e14de32f1e78088537e9b50d754bbe074a93efd81d71af372feb2d8c237591b2ff5a3f8bcf207ae4a74d575c68ed44ace684799564314f3778db28e5d0882ec7d5e40c72306236d9587f9ecc6a1069bd119d86643b4d5f86275d502ffe19b8c00834d2501905d06ebd0fc2b159f871da590ed9f97addb3e2e4836b8ddc79d30a964095760d4984e56563415c58a402ab7796660514e2ce2435d9397e690236d72461199174546e2d87f4d0ee7baf0831cb316a526fdba3ed479d7b8e71b2ffb4eaaaa782a765fa1ad85fb2a39c1b0dee26e3796ce189df0e99d9f81ca30b9531ed4e3eedf866343799d0a0a2a6a57474356dd0250621e187c033d6af7ec1052bb25403302316cf0fd5fa7cd27d5a2f28f55cf1d7b3021e8c41da5a1b3a2b3a61234aeb36da0838a3d2087c545e9c77f710d602223d5412fe694373f384b5d3b4d99e18220a07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8a8f6d9e8bdde9dff72301596512e33f1668e887f3e7f27e26727e4bac29b00f", + "proof": "fe0c8739fbe82b8392206f89db782cb1ade94bcbc4ff209859157be01fda9c6b7825d769713470fa8264dfb0f484a2b52ba64b8525a16931081d50ad52d6f211e2b0f09b4c24436f2c0b67a7ea4ff1b4c391ee5ed95ef90992d1cf335b097b0d6cc4a3ccbfa88dcb73a0b54b9b6bad6f65cb18268c0847f6c45302b5379cde6d9ee689a3b8800b15a1f6e8e00836358001559c29241570011ac0744e03a18d02d3ff67d9e2333e54a68745adb909e45247cd5202c2d61e5adc9e274534fa1b004d84cb5e09eab4c2ae86654aa97c772be977ee4959aa5c5810708763fddff9059615567478745a415448f6b056b86f75fc51a7c48a6228c0e2052e68ee91466b50961c2584ffac717a179516c89e8b44eaa27241cdece9ebc8d6f85fb089f873263e92a7fd6dbf737047465bc8f1adc7d3c5bc75a02f823ea70560087d48f969307a32039d072e3ba1e568f69c04f8a21469f0b33c9b320b4ba3216cf457066aa8f2542049c336907c030d46e136c85dcf7bea779f37f30b8ec348ddb4b56442b235b1bd0ae79c645b6aaebd1975d5533f6efb0d8903eb07b9010acf306798459e589b3d10ceac8530e9b182f80ba10fc0f2aecabfc70cf59e27c437d6058b604ce7a8a434beb26c141cf92056f954f8066e5fa53aa030902e346d068b8f53394cb410963b224b80ee663e74a589b525de91205dccd0806fc0a29ab3fdcd55251878900c309c5be244c2fa470f20537eb71fe713baffc9af95b5cb0293b7711d8eebb60d827471da174c71aa2538f32d86affabeaa0be177425f4a3d636308472677a149ba24cb854852e88a5547d58af75fd49da30cca9737ffa2040fb24651afdfdaeab696a7df2d3dbdf935d607421ac183fa3267459e6e660048af52650ebf12ccbc21bc52edc556317971cb19782af870b5a653193effc805f56c24cf07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ce589dbfe477ed8638fc2a834b1bf578cc4fab0361d95faa0696027f3936d07e", + "proof": "e4cf69a5f0fd98e098daa55c989aa713f80c715a72fb398f1aaa56a621f82a564221e036ab9c00de8b3bee8515728df481949625ace49e4d944557d462024b051ef924fc7b9f53fa3e71c54e8babcafc38e7d1fc7d9b88efa1d1fae02fa38a79949bb4262e5bed0fa8d1b51e329c3f45ba83dbcf9ccb32916ce33d96ca489c10a0a603ba35f1b618256759fc80ff261899356bdec2a12694c29ec28167963b0c04b97ce2a5b8354aa4f00fe7ee6051160c2346bde4a3eee71d0fcd1c21f7ec0f5b7c81a76ecf0e02a9af741fcc17be9a51e8ed7d38f26f68fdfe03819dbf04095c174bcc52e58651061e8f192f18c8358aa465fecc7473b6103785bb4542482d80026f3c9f452732754aad782c72635b41e8cd3f39bacc3a284bd6cbf3919772a200e7f4f4ee0034fc75bdba715bae97b6bdf764b3b26896ba20fe4137ce4039a8e3d96eb389a36ffbd04ca8061fb6042486be0afb976aacd751a6be57ef1908c2af8760ec9104aa0978f8b473ea3c00e4705ee57c54a8bb60acf4ec262dee47b8690fdfd38406bfa76865cfa8826fc93a712ed862a8fd3b4120a6ff23d9bd7374533528a178d977a597452679b79de3441a4739eddebdcc1e417840800a5046928fd83772dce77abe89d249f201fd4ffd0b56784fcae1c58c6b226a66d6ca5f2ee2952caab21d349e36d05937a4253994cd7774d4ad0742469152080a49a22c864eddbd958a97b5adb3b2a6aaf9f3161f197c589fa60d02fda8a1541d8e80637ee91baec41807e03c2821db60c79761f0961ab23091801a8391fed87de8af47c49a7616e809035c7dfb03adc85c75b0c1cba4beb270ae4b4dfb9971850aa13583b5660d7821c02854cf3a264c9354db07ca147a39c1a00f8d5b5639a7ad090edd08434883dfd991a9a3c26ccace6f22c9c67845a8ec67f3e8135b491d494a05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e0b4183013a45b312480e4d8f7ffc89481e19cbeea8b30f898fcae0e8c9ab357", + "proof": "04e4aa97262bec1d35be6465773d15f53bd8e06257670c9e688ad94a73e52947c47929d55c7300a6ddf8ba8c34d5093fea212aff6b0271efb0cb8713ac57480c70020b6774586e4a037471fad5e4a526098381b080ea71bd41ec79042f21d71174d5824d4f731c6dbec9c25b771299f734096d12c7fa31372637e00209c460252a00b746a11ecc593a53007fcff84c7f308f990bd66babf1651d437d21d75d07e2d5ba8591cc3f6aceb3e32876e5df073f8dbc338604c64174387588af2d3b00ec751500952a934cf5ce34028c2e281cf54f6aa7bec0e2a2c6126ec737bb570e4262d01aa1e3bd6aba2e3bfa1ca6807e026b37e082e3ccdf83debb2415730431049163ad1e282941c692e7b023f02a02706e360005aeeb7f8caf0c8ccfd1064030e78bded6c92b095d1a5f6c2a502f1bde26dcd42b87e0a8c024fb67f9b9084222d0879cc4177c9c75441049519a3378908173008b9fc5d778367d2bc083df787c6c7b48dedfc1b5f1d6f21c6fa4e970ad7a4234f607ab949ecd3f9ecbae8a208e4e4c67ce1feae8e178ea2d8f078abefa3971fd73b14200db03ee98f8178d7f8229f0813af0046a28b9b4b3f061c5a846dab0c9cbae7f743af5d3e405ae3c3918b7b52db4cd2b3675923e0e67ded6cae2023a800eb3fc7aa18bef46f24af1053eb211fb071b7972024b3e0d2b42492eddd0ce0b9a4cc921952f809a81d70d2a64069bf322b96d9631bc48a32b6d357e018a342d0282e486c5169b89478ac002ea73b85d9bb76097bcd4f3f2f693ae6fcaf4804edb6b6657a3bbf8c676b86d6f167901e027a386de78ce29d0184b8f8494a3e9c3b0dc52ae68c44ff03de2302342e8560ca352dd3069f9d41227e16f8a9c9b9914b4d8f0ff9dd8a4bedaf9e9049ef5b03b1a8eb4d9ec1cb9ef97ca29a7236624e71aeec53aa0c221346b254a0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ecf1371dda337b34f6b9158378d0eb9ca65fdb713f699fff7c6810999e94be26", + "proof": "d809c603693681bc17cdb45b9042c596fc02fa0157ec16153ed92acb801c69460e3cd18b225a3fbeeaed70fca803a66a8219e30845220f68994f71f671e0b9591c753b694d46da7f131fcefa867b07846208033ff608b891b8c2a94c888f12601e31c9c83e2bbb8d864db95248f682baa41be882b62f7a264881b76621d4e37ec5f52e88048c274c3851923f51e718355e4c6758249c506bbef6dc847359c20960bd5a7a4b5996bf6f32fa63ce3a4f41428a5a7091b8be9bf23b9c62229d5d0b57a2c78a0c8d17da716ca2321cc19dae7768dc3786f003254d2663001fa18f0278a52d2b1c2cc6d452f9636eb85bdf3ab1d776b4c3caa794505b34f5b90d36198a2e7b81cf42c6ed6f01bfbdb3b1f5aab2443556abab1ac18461efe18c6c3d5b026bfa3e4ef8a28ad6023decf5a9bcb5eb243175d75d5141f0c0be496ad6c8197838b52f0fa1122d4e6675e265e82144d96d127cf5db456368998a5ed220a629ac91da2390b9d7a353afc74e7a42733793026768248e1383bcb366978f0fde738c24536e9468540eeb814fbcb2a4a20dc60164bc502defe119e506ebb5bc1b6bc268b0b58a32cba60dfb670d28a53dc209326f26e1b5c57948cfc24b6b8374589e8113580ff9a5982b63e05892c3773dd2b9946ab5607fcd74822f4d7ca5543f6e041d95fe59106926d336d881d8385bee408db14fe8556a14e3921fcb50274440c3df79dc7e50663bfdf92cbc9d004fdedc08c9d2eb8d562c51a0f247747156c628e374e2e28726d4d67893ba45be6d6494bfd22883061c36df19a97433182e66918ca94569c3e1245a961a6d2fffe586e97bd8774d9bba22ed4b897481d565ea7f24108743461bdd9716fb11560b0fc584b0038dfab58127ac124bf5566504724b48b99ac7ca2ca72e11638a74803e856a9192729b28eb5d0787fe44e1f40d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f09394d73c25334a8157057e127e347c450f01e199e1422226e03d4f7bb69d0f", + "proof": "e0c47ff1d43cf60f95f0153b5b09bf58e4335137573c17b63554e7bb31ef521e7ec939c0452bc2f6c818f91eae447b2fad37c1bb7c39336ba2020e859962c63cfa696b54f788dd0eefdf8beb5e718ee22a1f164e118ad989e83f2bbbfc7881755eca49d32bd8badf2d616bb57bf6313e1cb5964250592cf7bb5e3341c4dee9188acd8e848dbce925ac34967aac5cdae5d9cfdcf739acdb389f6a885ec552e30dc57a54300cf9aec9db988678838165387d42462bc6bbba5948403598d42319005a85c6782ea86337cc8a5c1d495d4fbf01729f77c9759db7687d0cf8033b140ed255597b68e31cb3ecbe20ae4a67cfb94eb5795d1b7873fc5322b72751025760ec457333a04321a4547e14c8723b5a014f65494c23447800c1838c18dcd60063b8baa6e32507b0af03a43f76f772b84df8397ebf502a5c889691cf29ff9a622b8800280c322ff6c2e041094fda02833b046bafc16dc7314fca2a669302f3282850252d588fc81ef54cfa0ed8791730dca5707435d53785ac5a6863840561092f5e611346f9812d24730bafde7c9283f61c3253053cccb85f682bf512b534a105d8a77d1a815267ab01575b2e125049fcdeb5cfd586afd8e1bd19d474879ba46ab2a1c1cd8ce6e4ea22a3c11ffa1156f63c9f9f987f900897bd51db2444287378e429765956a99135ccadd86b87284659aa8ca120dfeea0db3dd6bca664c1517a50b89ae44052e4751c4dacff4d6c4dd7e72b0100d85ddb0b0b101fa80a708228a2e616c33cf8aa0649b976434b8c3aadd53ea97c8bf35adb449cb464b446d454aaf827d849a29fa8aec207655c478335962fd3d8bd2270d543ffe6fd9cd98c4a8344990395742ccee9726b3891da9aa6391b1a66c246e601a355d8bf2684320a2633694ce11d97e02baf64292437ed3a2bd93c5d292e557c828057a59969ca04" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f601d4bd649b6ea2d58f6743eefdfc17685570553325d76b440a9d761fad1031", + "proof": "38945597a16a11934fca15007cfaf7bbb7d8bc679f6c108682c17f1888ba6b2e0a27cc81abc0fa25b535c0261e456fca397b5e2a90d5d124c523966cc021504c001bc0cf9509021e1034bd7debe7c5ffb5b7a1e5c2e3f0c582d4b6d4a6e3de12f6da695b4999e0d8d79aa376974d0e832fda59cd2ac10e783b6f059abbda6e70d1e37a12ab4d46dcb6f926d7c43815b0ba89e140d0f7333e00b312fdd70a2f0e4ddd48df595246c532686683562808b0b4a0ff9bf217bbd3ae31fe13a8ee930fbd87261e8fbc6242b01bda2fa7e01834419674c949776609a13f45418cb35004cc2a21dae5cf8d47acd685a32458e08a2d03d58ba6def5dc403f74a9428543665030ad2eaa7446ba6e6f99aaecfa5084423cb35b0f133bba28706b8ea254cc568c5c5d617254e6aceaafe223cef16285c50919ea1ca928d34f595eb276567b43fa30e02b2b5db024a3cfec558f4a58a749acd11622af800ab6e9591ea3d20662b01f0c2be816af4f31696807bef41a8b6e99b50f332271bd7113ef3394ed980260adab15ab3b8a18696077ac5560adb6ba578fbac58cabd756c45da15130af487ec130b88d9f3088f200824e1bc4e637dbcb207d30c020ffc4ac315b58e6d20058a132e33a8d4f17d78a994c9e25690d5d1243c8fbf863b49167b878b10216468e79d1544a2e2c0bc3db37f36f7469580dc6692acd61161704fc0b0c694c0760f48b071b95ed56e9cb4ef8d0f4530d943dd39c7b39b58e8b9342740e308146452c8adb4a5e6269bc752fcb7402bfbb19924759793e46081b8f245e5dda6ece2dd86dbbcb9a0f4c59022ed3baa03d1e452bceb6395a00ed98aac3d445f14748238638d88988f5f7afecad8502c1317df67a4cb0a94338138d11bb6db156a3240090917f63cf9c7eea90d69e270d476be2759b6b05ee89486cfd60dccc50d77b0e" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 22 + }, + "commitment": "044d0c6ac5247a4bbaf89906e13a97dc142c1e59c2f67f9c972697f004506f10", + "proof": "4ee575f0d910cbc9f799a917b7d29b9090685759db7b7abbcf6ddc4c8724e033ce2cd6aa563032cf6f5e9e6a124250642e5b2a03b2999717f8b920dbab07d25a94b357c3967bf25f860b4db4cdffa020367d1dda11d19521a7ce3806c1683734aae5383662f19fe613a0a2492c48ef4fadcf8d4d68838f4865b8140fdf268e68826a117f296b3acb4492613e18f1a853f78b4d42ac4f65d372071ede0e1364024f33ae68a2205c0ac90d664728c492230da8fb46e83a99dd2590d34b05931e08a9033b4e850d6f81cac38c9e69cbefc87dc86fc613b1fe135e74d4155b383300846bdc66ae79de60745a143f98e76e1d308b39a71fb512e7df2347c26e55025708388790cbd958fd7550960005ae167aabc3448ee2d0e0b971d4286101fed60c921b39c5b9f31ede73ae5f9e13a98311406ce3b4c69897195dcdee04d602b23684d5d481252025d1ba264c8fb7d2fb5b5ac1c57df30e628e124b5115b3e32c28cee4cc6dbfee183bd57b03ef700a5178e9db69b3fe1e37e3734e588c9ed6ad48aa7806074d246a0fa1ae4a48f31d20664ba5b0114f2b929e3129fd744260d6146c220cdd3941a71389fa03002ba209e9b45bbd1a5702136cab80fc4d42dc902b90e9f105637e282d86e4fd702182e8d32ef29a71960744d486b249597d512c6560f25505749c6dea94cc39236476bf41ed03155da55249296b10d912a0b442122221baa552c6e51ce3ca25d76267d898bbff0694d70cf3a4c26728860b0b0a2d08cf7d779dca252248c9558d6dc42a041cc42fe2c56645e890b64018b9b6b73ee858637b2ea4eb03a4d096bb72b69f933f22eab5535acc48b17ac375ada03933d522befd875bf7c3ef18999c2cee49fabbaff65cf1e5f302ec6fa2fcab37d409db244dadd86036d1fbd39245fda05d765671652399f3dca460a5efdf81342701" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "00020f1ca8e1d412b6606449dcb4b29dee9b66135995dcb0773cd0fa1a70241f", + "excess_sig": { + "public_nonce": "103b4738fd4bd355acb5f4fd124298d9e2d330a2f64af8b93c5e36c83ee6871c", + "signature": "fe5e1c03383a38cc50361ae4a32e8d71a4fdcef1c46de6eccf1cdaaa5c1dac06" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "0845fe01b39ce76a35ec673745d20eeca06889064c687e1959b4fdcf63e02b71", + "excess_sig": { + "public_nonce": "a4d31da1b36cb57b29e40892e96d3cbd1ddaabc1dd6aab6c1edf78db30401925", + "signature": "493652af98377f5ec2932e08d515c716af4a6ffc14a33f25d78eaf7e9285ac01" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1c7a6f845e6d1e66628ed2cf458d68dd6e87bb5b0dcc324ee9eeb2d504cde10f", + "excess_sig": { + "public_nonce": "382c55d92198003c9010a7721384bd467e0d6c778a97210054a1bc25dfefd571", + "signature": "bd6232a9ccef87b1a992aecf38b6e4ae668ee9f595eb7e290a998692e14b3d01" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "3a5269fee840d150fd73f74fef805ed4333c5fcec2980fb10804a72fc191c413", + "excess_sig": { + "public_nonce": "a4c55a50bbaa353cb6e082f1e629264b3b3b041782761bb2d2922560c76acb2b", + "signature": "5db6073032337ca8ed2dca90ed8e89d713f88587ad14c83941896eae37b33909" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ec0246bbca4b27dfa26d4c9621684bc64ec37d5b479154049171ab28222f864b", + "excess_sig": { + "public_nonce": "78495542ce08ab701b51ad34ceb7e671fbcc812a163738cbaa77632443f77d37", + "signature": "9ec3357a4776c136767f3a803ae9a96d2d8c6f69f5d877bcdfe0572aac21da06" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "12a7d0503112a59818f973ab6dd943e965adc82e4de7f793b191615a93fd8420", + "excess_sig": { + "public_nonce": "76fd4a5b1a909cbdf4c866ee3109ad232b0aaa4c5ede1e92d6c8a92badfc5f54", + "signature": "02c8b8231a8b1eff61691e55a4d03679407352be63f9bd94422c9fa32aa99e09" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 22, + "prev_hash": "9ae3f8d009dd07509c4b55dc2fde6331a7e9a6ecd5e15e9a9355e968320be265", + "timestamp": "2000-01-01T01:23:01Z", + "output_mr": "1075856ed3fc3c4364ea9fc584d884293b1df09e054e582f2ce4003235f17c4b", + "range_proof_mr": "71bcd9d72922bcc933a5181240f81921be6e8bb80932cfabef0c3e1c2c9d5ce1", + "kernel_mr": "1cc44fcfbb4cb3783945119bbd8e8ff5565cedd3f6ba389189615a2489c0b0c5", + "total_kernel_offset": "79caf87ee4899e04266b8c508281945f1d37fcc3bd67ae26acead2e8913d4903", + "pow": { + "work": 22 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3855fca9cf1b9a0a5ead45cbf5e5bbe67f3e8cf5dccf98623cf1c2ef1bf31110" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "80b96dd31892bffa86ae769314dca4cf5ab5c8897fbc96bcb8864cac55db0d7d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ce589dbfe477ed8638fc2a834b1bf578cc4fab0361d95faa0696027f3936d07e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e0b4183013a45b312480e4d8f7ffc89481e19cbeea8b30f898fcae0e8c9ab357" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ecf1371dda337b34f6b9158378d0eb9ca65fdb713f699fff7c6810999e94be26" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2ae0a1a972816d63294f2e8e43bb543825e4c0415c14bd8612e5c4b0c400fc3c", + "proof": "044097e774e571c5fc80cd274683a6a1eae3058f9bfa7ba81911c46a0e116f1a06dc992ee8b5f18be360f644cfcc022cd54b804b3d25cc3dbf860d18a877730938da7bdb8131c59bd0284519203870d187aa1db476d705cd6224f6f9c755df65c4800032ee9cc9e4b9f71315bb50bae942bc80fdd70b4630303a84b70ad95d12d3f0b01f62baa17a66af220b2390b796a3af232799e6a2869e9368485a6f3706d588c72037f904acdcdbfbadba2bec07a0a7b20e5592e14eb2619dc5950421052fc4052d19d81b687d1b81af7580e62e11e692befd9585f8dc31f2f52cb502070e873d5785a4d436e3cf3c25546218566cc5764817cb1433566990f0c40fe40110bbf097eb32df6a277804ca2b12d557b00b9005b7da6e3807d22e86daeb326f42288f797d4118c877f0c139ed82cd4a4a2ef5996b2037d7f8b0ec8548be935c169f2205e55279573d57c394dc331f146a4f30069f1034b8d1feeb8faf3e70207e3ec1e0b144bdb946dedcd29acd0ffeeb5fa07cbadefc885c34a1744a5c6e159c810eafa695ef352cb2a9dfe20de13928f89da55908f133625bfb2bc708c33676cc11f87ee53b7a203bebf64dfa4d5557fc37cf33e527d53386fdf59e8c8758d6b4c9dee1513afc9827e2d498d27d74806c48f87d396d9d1943cca50737c10a944acf6a3451fe5c5799d75e806ff1a536f664af9cd98f1e98b5ee9df4670918e62a8933952ebce8117239f3a7661bdd8a04a1929a8d33bab6e537aca276833afea8e9859593f68b1aef957954406f784e50e3f91bb35467c7f9a4aba653bb2564177c0ff3fffa7c91f280650d016cd1f6148a800fb5f9a579f6cd659c908801fcb188408f012cfbf560a7e09ba014c921b80d2d23eef8e0f18d917c73bbc8017eefceff736aba65d4e7310410aff3e35f5803172e156ed08efdc2801b83d600" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3e7c624140eb82be900b7bdbc267a415ae65f437e648d43832c5fa8411ad2479", + "proof": "147556fcc25ef141d2dc4002ad504675385ed7365cbdf5db61f78f89d4d8d551b6a000be1ae969c1d1e5e0c30fc06232e791fd272d29078508026c21602f422bb07a2c05afc8d78b17f227bbe19b761b9d62d5f08186af14bf8b6907d876bd245e0f72211e8a0b1bdb3be38191283acf138bfae44ee694a53e41d0b0ac53093fd3f0ab866feea9aef030969289e3fa1b6a0dab3658b97797743b81768227b50b891010db324828cebc20e43c5b0c33cc2d42142d460384fb7f708a4020f4c00487f5ddedb42cb386fa17bd681e3582496ff6c51429fdea22b82f12cddf37840beef8cd5e961737f29c52ccaa3f180c74ba3c093d1cf3a8f7e11f59cb687b0b77e2669198bd530e2b98ce21fac3ffabe7e975c60d2c21fe9002f04d71b606bc715ab6e8202ba2bd83694e4c8e6e7465c0f00d58f7d9700abe4571bfe89afb087a106db2fedfa372869611bf913b82ff3feb2d825c22fa51392289379c6887f41c243d9d72e325a6622cc217356950eff7ceb360be563ead35d798235d62628a73b254208779ad269feafaf3332566a21065aab6c371dabcf5ce3acd37bcd9a1010c45a973fd15f7aa35b5075d3d1ea11a1ec704330980b13b83009f0cace0281894f624eb87b8b20f740683d2607c3af6d31ec51b3b29b39a20fe9632a6da594a62164c572432c86e7d151974c3593d254681d3ad572b1f4011b16dc8342705036ab244f40721cc6d13265ec5cf4687d76ae8d54623652e7add09477f2e63434cdc62bf4bc7e4df3100c4da7b6b9a460ab996e9e97688c5b4337bbbb00836071aca88c96984108fa9fb85baebd97daaa745f5b22a1bc9449b67dcf85fd591bd67061f451c0b4cadf86b06018c4ed84dda43e54f2ce1af8bb15d5c5f4ecf004b09bd06afdd89dd33a55bafd2405c84aa4058e2aefe937f2aa25bb95aa963df700e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4a6d0a4acd9f1cc0fec9234aabad11dc82d9a5cddb8fbc3ec10f9c75619d8a3e", + "proof": "7ee71eba62c49eceb9c02a7841ec69e2bdffa5f687e79d38f069f16dd15ff73180f27bd73293a9afd99e15ef1efb31a00dd75bd84aca37b59c0ba7f1e49cdf316c5dcf05bfa91f8389b2f5a34e0c7552e66e1946018cf0635bf6e82b12f45846200c8b8df784d8714764c4d81d40c8300a162254e8503e54f8bab65c4d6fe731d2fe3f4780a7e2d075e421e01c433628bd47dd5f04824e58654dae21d934640cebfc4aa7a31d3e5cddfa7eb49e518ead4a5ca183ed24fc39e9cfe17783b99e061d5615b4c18ad1aeeb226e592415e7368e2e434a21ceb130d3c6f8d3b7888004a6254afdb2cc78d2e51e04891cab5a20a4148ca793e0b0da4632886f792e32786e5d183416201e72621edeb4a6b0c50e19ede27f1d0b5e953c91cb75e5579a682e89772ebe3ffcc51b65f0f26f7d7ae531cdc0b703103b45b56167b5b7a0174832ef3dda644ee5f12a11e194fcbd8b5ee0379cc45c46d820d2f4078e2628395854bf72676d952fd7891ab207a7f97302629b2c66a9c79bc50632f576a0f938050c7d4009db4d12a57e131284d7a55bae87ec1fa44887bc0d963cad900acf811fc83fa60bde0721794ff8aedab5c7882f5dbd540dadbf23e507fe4a8329acf407a6a25b68970a09d1b2e7e830f1f65d31710d4865dc28aba37d5725040ddfed575aff719006d1538fbfdaba7279d8d1fbae4aee2cf1f60d49a06e917d0c0c294edc94db76ea0482e97850994dd395324373968845ba1cc921c9e351e40dcfd73728fa73a1c779af135abb0dde8481c5e49829b5ad0c58d5c04d84578a77ab961e74d22aba43565d7fbc6293f334f2f2f82fc34ca9b5229e7976d4c86f90979b01ac93a80c862def0ffdbfc8919a20fa6ca44911228d1804bc6be01a8f482808016dd8b73b6a3f8743e4b5b37b49cbd6d64ea0619b187a0401b3af93cd97dc8805" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4c30ffce6b5194060c2a48a73fd540944a64a1046c9cc048b915327effb06244", + "proof": "ee25e242fe666fc0ce8896789e2e8cb73ae08a5a6835724ca1378d05b0d29b58c6e59edafe9b595c4f689a638793c4845c41a818f7dbe324bcd8b98f95fc455ee00fa218954f73e331fb80918f56ce80c563d1165867056491fdd2a64126a84a4cf10af0a0d31830a5f602e91dff580214003dcb501ab63988375849d5afe80f2845bb55e5f1ca17213b53e21e223cd4128a14ae2c25c74163d5c697fae0c8084d931a5c0ada0fa6663fda103bd340bf10c658fb60fd8beac9317205693e9403d5935e410f6b4d00e23b674a91c3940db735f4b78f172c66754cf8569803550e8ee8f031fa2e098651cfefc2a693051fd44812adb4f56a894a2313d2871a185bb457d88df37085af0bd1c83916ea1c96a533c080299e3d0bda116e0cd33ece1aacdefbd0e68e0d7fe93338b392d936a431bf75eaa5bc8ac57c6bd8f69c7f130a94f4d19f711d14d36dc78d75ae6a06ad92e54ea9f7ca407e8b2317eff083d000ca3a96c6e01d3f5f3713ed2accff3cd45cee74faa7ced9b6c8c50944aa78622656ad90e630d7287df21cd1a2f0201ce1d91f60794a4c4905008f59a29bd7dc22b84e2bd7557903d9646ba5e603fd511701cd95be5e750cadac251db63b165b51d2f2251c956233fa0c60151581d7c5d94b4fa2e407b8755b745843a60ec4b15934ece8e8a8f3f10d1227ed77c7adfa0de4cbbb884e75a81fca6552bcae320b710ec909ca3f22b8a8fc2915212d5d472c739f3ed574f7f0b01853560070f692305ea24bc11a64fc348f1e1266d1b195c1fe4d66f2b0458f5207e3c4e7d71da433aac64961cdd4f7a04977a17e2900099c9fdd1dff77c58b5e753a9db70fd40b77e65fb95939d4effbf62ca67a3d9137cc4aeb8e24ded767f3e8220d932849d20fed268345ddd36a6372d65e06a8b220ef2404fc4ea4ae7c107d947aaf16f47609" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68c9bcc0a60e9af1d6ef9175b4f26ad65cac56d4ba36ba1ad55447bfb1abb259", + "proof": "94a7d05edf52dbfa7a0924fcaec01c56379ebbdfbb551565679a6d8ecc9aa90d84392c2c04756cabace1281a4d8f040125e53d559cfbdb5728fff321e347eb5752bfee4f4b199cc070c250d0d7e9ddf54176fa4be51a8ef8fb013cff5b87b36cac8a0df853877d4f26f3efd743f78d01100a741bc191688bd7056580b7a12e30b90da5a1296f6f9826aef2235043b776d9f8639a5fd2cecb155ccf46f11bd50dabb1f8b315d43be20b2c6f25c1428dc702105e6a9b18fa9d6c6b56485f47ab06559b961e84a4ad38a23e58d8781f683680191ab795f75be90daa2c90a6e2e501fc71ab94396041860bab33a88d3cce3392627b1998dce477820ecd91286b9237c89450e4c45d48e9db3b0338a7668e61149d27bee5f40b8e24ae328877500816305f06836e1b28806912aa01147d09b39e1db7fe7a43019f239ddcb237d81a3a1c2a8f0a574eb63df50bc6f47b3cdd7b9fc03aa471ea284fdba10eb296b9dc17e6b83c8ac0da3b117c93dc7dd13ad12fc2d1f8410a8dc682bd6af28f179c881ea257a0b8d560298709e61be5e4cb1d5d5500509deb424dd8b928b1835899a35d00e6831306aeedd9c935967459db4c2bdfa44cec1b2c006abc5570de0e8eb01c74de5dee33e161394ff2e51d64b230db79d4d5f75aaf579860034880670bb23f62dbf3f394bb23458f3a79e71ee3b3e81952cf2a05d985dd6419cd083f37aa74dc42fd5cfbd02de76e6071b28a1c4b2679ab82fa8728d08778635a5779498c7618f80983753f21a333c276afccebefa067ee338875540f98c8710dde7d59020ffa58dd89cc7922e15f598bfafc970c342fe4c84d4ede469470793bdcc5b94835576a1f10396e2d6f5063d3505b47c2c16a13c1c6fa9f0d6d5c0bfb583f79b404ccbd4aeb4b4740ca4f23c21f2830e9d5970ea66e08d40a374f54627c5f627c05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7a362127382a1ad9ac323c02414779abe3b8cb98f7ad8a2e4665e90e8103d833", + "proof": "5430554a66ce1f61be9ce44c7c26cb324427b306a833e9db89b806058aea71280c9725e93e0c563bc0118d4031e150285c2fb90bd7f21811bc60c26408c38c106652b06dbf2104d742dee2377f10347018ea906979c55ae7811026e3275d7147387f1a141edb322b503dde1f983564e875ddbf63928487e96ceecf81f665811da51cbbc1e4761349193b16341302da04214c35e24abbb8b951d3c742bb309e06e7a63ab9d7dfa02d6e2dd3582b9bd59df5ad1f0ec86240ea79bdefffc67e410f485822461427a0f78d363b3ec8e7783ae2f8c4a16012e9c0ebd87de214b17406a852e14e7e048209b7dabe324c0a83c419437a36ba988e24eb49177ef2fc2407b4342ca880acbbcd23a886f41aa164313a241c867cdac945544a2aa2a207746d90d36a21123bdedddde659509bbed966d04c75aa9bbb03ef894071333d27d43d5678ac36ed9db47e093d1f2c1a2909f1ee3e9db6e866c561e9ab71ea03806b59bedba50ce391b8940027f3c2d572e57316239e0691c1870fcc7325168a5cfc2ecaaabaddc2332d5210d02cd46832a6bd496f9d61125643cdb95435ac07e2360eee601d57577f909a1afafe542925c9fb64e2ae1cd6e582aebc3bd37fe9b6a607c05d06b27840ef82fa1b6b97f9e78761406d23a503724147f797eb0d3ea9165eac7276656792b4f2d9e8061e97fdcd8d62d576d1dbdeee158c324465e1688b478acbd1dd8090eeee16ea9c2ea4b6a144ef311e007dcf1944cec1c73850a57922a4999054128f2d4582b8f94089be622605893110807ee249a848a98fa1516c0d5e93d5c58252d9aff2404f626b390d547508b056fa9f064c20a78f59f9a6d72c595f9bffa5ebb5a359751b0d13f0a5b68add1239f64a6f88bed688a6e1c14c09498aca058a7caaeaa974c038660088eb254b23030af3b21168a39d3f8722600d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "84d1a2e9016d13a92df5cc494b5303bfca19d4ea8fa7ef8b94f218c4f5473755", + "proof": "76a35493a282fe8cf64286ad4fda79485bbf699ccea0e41b8462355cd62f693bd6735de8d9f995bfdbd644b7898718bb19aed627ac9a813cb8649e911300431578192c9e14f5b43cc562c67d6374d4db459a4f3debb70c19534ade5a6da1d02b1c3e58710fd49b676f5220b1247c0ac9512d6446d34735cd4fa41bd92717fd10a208a0af41c98c2171493bceec5924a3125ff6bf21ca1057001618491675680e7113dfda18ca35d2e78b12c03615b9c6a3f66cc7f5876b657c8722e9bdba56082db2c863074995ff4d5f7c60e638b0d425a5ed192d1b11fe8ca8182ebfc3d30aaa35c4f2aefbde090df61d94136344676138d8c8bc2d6b72af580edd9cb67f5efc89d2b52d0ee49fbfee8824f203e3fe72536ce2feda6ffe86d333b104b47743b4a86fdcdc6f7d22b65cfbc162cf7728907f2715cc355a875ad209028cabe3694e04ab1258d65121f2751d37baccbdcf2f075dc071a6986b62400f0286e1ee76ae876ee2e8a816250bdb9415f0f3155eba3629f71fa16e171dcf3f5ac1c8ba0eaad045a5ef810dc34cafb9c748cffc2956f697b7e2dc0c093b5bd7f89ce5b36318a9b70634afee080375917e7d588d7828d1e964ce2aa25c06f0654b16c4634442bcc944cb3182df41b63cf5cd5d40d9c63685502e0b3d5e09de0403726c25640864293784ba66da45ab5d7df6d991644c89840c1ce4bad81b03ad58cfb6967862ea467e767a6c2b68f33df5858214adc4977a4617d515e5f18f8fd997be3f6c36c15dbc774b15a57c375ee63bc78e89f5db05d21607df97409967d924e1626878a810faf7b7c4d02db00bb3da5ad7a63158a2c9c2ba8dd89c20a58c4e2e9a6d692097acc10e040750c03420b43576ffcd3e0ab8ee08cc13a78bfec26ab5910d5ec444095e753cada6f49abff6364c793a7d3c56937fc167f2ef102dcef2830e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c67c51ec987c36d65de1649720ed8d01d242233c33fa4c6c7db3efdd90ac6674", + "proof": "74d0ed49c565aabe67943b00e58b717646fa4b5b1d36dd74b1c731c4312ff16962af886bc2166be4a5201f0f190d8486be671e801fd837cb42a846ac5546167b38b24088174dbe951d7605f2c3309bc8ee8b5a8134e7cfbfc3ae4864cd66734ae293786d5f597537624180fd48473a3eaf91373b40456307f63165d6566496157f464468d921eda8f826c736a45632e3756fe0c4c9eb2717c5d192548d2d190a64d4d4b52f0a154e135aa1d467613e48375dca41ab952047425d73b2ec6fb70abe1acc50292cb6f7db9ff87425d0ef8e7547e0f3a4a8f920b9f59e7a1f0c3405e89b0a6ed60a65d2cae5bfe07153835ea76e148fb2302687ef4e61babdda3218089d4f84654336bb5f728e946ce3f8a86a8e24eb9fdd9fb97b6f31b407e8f34c806591de4f603ba8b0f996b938450af2a3920269a17c5f482cc8e1711e764953980f5d2a576574004cedfc950d0e028ca5f5e0799041876923c5cff79282bd7e34157d7f1705e7b47b0a147d1789fc32255ed250dd56364b43c0cd91fb627615d410dbe8e5189baa5595a7fd0b22d989862d8bbc8d5eb6d384b27e5f8a3a9b49988510287dc9b8e01ceda2ab253b9fabfd5d1e64e4e96d9b2dae975b4d297c41f677f6b5fc1c2b9a43bbb312471f0221e75a3b56f4cb42d4868833d6b4fe3a7f56fec2f921fcc1f766cb67cb9a37184b4b90ee2603c5a4244f8ea98e16b500312adb95d58abfef2c8ebc85afd2869ff8d3220b06f97bf38b5a79bc2a86d109183e8f46a39b99884f36fccfca85893ebcd454454272727d6fafcc77ac0c8d875bbaf3c0475af63f12d52e6a81a33ce45f5f13b320ae2052fed4fe34acfb3e737b58dd9f726ab86343c28c5a935e4e3beabb104b652d401dc9ed8e4f455ab64f0dc08fe401db18818c94c0667cd375bf66c87ccc6741dfe97bf724ad942ef13e0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e2e8ae766e68d79a86f3f7dfec8d2b18623e402227a6b674a710c28fbf6b6e03", + "proof": "e4e2921473da18ec1e7bebd632df1b2516dbaf28fdab6fd2464600189e737b68eaafbc2e8d58d4a3e6d2b1c13ece2bbac717b936bbfd2ae172c5300da79aa25ab6a069d1baf3158df010c45dc6539cf23fe49589fe10aa2413c14ac72771c14aac39fa1eae551dcba3232da39862a0d0a6db90d7e67e19901876ccb42b795e131e7142f9ff992ea8f09ab84e5168d67cbd87ec5e761cb981ad72aee34b026405511879a8c691e3b76715285f18206553f19d857363357415590153f7fce61104d9961aec4465a0d314b6fb506e59c9d5507f27bfa4bedab2f1948117d0fc4900384336649e4be336143b6be9721d10a403ded013b0fe6c3c047fa8f82d4e9a63cc1708df51fa79b16b14e1266576d0715161d7802302589507370e67180cb220cae99e18ca7fe17bad6b14202bcc967e71d9dd88bfe723b274f46be299db175a4ee70b28beec70436261c396798200b6b9ed0395cf96614d1a689413d6e10b063a961eb1b13d842d3ec49a6e79250c7ecd83963f688916849e204b9f3781236af492d8f933a9377ee0bd68cec47fba7a80d01131f0f3b41b8e1318f7c2902a54da20f199601e36940550fbb1384cd1692f288867ba609cb70b5c72d73ac5735df8aa06638d996809f6f3deb6b860964c2aea493bafe61da9812b86575afc9f4d62b70786035ad63254631551faa3e39ad0988456f3c4c8f483321c17171ce420128249e119e2a7346e220c5a06e8713088f9deb5924d840577d705276e7f133cc61be4a52920ecff25753ec30b7eab51e985a13e735520b9d20f0945c5202b4eec793f7ad56418e7293a7f63c2f8523fd7ec8d2907c57978155b0441d2d67114386f5b904d0239a3f2cb8c434103d8bafabac404137d24e4b511f0235a1fc004bd7884f52787d09a7dce360c6a0eb13a3a9c150d3f5304031c983eafe5243309" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ee76423d87964f6fcbea0a74708e736360be916179fcbb0e0665f56e1808de27", + "proof": "9a68f947d8f1400a486e5ba13e834a0abe446c4efb7fbcbd73f068e4c2cfa53204612efe2edad8dcf2a389b9b9ade1458f8ed0ccfcae4ec62b78f1544eec6f60bc6056d4427f6f0adaad0f4977f4c1b1cec0f827e19b54c52159bfcb40e50340d2b053449ac08090536d4e08aee62a5731519a2f4e3d42373f8b641765460f4ce9bf1c0adbc902dcf0b0c0ad7733d490e6c6d526834419ed15973c1b37b80f0a161ec80794bf0515dfbd39de30ef5fe63be820a50fd4c18a473e7f4e4de63d034a60711015c592aa1207d1024f9cf0187cdc897b85f9be6c7b11cb5120b8150e6e4725befeab1a0b6100772e34c16578ed6dcb582c0eee59ab0d996581d7dc2224feae6a6b8b29c1061eff1109b2d01863518f570e435d112c159e78c7ed033910f1b1a8176cbe3113918e16b2c579545fcaedcf82723051c7c35a51d5fa6b755477392470d03783194e504522362407dff66d291d3668770751655509a7d07804e0b5514a9de26124365592912920d337e79212709fc8e578230d8e2257166f32416f12934f279944e73e1a27c452e3b4829308f6cc9334e3a812c5a374210674e54092593cdaa0e1bf4c6abb7fa963a5de749617c344dd2c21bcaaeddcb73ddc0785fb68016a6078752729748852ea4d237b56bfb00eb94a9bcf4c4acd2d1b5aa3d22c3aad7ecba42cbeb770c11ea48d3dd3df7d7df5707fbc32312e8a780a8c0e821e0e089ddb69a40fe4a038015fb733c202de73d4fa3ac145fd429fef36ca09ddc58ef694a3f8f2a7a060309b6c5808aeee6217852ab1e8045478e8b5791e17e589aebb1431147e5626ca8e1caca48be6d29c2d94ba92f72c92ec06a4443a6f52e59699e5a430d12b24f3276cc8dac022fb70fca68242e3c2b40557d702ec1365422f447fcce89fbdf61495361fb9afcf90ed0f6d90aca18e3e65488207" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 23 + }, + "commitment": "5081312e47ef838bd52100e61a9307ba1c7272e3ab4c4905b9d5bb9b4d8e5674", + "proof": "988d78bd0fd4dd71258715bcde3f39452beb7a7f4d53147ba70480f8c881ed192c076f13397e89fa3b333862f1b44b7a7a469bae1b2b8cd8c4e236977ef8912b44860a65ef079a7806cdb39f245a05950dd1ea9ebb18d986a9e97c1ee6233923c01914c549fa12b80272dbb26fe9e7a1dc8d75dbd9fe2117524fb48248a14831e9725d5e1ce9ce5313dd8f9547c45365799d64dca870cd22ccb3701acb11e40d5e69833905e34282b92d61bd805bbb557a331107eb938baafb7e419b53138e02e4a36347e0174a3e1ab3be966e304b3b9044013a8726d391ad1feed66f0ee003e2eb77c3e529f1274a667f0375d78d21dbe9072ee08f5c9b20d08f49f4856b2fe8c42612094318a3ca6053027b26e3e1ffc8646696ecfe59e8820b04ed23f429ca6615b485fe77a0783d46fede579c807c7447fa6e66b40628da2508839db42c68b13376bd868ef18fabe6ea27d70ef68fe7e5103b8b9ce9592a3de8e940245ea6df8c165f87663f8617b17da07109b9d58156a6c9a0e432ff9114eaeee4885762d24eb96dc84af61b7764a43d8306081da10660ff93195947df7e7e14725f3e4ea999ce5a72a394cd9108ada102ba2e745aa6cd89019d71a141cc047195447b526db6c725672002302e92d332406fc9c4a687e7daf9ea0f79bbe61a88b8dc7314cec1f544179a52ecd3b186b3e43fd8bedc5f401a34da52d2efb4d7489d6750763e7afc11e8ef30a7d402a64119ee853c3cae2bc4826d27b85f59a63888e52890e73d21bbb618b171d9df58f6131a4d1ea8388c58ddd41383d1db31ca8b246fbcb3fc1b32b53e18058abab391bf35bdecfc92f46b087cd970ead3e2f2901019224ed513a2a10fd42af05e4007d80ac5d3f496d6ab24517f441d7369482e0c0973497762fdcb6a1b609f97668195b3decc05f41d034ad4dd3c764d45d903b50b" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "14881292781b577fc59621f83b6dd068e98dd5de5a24a6c8b371158c7735cb59", + "excess_sig": { + "public_nonce": "8c9d3eccf677c4a8fa690022511405215f390e59d90b86fc95adf657f7106b13", + "signature": "9337a75243ff7e715ad52b1055743ecf4e23c25cdbef96055ad1907c6ed7da04" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "3c0cb447feaaf5f53d736a2898b64f5c2ba25c7a781ab2b840ce303701f3842f", + "excess_sig": { + "public_nonce": "aa3dea4d761ee6afb5071a026754c4c5b70e7d0dce9c7e4238fc9e2dccd3b123", + "signature": "518f8570704ce128001e22c709e71ca0fca3c3e85f73729b61f8e572f5407103" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "a2ae43f59a61f52cbc4ff427452c1220537d10b1310c5e16082d1bf8e8a13f4a", + "excess_sig": { + "public_nonce": "e2d1f04c9e8828b9b8e4918a0169f15f09982a8627073f7db9640f258ab0531b", + "signature": "d0ce2661712ce171a12186aab359ab48b767d84c09396ded03efff8931816b01" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "def3281281e295511c24ff55df44407add13ad334e48f5a91e532f02eefe0f32", + "excess_sig": { + "public_nonce": "be15ff4e5104d35ec88f39f5ce5755ee0c92e2b192fda6c0adbf140d9a25f323", + "signature": "b7d47c7a89fbbab6288074d9803a95b10ad1b317160c338e6024910ced180305" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "e6437fc378dc6562fe14266003cff9ac153a6774aaf89eb47842dec6884b696f", + "excess_sig": { + "public_nonce": "bec096bafb4845d2d4611c9c7b95464925c48e54e788bbf343ee1afce9c6bc0f", + "signature": "238d981a8df2c87b0d093f2217cd3806b50247782d245ba4f88b9aff9c04940c" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "e2452887318e659c4bbe629b03cd98850594485374f9cd7b0b984920fb409d69", + "excess_sig": { + "public_nonce": "ec99523410aeeffe414878c320c3db1806954c140896fa76ae7e1085d065aa5a", + "signature": "543c16da1c07e2234bf4fb74688b85e0153b963c9ff8463786afd0f495f85702" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 23, + "prev_hash": "ec9f92b796fdc570c9cf5afb91e291de2e1754fe04606fcc27dad82cc680b178", + "timestamp": "2000-01-01T01:24:01Z", + "output_mr": "5079dd63dc30a7d32eb8a4e75538a091eeadad8a07986d419ffac374ed86eb45", + "range_proof_mr": "aff89af8f10272faae5f3a7bdadcc970cb5e9a12f64608aff4f25a1797b06a7a", + "kernel_mr": "cdfa027ccf1eb2a0373d76f0058c87aea56c91e5165faac258d8392a5876dcf1", + "total_kernel_offset": "532b3a69a5ded22312f441cbdfd2cc2acd926b9c599bdab75621dafef0918005", + "pow": { + "work": 23 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2ae0a1a972816d63294f2e8e43bb543825e4c0415c14bd8612e5c4b0c400fc3c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68c9bcc0a60e9af1d6ef9175b4f26ad65cac56d4ba36ba1ad55447bfb1abb259" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7a362127382a1ad9ac323c02414779abe3b8cb98f7ad8a2e4665e90e8103d833" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c67c51ec987c36d65de1649720ed8d01d242233c33fa4c6c7db3efdd90ac6674" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 12 + }, + "commitment": "1e44f23761abe164ee449bd913f95d42e61a55c151e283483ab6e18c3c60f17d" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0eba3f3363d3dfe9209ba52e386a925c1d070d2350fefc17273d04b46faef579", + "proof": "ba49d99314bcba71b0666af80db62caa7a006c458b574cfb5bcb92e19c028a5f7ee8e5f02d7dc238ead6a4960f8cbcaa05eaf7d200386a71836061e9379b4b4bf845010c6cc6bf98d87463bad580bd51044a1c4fc39f80bddde8faf459789d36d058097a6ab9a7948984de7c07bc557c57c553d8a3a91127e5ef8e86b3c6dc088e28a1a7135eb46f37d953c8d5ad46c838a869d494f4f87ef8df1e337eaab9047297526ddc16d265ee1370543c965a3e62743cdf6cfa2fc7052c12a531e4ac08f4f178f8b819a515946ab1f7e43430b6168ad644c2166eb7f004ad96d6e9ae06ca1ffbc5d550dd5656c3bc0daed5810adc4caa8386953eafebfe75204874d554528350bfc34f9f45a667ae714fc01be9d8490370eb1ae0b1d411c708b41fda303ace58db4c0a0fd9a7e1142f3a0f56184d40aacdbdb83ff9dfc2c74cec0ed07196dd448350613da1b1c601d91713f9e589ba094a0161181d59c069ac37a0be541618cee1ce5e32dcaaeea21a8d86a47cadd3c804bff21c5c71e3aef60c95af1ea23d601c841b32d4e86e03fbe2ec44fd29771ecd07d5a2be8be788a3889bcb318cc0c222318bb89ff7cf5f5108c68a1fc276fa48bd56e34ed66a70347e7fac48869023c649ea00339eca8b1a8a3ae8d2a48cb0339d53cf7f5ace5a5e3b1fad5d1265b43149d97a80647a1f9ea3c0840be2f32bfe4513b3bbc8c4a74416c400543ed5b706c1b6c85e3cd0750354f27425ae99a00a021bc4a5009277ec3c03a9512827bee697b182007325d0821f8041028de115c601c1bc43fddb86e94d8a206aa668a66b7ed625f1a5515b9bed25aa344a56f4245489e150561601c777a9535e79f497881ea26a27b3e37fd4900c539b485dc44b0523c63a763c3f3e9b50d302ab78365e791beec94bccf0e3b446fb04f750a678e7e8c10c7fab27b8a4204e0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4ae02a537b8432c98ce8d32a5abf2eee7f4ae8f8067523c9bb88aecc0f61d634", + "proof": "403e454fc86f2d423633fa08509d40e38b3ed30642bccba740bf65a861d34653e4f17bbe571e960f82b3d03c02655d375246482a5302ed9f77ab06888cbda75c7e46aad8bff61df266516e9e800ca91171cfbbde8662f9c4d6199ff464b56457a6c365f4d873382db1cd5f9cb50c08a02825e9ead7fda8a74ea6add92215db11330410360c8d44c09a5bc9ef1a08d1c2e43f6db787b0432e1db4bda7118d180f28b1d526ccaac3dcb6d46225c6a5dda436a7c6a25776bf8b99bd199d153a0b0d0daa72635dc3799304c5bcd6a28422b30ea927f4e53bb8200e2f00036b20bf0a5a0619aa70b496b82d88ec4b3407398768c078224b224bf527189f5a23c6a707e8a57800a4eee9227821ede4efdeb385712ca440ef87db06b16ae01915885b386eb716d78a782b241157722435ed7d4ff7ad11d2e243db903565b4ec8d94c537e83fe72c7cf0c1d1ef8e9c9cbbe438e196c786e620b4ff5ebc341f58be01be128a27ae4462cd69134b65f261b4ee01d050eb33d75c7ababb1408950305086e40066339c142db94ef9419ef0fa6e2fb1a06569ba630aeacae50723c90e8f2c0199a41b2017eb9793f48b9e95cd70ffdf0fe3a635ac0dc742e87c38dc20894d97e9a0411396dd864814c7e53b3d1fe4962a1e80f9f02f152c92bd91f25db4ab760622b3701ecc68e357c012eaaaa917004cc78ef385bcd525d2600bd6cac7ebc0fe84ee4abf26f80dafd9cd1bf3a3cf94e61557e64d8fff08035b21749e2976c682cb1347c7538caba4f9526aca3a46bc47f790012b3a504aa7bcd557eca114b6540d6b1ce84d4160f15b3b429a37d3d534a28f91662cfe49fdd2440e4d47cc746a9cccf7c9e96c410cdd6966a101c18eadb80df67cd5a1121cb7453dc9c4376073a3e3dcf9f25e9eedcbbd9922cfb980bbe2bb56165ee3e9326c92d82d6ccff07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5cc6b3b89d121cf06d45ad4353800b5afd25e7551415a1eb4887ddd180108e3f", + "proof": "925accd6a97c3e351fef9234673cd27fb32772994ad82da87c3b54ca3ec2f0664a542d6417d35c33953b5ed781de59f1709eab7a1db509d544ae4013c3a4966d9cb588411d1f5ef45a24996c727267ec79e3e05b9f6e3390ce4e95b71d535d7b26c1c0ef713987531abcb8cadae4c261928f831d4015d00e07b78c85e020023cab36e1e18ef0e75b2a43a1688288b38228b202e8a851781c194737a076c75c0535d908a200f9fdcce87648300fe46a7a24764a902ae98790eeb2278b2cb0ce0c33ab752047cf64e700dc758cff53d8ad175d798389a20c6c07068a715f75ff0ddc653b17cbda682435cdedb7df27a37f81354f9b8e59026aff64a84af423b1238420edf2c2096b2ed66d655d53b1deb3e5e8b69eca8616b076bfad76ba7cbf67982b7d8989f6780be7e44c1ed3b132a79fc9e552cf6fddd0ff2cc181aa8f8b0cc686e8f5479ade11b432f945a59c45decc58a4e21eab6af0cff6d57f2a96911310501cd7e348997b2d8c86a3ed3eb54744f7ba18327c19d2d6136901d7fa4d503c9098dced9ccdc4b7092654d521f474165e91e95b7098bbb4be624ca696a85d302b3487418ca0fcaca7f6cad1d5501655971549425b1a9bbd6918d96db877365e9fbfa34087aa50d3cc701d89b2f4f228bd263f27e7ac1081ba35095d65d61a4a5e86939e4903b233eafce36560bffcb983e6c04e591ffd1881bac5bee83d797426f95a50ca31fa4ef60fbeb3ae955c2759edab34ed69319086f6c2720ea0505445d66549fba0e04eeccbec646a8429946144fd57a5fdea44ec44d92ad651139a8e6c20cfae7917a55ac3342b38349483db60bd143edfa29d7a079845f8410830649356889fa7405b653b474844646efa55af2cd498d2ef12165b2af1a3d1040d4c18f0f0af7eae5d9f4ea275676d94c8369e37d46d997b0a0f0a2e260a9f02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "76ecdd92a79d14763c3a96b5d17b8db110af8dc5fdf967bbef024975f9525309", + "proof": "4a0d6798f95c108ca65d9b4b05e866dae26568f469c75adbebbe02ecf3f2117966080751e5c8289dbf9c570db08e079f5fd59f986c7adee572a9b06c0ebd8473ccf2ecbf318b1744989ae5fd4eda686c3856789e2c88e3c540b98fe2953963576edec5a2d81514120dcec0116f5374606d47730e86205d1e20362d2b53e312520de93bcf359613d104a1a84ed8964a0f34b618c343d5164dba06d9856cd5ad02a5ff4a06f0d9764fd0c6ed278b72ac7f8328e7dec120a85a43341a6da65d4b022e2519ccf0cae5d4a57d9dd029533a936d82f5a643b9e1f98e5eec4bf1eea105d893cb45e778b2f6aa386f00deddb50d718748a239826de8525ff167b45ffe08505feb5d295f309edf0cca8af293b2a8af5d683303be9a23ce6fa179c67ad9688acfb462b67389305e74b47b2a8f1ddf662375c729a23d3ab56c1f21dbd01b04142c0e1d876ca4bc3a57a97bc2152870b88ea164b97a2147197e162c94df627496952628592d8ea64903323aa5d25350002150db6baedab9f97e10bd46dfaa506c9889d4c2045055d1f112d83254f2d357b10be428842dcc1fee9eb8b7b38f6408bdce1c5d592ee09750416febafecaac38c030417bcc05ff72718f6088d5d7e34b0919d26dcddac7a6ae5862c94b6ea699525f76244ed663ea2447b29f3790744af66f60f68bec5a11300b005cd8f5a5688a12ec4589d2c91d417815d4ccf4dde6ef8151c6236ab4a2cd3f5ebae01a09642c4214f8670931e80a68154b8262de4f8c2275b213a4aca5edfe433a02e1ba636751b523d4025bb1604af1185d250b84283bc064aed616fc5b12ee424a59d23710164060f299fe4cdaedadd5c8e52d62ed9fba26d403968dc70fe259ab51dd115e93cec6e71b22f5383354d711a0705ca783f96690d05a8afde6c6774764777606b704977249f7d84f27ca84f4105" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "aa7b597176e9fb487c85d9fddcfaee16f6e3bf126fc6f4bd9a763e6103722e12", + "proof": "3455e7de23b494e87b48b9592a0235c700b4797cabaa45906321080edee338644a050aaf41b9ebcdbba127300e86ef910c51bc03e1652b78a3d2d5c2fd195e25f81df97d4693a12b11637fd88edcbccd21538e5856350e1e83ec941c69667a34823b19a21c6042494ef710dce2e777f0635af8697770c30c286ecaea2292bd2b9ffbf9a2ee6ae8ea30f00acc18ad4cf488ae1714fa01358118f919fa52382f0a64542a6bc21b506ee2e54cb5d930689964d1ee7c0b1d701dac62dac6fd561d0f022967c6c7386043f969cfba74081977cde1860437f3079f11925e8dec1906023e6e3b4d70a4e0764de3c05c5de394d7fe0c48a4f6a766c2ac9398de34824503e04abf454918b2a37c21d194beb880052011b5357cb3e3f3da38cc53d150950840620746030d291158b6f00e6bfc4d350bd321a6d0cc5bfb3481e8bb5e71542610785465b599b9b10eed78a1833955e7ed42144236360fa337a26e91486f7b4bce92d4365d6fb3f84ea6d64daec5b587d831020c80879619e8c56d1569cc43361c1fe9bd4c6d271ed34d240ce37b1bccc152b8bf3c71332143f2e3e9b405654eba967f2dbaa0fed975632039a830498a66ca2782bd13fb210ef163b3d835db6b6ef76321fa2fef78045d18ab9aa63fa1864cb133020356148ac154223723077870d36934a46fb87f825e87618a8d74092b9feaedeaade00b0faf9c450f2bbd408c20d7767ae665a0e2c6605e77b6fcf86042305028a14cb7c11996dd62b6346a3202a2ebc1c7ef1376ead02c1098798222ab278b0b2000b700f2563255dee333b83c6c571c48180be278e4cd3a38f3f0938fdcf2c5ce9cc85497268f601f9402bf50ee647b25695483d235594bab1e16d2c3649cd287bcb12201c1f92c7120024106bd9654c1af137f412f83055a4138d01b4f3db2f9d1076c73b82f1eb5a100" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b03a9a9a00161c58824dfa531bb00d174a961d5d0e501da994e97243026e6c46", + "proof": "2c31fd9695d57e4bcb05f8bd11ca8e14952824050b17c56edc5bfb2e7e200574e432a76b05f075e26b69dd3a0bbf3c2e5f4ba29a239ace373e6803a5444a5856526104fa8498b9093a9716a84213e18852358afd229dcd574bd5350a2eda9a0d9ef9be41c69c0f15f23273df5f2cc34e13e421e30c69ef775a01f6267951364cbabf11dff7c46f69dc974c805933d0e48dd6e1e9dccb3fa5bee2246b326afc092cd21498e4133f5881b66b77dca7699fdb0fff2785dab1f476b613412e7a4e0f0d9b2ebabc7fa039557a96e3a9272944309600d4ace316fddc3290f6ebbe690e284ba8de720f4edf78efefdfb1e43a3cf4e03f2342fb3f10d57f76dde4f298297e0ab9835f0d0932dac817f46e28a2f528eb6930b29ce3c84591c74f0ace5f046a9668ef99eb4a234b55de8a707b5aee08f929db465078e983d2d041dd1d181ac2961089d67a1b4c30644ed64db61d5f00d02a24f3d591046acbaf0e872a88002af69c0f3968396e68e8c0d281af58aeea862839df200d425ef536825403476d92982483824d9bf373b086f75ca20ac1e3b1dbecc88c1abf155897d8d22b7f66c4a04a415d99b750835bd20f6ced47f3fac4403b8bbd260988b9c172d2703e790807109a5b477e6bc2d1abcee8d9e86737ba81c2eba060390236e2ff6ec7e404ccd6998ce59176a805f2e768de17c3a80a257f7cbe39177b02b514398a0fd65fd65bf0ad7aa1970e1dc68a1ab4c880b5ae3eaa7dbaade3fcb902c1189834d81b987f2fab2c090616b7ee41ed064e3ff4466604439dfe082a46dc95e2d3b22f2c6ea6779c9f2014efedad151676989d52a2f74164797246fe9b61036bc974e214b491e13f65fc6bb823ae8745379954fbba38afe8348414754dac72c47e426209c7f19c27020d6554a3102abe6098814fff6f33f77da4553ba45b19abecb40308" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b499ae6836f019bad720450ac1d2c16b5a15dadc23e64116ce550b851f55da50", + "proof": "9489ac08ab358a36015296cc8738fc1df0b537db27df54385c94741ece5ed210965c59b2e0861417ce9f33f99dd89cb871c7936bd17bf2bccf4e377da1096f62cc39a2e454f650e36774ed029b5ce493676c2c335c9dc3779e7d3d9742f7950e12dc09eec6932435fa91e6a790ec1bcc5ee045551a96467ac372061c07ee6b2f65885b21ded17ddda885261370b07081f0a254a0dcc19263cc080a42a20ae7027fd7cc9e4b9d85f6a9bf9c1ba624b23cf25809dfb9bbab8b320c26b468fae103495884399587baf37592d43c6fd16a30847955c3ef8957e3d6e5558d0045590416c92b53dbb8d12a42a93f7dd612e24faac8b046f15408223fb61c07ff1b4c73941418ef22a52aaf1de0929b00b9ec23e1b1e417aed0028610b6b512a030250b28f677f15a97216dae5994e2d07d57d62aaa44537955b59dd052707a3000e469b671eca64684a494c03713765982948da0f6758b0d4abf31faaf6178491a2e0426227247ebc89636a67b27bb659c2e6cde6cbe721c1f6d402ddc8707e48bf1421c13aa1a5433cddc5ef903b51fdf08436a49df850cb717c705552aa9257cb6346a76bae216ac49273f336e7d74d9166df70c3ef4460f947971c83171f3e29213380c7d6142fd6b5cee201afc6b63ae297b15552c63f2e55e2eea4570c980333afc946ea5312497978c0bc85c86d3e325553ae34aa3a866ef0a07741dcfbc5566742c2a84382c8a46f4ced33250a677264d87bed244c4142206f9e4f6c671b336a087f7e988fecedf37612458f32effe31ef5cf131edb0950e98f9355859162381683d6cf46acbe189a0e387dc8da9a04c38736a851949bf8e06a96b51d81b35b785042e847e93645209fa018fa925848aaadd301b4ed18e83cbf44f30067ae01c503b702a1e74dfc4383450c56cb573efbe28b3e2c9496928c0a77d032d74c04" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c6a45033997beb664a90d59ae9e0f97db85741f67f677b932a45a55c9099b220", + "proof": "b6224c74330aa97929b1928917650e99a859ee98142a2d0f6d8fa9034cbad429304330b033e6a0b4b02f56a4a5a171498509131f3f8fcf3ea634212e5cd5ed6542c127576ba054509598cf329dba9015240d57c14fc632b5c8682936d66659627a3cf84886f2d522d3a1953db72cbd6183fa4c39d748cc27f0e34b221da6c62e75bafa766db2d6945866258dfc9b7d2f45c840d1a35fd71ddbb57b6267f3760c15a5f5fb756f0f062e64271559272d2a7a06662148cdce2feda6fbaa019b5a00af197d404d350a11038e6992bfe2cb25fbc225d913c25417f4036f1d86097c0b0478cc5f86ac06220d79168f07c224f53272615f21d3c9e68e67c947eb1f83461048b09c24f6433f018074910f3e796ebe5ccd3553aadbd011fb2bf98cd4d54618501d060031c92ec16a72ff7a2fedf183bc9864547cd862e2e3bd7104b8f328e43c0dba5d99cd175be4d36a7ca2dfc40435840216c3c0e2b9cab8befcaaa76ac638521ee8eb0e27056e3634e4abe6b2562c851ca19d0085ca347d2a8c234122243082dd3a303c9e5483b44243479f61f26656f3fb74cec8b37803c70a9f0108284283cebd98d2b344a8376d92f07492698aef8d3579d1c1e71e93820d4491492acd1cdb2c5acf1e7e3db43529fab87d1bd95bdff19e67d25f154602b66cf242367fc4f408a88193bc33e6915161e0574039b4bd5588e77ad38407ad14c3a6029cf2e0988b904b28bc199fc57f2f2ba55467b2cd4099d33506983b4fc99e4e6f3426ab5d424f92cafdb7bbeb3d35f325d9df3bb37a50aa4c1c8e56363a682614ea626d562b02be5fc5dff1961bfccd5e0e4de8cbba7382d63e6fde83b086717d9409d7d48ceab2ebaa776cc709dec97021a913d0ca7c810bb1d2e29000cf5602587a8254c37bd058ab3db451298476ed9db8266297e09f36174b67d16a0f6b00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d249f6f3b42c676658c03372834707dd4f8317c3591ee8072bcb1d3dd5f7112d", + "proof": "7031734b1c7663de05e9fc1be940d065bcf883a47dc72e5ff1e807de2fab035ff4656436cb85fb5aa023d89c1f66dc7870bb358384299841e7db9c1766a7372cdcf26158688e0ce430709729a663e3420ba0d13a84c16b305820ae8349e571649a3d17f31c1d9418c060441d4b959da3fa17649650ad61a8a4dd5b26fcee354c9b662b1fb60137bb9953d389a31ca220119db443e3d8e52612821e0a20c0da0d1f5309e2dd7bbab01c7bd84d957d980606c4eec5f209549cc4538e9387f2960e42d8f63313140a8605a8ffd738e3ae4e6e71b727d7c965156b38a717773ea2059ec2276c385e279b2b2e2419c8cdcc6bd24a958cf3b3761442f6ac0a4cd916520aca05158e884b1cc33f2cc4735f2670c9b9c71dc979e732b642f7c64093b5221a878202361836dabbb0d2c68461501c4a4228c275cae5cc5a2cd832e89aee38826fe79a167385f5cd6014b464020ec572834a5722735c50933af3b47f59f76a68f30e38079938007366165588e603c31b7d95b30a1e5515dadcbf0a1597ba5704b79b19db4a206bf8e06f4313e0bbe0c3e88d3e91bdc7fbe134b0deec82f13d2a2c5c5a8435a6e603e3d3b83dd0a3e985c7aaf4fa16128f7f6357f1d321e363888b6626fac2f07ed5e05d6f7db35060ccfe5acc1a94ecc7d32cc0ce7e18f94aac4a9171e462b8a34d0fa60dde00ac44a03b447a2176916196e36dbbd2b15377fa1a3f5c1758e476b5483ee76bdd9731f5b66b703ce21e3b573f6369324f036f04dd8dcb7ad43ade720593aef8bf57efb832a318317e26aee3367346ea2d035736e797671c51d7c2f6ff856588405386d773eb9c51a9ad13e9f2c54139d4b144ba31e424a4519ff1ebd8807e46acd9b04520198f74ca52f2e79045eb78f505061b77093df2dc9f84525fba8ba77acfdd779e4d0983c93e89d89772c17001db0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "dcc3acf86b0562a3faa6f6436bb3d7237ce5797b3c84b0f8ab4b7642bea15e1a", + "proof": "44f1a7b18b8846323f4d03ecd136062a52b57052786f4222f3fffc265b5a5b2acc0005405f50f8ed7da188cbb96835ffe5cc4e7345f352cf9d7446547479e6609a25f970f9e5cd32211991dccf952a673ed818cf84f816e4bebc1a40d68c0c03a033d8d8533050b2c12f60762b8f4adb808b91226c04747cd67a1301a6705e224269db0789350983eec23d1394f13b00564cdfec0e2cd90909a9d79006bd4a01e2d760684703560b5a6f8339768b1769d877231ae7ef50b5760b8933aae3310869a52f239700bfaafe287d135dd4ed71c05c53b781f10e646c205e0ccb585c08b63e7fbf6070b15df3329253f07ca84ce9af8ebe3ff1dd13eb7486a55fc07d3458fc3ea8d6472ec88b5bda592a72937e355115683dc7a08fbf364b3b623a6554e07f90f2d3826b78a6ef3244b634faec73eac1d51a6e231603a91f07e6e5ff75401a3d11aa8fcb48158ef31aeafeaa7fcc43d48ac01d490045bb355ef68c193d24944ae324c178b0b9f9f94f9981d078ea1de04471b364ef0e4a1928baca2a2f484731f761b48d32da6e9692142426014c8233e80aedf85db7c35d09920bcf6a3c8ac480575a95897fdc95be9168ac9d5e01f7b06c10e17ed73efa2631b55079d0c5c45da45c9e4e0ce1a55d7457fa76876aadbc57ec387b95d3b4b901b6ec74daa28f6f8a7dfd1689b6491e01e250eaf417292ac4cf95a480427e63e1d71f70c0aae8745bc377dfd168184937675874bbaf1efa3f897f9894cc12bb6ad4c70ec69267f84159edb993a830d672e7f50e1f70e90c1f4e615cd71b9f2b6735d447382fa44d7ea13b78ae1c64265ceae13182d27fc8ce329459c46ef6ce563e90554b26538b1e2e664a5fdaab14093a0225cfa9a0c2bf6655a69c4a40239f91ed0852f3083d0d3e3e83611f0fe024cf882123d5196fce020a34e3e487f7d6dcc902" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 24 + }, + "commitment": "f481bf13c5263ddb2b2fd61fa0f884a3c5d646790791ef886d4aacae3d5e7f42", + "proof": "de9209488064a20c0b48ce6334b8e04c523397bafacdac27f2ce8f620d4d96419046330949be26d1469c38b83cbc6c1988b3462902c694f137c08eafdd7d5e621e800c7ff154190fd7fdc1f7476773d3617078aa6ff06a35e14572aa5086776efc7bd172fefc1638127c2fefd37756df662433f2cd64f50d70ae0d8d2ed45b07e0ebf980252e45edb4c39da40c2608d5400ab71698950e35b95a0cd791310f0e28189c6520dcbd76db1973d5cab95219c0c5efefdebc4aa5ccf61f887c2645031386fc56c6053c537f6a88c4b92526a616a409bc7e835157b2d8b292b2e15501c6fcb80d7c452bd4da23fd7a05c5ef8ef7695bbfbea888466db5e3e2dfaf3c637e9f3160ea6fffa8f40650b4c83586d7b24b148c6e5622d2e69836a8695f6d3530db64585745b3e0e2dd692166f39e9e2a0eef07e75ef2245e8e293d437f3e47bea752d59e09530a0649f3411795342d5cffa4772276b819fe78539a2e979873b6c29459fff1e9d6dad079d588482da087c5986985dcb601f872b52deecf7177c23f033d04180182ca944e1434957028365348cafa011339628374e48c4623375c010719f00db3d1a68afd75e8ea54d35064300c2a974eeeb20cda1fe14ee958f0e7375dadf8251da8aa2aad234c10170b358bda9025de22e83c32f7db1192046eb6c5a24a82133a086c03a977ee7fd121011ac5aaea8077e7c2f77392e9d829905d9b55cbe472468de6bdded3a9fe1df13789717d3b73b2b9d9c093096d577ef41b7decc28f9f464ae7790d0d2b8c2f811003155fbb4103404d76860d80a537600724cc145ef8a3fc6a6253c15bf2c8009abac4f962d9f05bf2fdb5fddc011b73e7c92816234fa9d4fb83436459864017935f17e74802825cc05be1d2d35503d5c9d1c7ebe733683b6dcaa908a9a37e0fdf96750c052733f03fccdebd36eb03" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "0c4792fa6f4563aa62c9d602228b7e2eac89f6484f7ec9036dd1835e78fe0a28", + "excess_sig": { + "public_nonce": "d2141aaf009dc911fb93944d445a7f46e4f3d1aa09d8050ab6419e97f4ddbd2b", + "signature": "0875b3aec06b1c3b4a2c88336627d0d2b73b5ab4fb36944524b4b703d3d73c06" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "209252dca08cb170f0491a3bb7799526b7596f67bf26beeb93839993d14d6e5d", + "excess_sig": { + "public_nonce": "0ac8234b819a918b6d558fe8c80cbed62542dfe38e15c3b552f74be0516f0b55", + "signature": "68d9c836225dc0c03efe32c4c83ab829da8e944c89c7cc31cfb3bed08048860f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "9e9885f1522e3929fe3b854991d6b4b4b261d4944c830e7cd90359f05c9e6703", + "excess_sig": { + "public_nonce": "d0ac6f7567ecb57d2f4b667b55a8da52d2f5183613d368ad986013ebc862ea48", + "signature": "7717f34e406aa7bb6afce20fed1cd5e4738c02bb74b8bb53b1aa9b1fb30b9c01" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "b0239f3fa1f7816509484f555ed04cddffad93338ff63ff6cff2455bf7e17212", + "excess_sig": { + "public_nonce": "9492b0c2532a6bc3fe0e1fe121d05e6f44a0e5d72d2c373ff33ec85aa2d92066", + "signature": "9d5bf0c68dcceda04cfa910fd54da4cb8dadb4a12d71c45e03299dddbdd79c06" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ecc8a071fd55e1126f26245aacfd9bdf0fa765ae5ca98c3d47ea0bdd093f1220", + "excess_sig": { + "public_nonce": "429f852caf15b8c74480ec87bfc9391acdfbfd7cef3f0f8df306f4bd05d42f3c", + "signature": "d23a8c69ec757e1c3d6020f4a664f47991c2db2f7940469e210620d6774c2203" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "6877a625ae6f7efcd51d79d79ca60d7a7ca35f577fc5dc326dbb77b5d259cb22", + "excess_sig": { + "public_nonce": "30f4bb84850926e82ad89a94d45e467a4d0a1896c0b457ebb62562738da8772e", + "signature": "79e1a12c2af7b5162edb6f92c3bcc930a21102524a23151f1408187a48f6bf06" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 24, + "prev_hash": "118ef404918ea0bd0155809c278db269494e1a83d591a7c83cd032e385d2a252", + "timestamp": "2000-01-01T01:25:01Z", + "output_mr": "996f4471b16ebed21201af6c629062570273786464831a3f184b6741b9f05627", + "range_proof_mr": "f412cf1969f1971b496216f1f20a39ade09f342d8f1790a759befca197a9f41b", + "kernel_mr": "5d5dc2134f367bafc8261220a83035a2b059c1fcf14c7c46d84b02591193329e", + "total_kernel_offset": "54a727160653c36df0e1226eec02885dc7f3c6cadc63ecb4880c9774b4a16101", + "pow": { + "work": 24 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0eba3f3363d3dfe9209ba52e386a925c1d070d2350fefc17273d04b46faef579" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4ae02a537b8432c98ce8d32a5abf2eee7f4ae8f8067523c9bb88aecc0f61d634" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c6a45033997beb664a90d59ae9e0f97db85741f67f677b932a45a55c9099b220" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d249f6f3b42c676658c03372834707dd4f8317c3591ee8072bcb1d3dd5f7112d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "dcc3acf86b0562a3faa6f6436bb3d7237ce5797b3c84b0f8ab4b7642bea15e1a" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "34330fe2dc78cf6ed8f2e6493e63fab638ed1de7b422c1c19f7ffd29747edd08", + "proof": "1af8bdc39c2b9ed4523330c24b75c708831d09c7d29a667d2b2fca5405fbf552965013730a813e4543a8a2c35657e9b93ed7c241d86d5518faa05b4598dc310aa80dec836e0ef3947446a5f635ba68443a58c6ac5fe2fa558c5ac0fb02c4a051743ab4803438ef15efab3640669aac40049d8943fe481ea27cebf95e8bcbe56acb962cfa296e9ec392b4313c3cb5b4a1438dc4d79739698d3e23b5f57533570f2b2c6c1aa134b5a72079b59d3b1bd6a6aa541988c19ea681786e19627516f50b477f0700c4255e2589443b647fff85483903051137c1e9e4d2e620243ce45c029095ff8a29c6ebd26657bd3897dd0d2c298af610927564b68a79f5d21b609314947d798aa502234f3756285567d8a9acf2aa177304394227a494f11544304b378c9800d3e402e05109fbec94e0dd2aafd7bfdf47d65dd85b6bc3f46128eb87767668e98c460c596ece8eb8e998113015b86f3c33a60d683628b89c5d11e7db11b25071356c648cfba3c10a3f0f25f7b74bb2b15b3aa7424c32341b5e64345565c07a32ebb3b5e8f1101223ee4ca02f7d12456233fff1df839c1931bdff6e4652a8e3a035808f0422b206000287598c705b8fb449aacaad4570dbb484faa4f733826c2668d379fcce0026ef47f19cd5233c0b468e1c0b74ebfdb770872e9272508cdf8b0d7b200d0a40fc64f22fa9b9b16a8f7831850af8c10466343cb566d573665803e0c51eebe40e5ffe39ae8fe08923fa0cbdfad6a2a5bb27ea7d8c31ed1b18e7a4c58e97064adfb152bf35e8240a47c78b4c2f05d317bc265b54a4d51307787f4f06735b7515dc4d97e250456b86e9358fdc865e9d5a69312b0f71343c1a127c365ef89eca572601b19207e347cbbe38e54355b3263e71d994f1f36d200dfee163af675211c5e0e6dba81c4083c9d57c2a0fe5ca4e6d2493818ca142a105" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "40a116b2230cee973b9f2589dbfb0c9da4ad75a13227c7ae7bdd82e029b9ac1c", + "proof": "0adc87753306e755f43bf85ba50d0777adcb15ab9d007f0ab9f9386e3a910d343c4b33a41a878b93934eef7c13eb796e41988a884134cc092b932e9605adb7077ef9d45b1c5b0700ab73acf201b79761a3cbfc46559ab83c050b12321a1f7545e6208e09dba8fef0fbd9e9caf6ddf3ba0f8a5bff48364b9175d24892938efa65640040a2c3ce1cd42a0f077a2b97959cc903e981c6085d614eac43658e76a804f24521f40b5d92a87d2d1543ddf20c0eb59e87ef02c170c28fb3758f562d9b0c0f3f12ba18adcd6643e3bf5701148a512d5aaf045115f8cd84e6684046cf510f50114170e6cc23a45ddbe6f5ad8bf8a7c917732d5931d10d30e124706911d948d4041927b028f54c5bada3728da9367cb8542275cb07b64d9e885c97f80d376596b5152dea91387ce21b4553f756842069cb550e5e7874e953a6e45fd9755f76709b48e2ee639d77006564cf90e9742ed3bcbd8eb8e015bfc1f8268d73603d647891e668b48a6c7e620bb3fe1c7ed4a79938541b328e075eb140de4df7326d2f56938988a11fff72e36442d848fc193a967273b205b0402223d9422ccb510948303aabad29c9bd8baf195135eb76ff641be39bac00b65e6b70ebfd3ffa2f47081a0f0917538199d5d29000e2edbd461c9f233e5d0aa60919fcbbf210f14106117c303ea53e0c7f1363507f0b3e8eed7ea13b3475bfcad59dda0ad2edd0ec044c9e862ccda487ba02f2cfe83faab1f0d690c4938b6628e9fd8a527c2edf8fe6761ac8e79b7c0872d83a2d8f9afd58279a3c4cb55f22ca800b380be58e3cdb0c3384f9fef0b68f1abbdaf235133bea6a6838d474f64bc9fdbf77e66f0b695d057cc245b89b9b7612c761ee52697eeeb53e143f51668f893ebc2351b604b69905094e3390bc17b4f451a79062022ab4d3f6d7c798ccd2fa032cdfed7f94d9643a0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "46d8f6bb78e87da8f8020d1beb49bbcc4ca938e50c9d67c5a682a76d3995df6d", + "proof": "76d6d7f752d4883fada85f6cd246d5dc91e54b160a85cfd143a6ce132037920f5c90de197c4a8f41e0175cce932695f541b60470ca2b6b58c0ec078de052120254f48bf62f1c0031bdb59e842d96391ab7704c8f45e3db9e31a345700eff4e49505afbdf6e03244d6229432a960c5135fd959f6e2e5074f22588dcdc2c317d1a9a83e3fb4193fa325b0b98db0209beb7c4431a47d9756e4054347f597e94b40bc00fac9dbb32714a47531dbb619dd315d78bf30d86bb85e63fbc7409e445b5080a9b96f4b36ae6a6c17468e05cec629a1aec495ff04c286c4d5c822a262bf006a28030b8f04798625c7e5752a77e2bc07b0ac80cc1fdba3aee44acc13bbd25641208ae925d5250900336f8d3158341f1d96d50fcc205e8f5616792f5690fdd585017d1540ade865e0d1b439abdcb424881162725fab6c2705307e4d36cb98d4bc08299887c746938d7aa4edc400e4fa0c6786c9ad80dc537c86754280070491210e7618f2246272010e11b134adff0e7dd5d4c6ba2ffe6799282193b40bd3b2896e97ef7a6292d4a1ae06e51998c58957474b5a118f9b302dae3a05d275dd32b7aa2a847726678187364b9934909df629e2d0b88b92ab2ed21e5d60e72c7830cc8fe71dc76cc9918d1118aa01f866d3466929ea0b4f51e66cc163bfc5a0b62411e8c33fd2f4c21e32b65da55d470973e0b0ea7f9148a00a1c5fca7f0fd89b66e46ed486bad37237f82c70a24677efe8d8e06f4c1a8b3d594f07545f03de57459c6d348808dca280c14287dbae23a1b7590f25bb654ebe1fd4c2ddb62e0a2cb3fc8d6a1461e542c92ccfd58abfb31ed3b50db31fe71cfa3a3222038ac63485d5cc8ee5a77c90870a836820fe167638d08656f022dce6d6be96b64779f0c6fd90d66b35419aa9f2eea891e6d9a65399a6e74d614374ecaef3529ca75362094d50a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6aef0a80b34f1e75a1019eabb83f872156a61931504abd6608ce468b37d1083f", + "proof": "3243e2a1fe7d53ea7239290f786f65d1cb59965c177bf8f87bc505fb004dd67666c2c8e958a0c77a685f74d5567d48ba6a6732a903889994364465ee6818ee2964ad86202eb5528e7b375fabb1903d11f5991dceb4e5c95604d42de2f2120d1ab2be1c53a4a8d83c8f528806a2472d87447aa4304e79006c89bc926b490f0f47e287036bcd55ed4679d2b60cdee7cbd3b7e4dc1aff97d60dcd7a364ec400a1052f7a805e621da374dd0438669b8550ca02387d7ee02eb26c65f2bd67cb98240c67185163f5f5560c635e4ad9bbb77fe501bc90588205b8ff90c355d50c4fa30518a3627acd2cdb09aa6c71ea19c59b9506941918b29fdc9a63fb650af9c1604b9c4fd70e2f65684c5b656c6a0be6602ccfaf0439bd322e0e64da1b773b55067e66d87c71c1197c73e902936f49b03ca70b1a49a3863099d611afdd2938eee940e8ae0b87f8271dbbe9da54f131988c74ca147af9f1272164102ee2e11fe38a4a563aad510cdf7130e4df36e2e2a817a308e6f1e388077ad87af2f19be11892054a1d0e7e3a7d696d15127c19dca9223d97b74471be5922625eec2f172cb2940cb699cce925533e69d30bf412885c117c3399eb9fc1858561c2ed8dcc22a59625524d9b8e57fec6bac644f145d6298e7f280f52695c2ad99c2bb677df784f8614b211875e136def5213f228f5f0adbf180a027748f1d0c811a60edd910a987170b6a3079d9d79b55d32b05bcf17b25eedf12f209479fac6d480a24600358a3a40a027f76984c2ed1356aa9229537da5e5b284ed085f73a57d157aabcc5f7a3f4586315a28a453d70cabd9c1f447f821b60e8b4c7c32a35e8db9db31203f19477e04c5f8f221eb67748bc81cf92da0b1f49e917b433137aebb2224b8a1f4c7e10f688d3278e541519395eca443d66b1678d91a861133b2f0fd3f793301f0beeb09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "702ee43603efe7fd55ee9876c50a9015743b481840e8eb36db4891476139f132", + "proof": "747a16ce671073c307f880297ec052d2cc9f27d8ecef420eb00bfa7057c4314bb48a126f288e140da390f166ed4b689a8856b14d4a5c451065f29fe4cadbab744a746ab91468ed04d7a1cac666b55eb28ba1232073c10e8d81c30a0c3edbdc6f6cc0ef0801908985d9dd083cddb0b40e1485a03ee0b6dac4ae8bffa7cff37e7252694ea98c1fc80a1d25755011ae53c1632abf48163e08bff33bd57040c1120abeeb3c88682307e673f58634b7bf6b6d21e88cfde4cc11e8eec57320eeafea052d7023afb16ebd987f9f7cf6317690783c37a3ccf4cb99ced4866d62f5f4950fe690653da0bf31d68cf5ada41a196e134773fc30e6ce8395bbbec47e62a41242845b816d18f68c1db3edfa6b79730426980d767ab0e4d429b8dc056999648c41d66e515a1edb4ca53394fe64202dfa274955f3d9b37d77b02aaccf43d7f20c73ee513c6eb865ded8eb6ffc1402b3a44b39b755688bf2aa803fa4364a0eb0c115dc949d97124ecd86760401579c81b1f0cb4338c8cbf5bc196a1f6485dd0bb003b0d62a81f158c3cbc7e48219e9cb7a187835b9ad122dd26de48cd9a8584c195d9a2c3e73baf864068ab94cd4ae6064f750994b316a083b088b41f9e4d9f2a25bb2ae06aee4a703195e1b99730a618867ff95c13e514bda50520e2dd1cfccf74a2a1b10945f83bfa4e321d6868b9c3d0f111fddaf0c7c8a07bcc23da55b2ae7172438f9e8154677f81e726aa66ecc5e086b664deb6ceabe97c1774c6d22794415d62d03c1075d034a903214d5830684f40f60abc7ea4464d68172e34493e9a53e4cbfd2bb09a64b9d3c259cb5e6e48a05a78bd5848b372d44e40083e72446c56ebbce97d8f8890f617f09762440145192dacf6b9cff802a7bd5f600b0afc4590bdb13d2af340dd761f7d79bc10c100fa50a2d7228afaf41b24ceb2b9e4534720f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9c5e7c7e6c679a971bb64c0566c9dae7e374256d8d8aa89c2d70c0262032600d", + "proof": "68db33c1745f8bbd55f6ca0d5c9a4b3bc3fd9dc772b01993b3d2ed1f771cd83098fd6faa6e52fdf06abc884f4ef53b78461e79b131383ce5463ae5bf69f98d55b693db321bed2581efec57493a5995b7c8f9f0d4279e3c5050051d448abdbf3e4c3d0e51106acb6ee6133324afc6212c81e8f2eb35a81cd632da15481f0dd46463def2ce8b50d0e4c307676b4b21c3145e60217568362d0689f1ea1972b5040b44f30cb654b7472d0ff59393bbc00b8ec9019b46912708c1e6d9b174b1c69d035c3bb163bcf5cd920179467f8e54a6882d7a6f797427adb58337ac7cf202e80e4ec51602c02f4261b63b84ce9bcd620bbcf9a14abb366680847e3509c2e0651120150ceb5ec8df1ce6a087f1a2b4738b262505d4c8346e2d01b35533def71a72f08b172e62039d8c81a4cc531d6835c3f5eb77dd9858fb11c931efb24584ca49167769ae53ad99f1b6bf7b5c2f3b700096ea5128a487d227eba42f431181423be2030034f6ae74875ba86d933a19f84ff83e323653dd00bb900ff8fa29e53d25da8176262e7aa464eef5cefddb5e464d5967398dc3e25d8ff8e1ecd0ef26747b908f358fe5f46a62e0b08cbfb9e00c72178e7b026e88b9d87532c1b7fb3f0863341090c41398d1c121ae3054ed787e8ef379c1a59d652c7cd9db36035eae4626d8d4a4ceb76d888b354997b2733536af8dd83540b8baf6f472ffc3b866b6616bcec09d352c11d975cd8a8e8426191b25fcbfcddf0f24c0273b48af7a3743d02a94f33c0dac02e5a77ae5da615c8621b27b9007ad2f01b61f9e2b457fbe0a761200fcd63cfc1855af7af1358e62514f79e86510f2bbd4366a6e798cc626b89156fa82ac7a57645c1f1633c9855754542d0349aa291a499fa8e484f519a19193047ec32417ef6cee435f40607286edb066e54d644053ad51a81ee25a687c99fb03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ca8ea0b4faeb15115e9050214561467cf0e2686e03c2a77ade71992785344d7a", + "proof": "ae6d888f703d728a6201891c6ca75cba684903665f67b9c57951c053cd02e4342aa62e88c80041b0e9288405493ec03836b47dd6c1ecfd11e4913439b5074f3c1618109b2a5371b80838df9fc0fa8f9109cf9aebb22e4c1157d82e81b6243c401a456c0e95b5f6dfa46c2bded17868b3e12aafecb1524037d9ae8b4eba880d24f59de6fd60bfff6879d5ff34e9027e2950c505c570531b93fef0f78963d63a0d100fca15a4010b81cfa6aeca25fec30178830af153d99cbd9b295073e5dedd06d8d046f77b3926c71cc96964970737b36a605a52d16efe44f71d5d9c0c05070fa2dcd939eec952fd1fb53682ec19774a8cea192c1c5190045c458fd54e888d1236f4e1af0af38da87bcc0bca69019f8f4d186fe007874ab755d88386aa73dd2a54b0a3d45b90f4f90654a840d6dee32c633f87e9e03ed532765d5875085e1520fa603b9f46e27b320a1e2787ac6051b961fe94b641107137fb637d3dc0fe153bbe8fc53f075ae97e5d7f608a4104e440bf848850ac8e71883ad7cb205a14041e0a68f5e7b1393fd864ce0b4164c6f5ddf34138d0491c064bd846330d31e3d940deabc6cbf434d1c09b9b9789462f3918047575acada942a407dff0d1b3e9ca1c7834df6441f649082f252c9d8fb480b3dab552d7a513a5ddad308af38690e905fa83e2e163960ba995ab90c3db991ccf08995021a0f1711369e428ed125c57353abd0d54bb4c69da0a0951cd322788d8c136ea12e262585eeda8714a6d35f8424ce40b2c4f06f75e5b6066aecbf609f0baa553000db9e9e3b807ba6c9708fc719efd482820330069ebc4c6f69aa760d6c7c8118b6aa3ea02069c1a788d85c84317419c93aaaad30d57df6c400220c0d514789a160266c589c1d401e8821d0e0c6d8d52ab6c07910d4a4c89307c8b5203b4cf1b681a68b7ded717e9cfc565bb05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d0fd066a250c35914541e8fe97a0e4d49fef9052f579b032b64333d66d47ca27", + "proof": "4cc8d39069b401aa8136ed3fc3b13b196a76a062a1424d4431f1306ee0d465149c709a7605cfec77fe872c751193011c0cb696b65db151f836fbd48fccb5d20ca4d02e383c6c15400cf740add1661690578373afb7e1f08f5828503d5cb27364e27225cef313a433f46479e7d62b9f93b4bc6eb08347b51e3f40dfdc0e6b6650b52405ca5a08430c63538b9282ad9bdfe5016e94454eca009bec68709d83b8032ec07370dd6afd7f8ad0454d871a5d0a1e8756ed05490d24b6b828b5ddeb5907ae3392c34d053c8dc763cb2b2dcae08e386df426c81bc695cce24499c8f63c0ee64e4971ee4c027bc67e322a8f795d82f410fae3d3a9fbedb3f54462b7e6ad0f5aee327651154997343604d666c625efc8508c62d80f30308269f7a7ee8467125834da2a00919a5f82517f42b131ce02c9165f7e26a0815e1dd0d2a77fcf8257a0b4e102f4a80525bd4ecaa302ee478e143862ea000ae4df96a6e1ed390cc721665f9eb1e5386aa5dceb00a2d2a69ff84b381bf31c2061d4ca593af39acee80c365d3c2e1056a5dc866bfae878eef14add413b6f1bff8920f4bc91472f7f6c4afc6aa89ac03fafe2ec51f9c86ba3c841ad29ce2b4e5fe7eac1cac400e0f51f46daa9a37741f3db10dc3bcf93d4476582fc022c033adb9f34267c96b5f60ea660dcdc5277b0d3fe59b38afd57e24c3c89b6dec2238ae5e5d8122ba6dc63da977b1e40f2fdbe45e252e42ea04d4800593e8b0c309af6a8076813bcca079ffcf1078aee7487538ee2bbf4e3951d00d26024faa0211a454883e28f2b4a7be0b9f75ae88d4fd70c08590e61c996af26550b3556d60c49498f1662883529d663481d1602fdba12903cc32f9b94313cbe4507232d7b861542ed0c2b682cb4f16959a9008b7ed6c2307581f7bedfc9a825585ed915c9bacbc886fae3481726ffb517c505" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "da6ebea43c01a7a64befc4108fff7ea2cc45478e1341638b387e48f97e07e521", + "proof": "16baccfa95eb1afe41861e6d915fe5def50ad80b1f621cfa6590fd28e9504b791e1446d8c920737f285b952fec6fdd91b1d78cf4208ae1323b0039707d21eb3a8e70c0fd244a839b8306a229dbf2fa29fd5d73be3985c093870334674c2dea135698420effb08ebbfb8a7ab30b32c6a3f0726fbaed51184f526f3af2e3f9ad3ac35ddc720aa101632a9d9371b42d6abda0f6700436468c874c080b1dbd6b1002ae17e3e32727bcee8a7c084eb4bdaa628cab7a581fffca0df8eacb32878d9b0c1c6cb4ba93feb732e4a86db79226bb9c581249848cdf29d09f9a081bce792600ba9b00f6e6d3a5a37ff45a548b7271338a916c2d0823067c026c23415ed499333a17907471e46b1daa8264a4954453e11e02afa65a081574df97d6f3cd41dc5c9c8f1333f1607be0e5e333c55d11a4565d2d838499ce9a0d28dfb5ae1611d536e2e153508012505a989a6a2893d3867efef3f8ff2586fa17346579b4ce03a67da09dcef1e15ab99679de530a6ef29f5ed8c7f95333afbeb7be490384fc543f3cdc3a1a87f761c34cd21f378bc4e75c62d0bb65e0051365177f0f5e299e43f763305cdfae02e126a0d218b31c9deb63c418873067dbac10c09e1acaf4220be7373847a36a391bde5b4e3792fd28a0767b33018afb85b6f7496f528019938f493282c98548efe03d6e7968278a8bf0c88d151d60950e5c0d2c11e571b260772c594282eb1ad944e5bb55eec2fbfd54da2b64b239df42db9db7bac071031dfe356554c56d4d9cdff5d1b8a6560bcdff3008bf0b30dec9c719a081279ae7398f520dd405c1d78eaa03cd7d2ef1f00f05813b085b0d8a8c290e3c1277e4047cd4725ade06464a345f5e614557e0e257c77c79d62f38afbe0dad5b2dd6656bfc193506b4e5020232ad7761cc414a8b1c425b46458b84a2ee6faba600abe0d977b99302" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "de2152e3a4d20651e478538fdb8ca2cd12ea70f665a5968d9b3addd84a087265", + "proof": "2e14683c834b1a1f7a3107588620e1096133218103c68f97dba61a0e62f05f45f4852e539add5779a72c0daacdd5ba65cd17e4cea4a28c8ff8bbc9ab1eda792c9e46da192ba2287150ad7af8f58d9d917d1d73baa974a673474e440a6206931cced4fe5f414e607c0685a48126cf58b0204d8b269bb5eb808c4d003de041ee252db0f5d831f8f0810a8fe20164d1ef3fc0394ddd1a65f0bcc1336ed4981b280244ac1076c0788be790e6e38e820675e8e7eebf90653e2b37645bc3b76e0efb0ce83df210e702c23fea7b1cbff7a6c694a28bdc4a52c36284ed960033569bac077c5092b252aab02a0dfb451bcb4ff8af1f7fa2e4498b8412efc2a5322b434915fec7292f1eecbaff2806bcbb37596eb8cf487c7093fea913ffc8fd0a2e1e9740ecf40a75d2cc2f47644d24f1824f04dcc3db83cba64ae8f7d6acaad7e03e7b0290369bb7fb6d29dc08294b1223630a8f59a3069cdf609ac249f1a08554e6e527349f739fb5301acfc1fd9e5901593da895f6c796224906ce6df6cb2ac3e0ab19fc8e912b81b89b457608c2a055b0c7e99221c26f2eda6c2c008513c1bc76ea1b6c2a5b9933732e64380edaec3214c424f9278b3b3ebd0175e9043f52dada7049e611a6bba855de391c3bda511901761afeaf55a77421847f211aef1b21724672d0ae7572bdcc2a346b0269564c75a2932ee31e4b939622bc193f2f3c1e91a033a08ee37f422088a4c16abfc1ca2783e1410d64755701b6fc7ab49c0701c9fd1a3abaa8ec85918b97387f3371aea9d3f257f6f2e5e0961923245542be39eca306a6fe0f491a793615487ef2258a10da09a81adead250672f325143ff90cd3ee4869a7c6510898f9fc6218c38ede3145cb0d1b106df46c5d8bc18955a2cd2ed30cb2340bae81e62d63fcaeeec671d57b07af8c25bf24447c108d6c75c5fed85406" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 25 + }, + "commitment": "0678728e31adc97eedf2840ba93bb4d7fb9a17c55a60eccf5048dc8dddbd5148", + "proof": "62a3b9e20fe1fbeed9845e61b40bdfd4a86de41e95d059223d1d25820d616916c2084ce677f7756e619fbe74b0e68af5996cfd8d209646471854ec42e03fd730947f1bd892f425b8e69bcdccd2d0a0ca727a9dab12e03d2495d9608ef0408a7d4e51ccdbc99ed4b275f43a2972ce06230f1fb5059f084739e6904977cc22a169e625c578c72a68e1dbe0edf5b04b753e524629c5b1589c671ebe6b9f2211af0ef0992b4675b33ebd4812b1b5a5b02414f621a860c5064913ea3ac005599ec70f7d896a89d5b5d81e0980ee29a7cf53d580332596373c0589c5ff32746df3470174a5e100ef3075ac58e3f4c0d4837dcac06acb2226e015292e2d329cf023d668da864fb2ffbfc2d8c722624596d003a4b2af4271001826a82c56078fc5ef697f96d1b331a52241506a1a38942afd889b5e6e43ad2d7237265206fcde8bfe3014128ce1e89b7e38a074891cd5b09d332cd827693c646e527a530d40fe9dbbad59fac0164e98978f75b01f64b0d89d10d4b1ce2f43ce64dc73258b051c20b7395d1274508b9ca75a30f0c4e7c40f63e45399a42c5a1d5619b679c985939d69d97ab05c89684248b67e35f064b1e6add4b0343b9c50b8509ced06ba6752fafd1361a66714decef766d13134030219fe1cda3268101372599adf497369f0dab77a3c9e9852078f0fcf8b328b437baf5302fa3dacf954cfb82053b63ef786eb162353308f5c4334bc31fd8fa13baa592f2c1d5c7fa71b63ab7191651b5b5ef3d85261f8e96680662bf7d86791cdc9168b0f50b501b5545c5bc767f7509dd8ce40bd6116c1e052ad2cf598dca55db24611eda820cab0a9fec76446a2c41ac753fa835081feec977aedb4349c31de2709702dc4f7881cebcb1ec7e7797e3331466b990a360aaa9383d791faff2b0f03cbc189945b0446669f6e900b70aa592ac7e7500b" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "0e97a4388f23981093b255cc46e37145810d9b10f8973e30cfc7402265853c5b", + "excess_sig": { + "public_nonce": "bece804c74119fa198f23ad0f1cc32589b7c993814e0b4a61a5f7d0bfeeb661f", + "signature": "2dd69ef40fbc4e09723af0238f99cadf1fd19eb366702bb992ba39b886a2600f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1ac69244dddb8e724575f2a0b6c351163070a3f2266be953c3599db0343c3b38", + "excess_sig": { + "public_nonce": "6ea1273e475284c687f323610514a282c72742b67eb334ee53874eb324047c24", + "signature": "7aec5447b0d14390f6a902fc746df5d5798f28ce65ccd3ac1284fe0876916d0c" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "309e860f956b36ffde7105c65f507136cdffa7fb8822c9a31c68e032698ef473", + "excess_sig": { + "public_nonce": "4cf706c00cbc04421fc2ccc636a0aa29db7d238d82a9bb3496d45cffac84f07c", + "signature": "acd3cb6bc9645cb48b46ed7cd844c3fb15eea143f38a025042176fb7a218a902" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "8e71ea1ab8218ff385d41aeddf8dad024682338d31ff4a2f84a73dbec945615d", + "excess_sig": { + "public_nonce": "cc5d4c4e1c2e3d6b10afb973d617f977b76d1c7d14cd2635c53f1a3114c96779", + "signature": "7cbbf94a24c023c7993d5b821bd9691b25ba685c6599258b3bef205c88318c00" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "9c1440f7210feeb934a04fca729d6c51d6f436ca4573ead15d7c1c4841723304", + "excess_sig": { + "public_nonce": "184c968037fbb2b5b89ae790ea9ee17ae230edd6f7f51c12f27a0a1eb80f3f32", + "signature": "f8b7464dd1342e289287ec3b0560937f337d57f4ca900ade820bdba28f39ae03" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "261df0813d3fb7108ed8307d56c6ff5956b6a8871937f70fdffc6a48ae1e2067", + "excess_sig": { + "public_nonce": "6c6420dc5a64ee2698a9fa48fd1c9ab504a2d24fc1e5290faf3b78ffae183327", + "signature": "4b823e92dfecef0e6972bfc6dd175ad899685bde9f290c2ecc792b387241a004" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 25, + "prev_hash": "4bcda91b381989f2dd2900d3659b2bcd6808e0b3c0a3e5f003dccf6188cb9034", + "timestamp": "2000-01-01T01:26:01Z", + "output_mr": "7483d42b4d867b27e3f22486697ba70b0c102a1d8d64cbd0cd6df6aebd7fd9f7", + "range_proof_mr": "7dc8592a36e653ec5444b38f570b4361777616c1746cc3c74d2fea90491d554a", + "kernel_mr": "71b0c2a86bbae9d365d2d68476f2314ecc7678e792bcf704cf3e0537277f6e77", + "total_kernel_offset": "bf5718985e9684bfbca0763c9e9ac9c0feb7115bfbc475bb196dcd4b7f341b06", + "pow": { + "work": 25 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "34330fe2dc78cf6ed8f2e6493e63fab638ed1de7b422c1c19f7ffd29747edd08" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "46d8f6bb78e87da8f8020d1beb49bbcc4ca938e50c9d67c5a682a76d3995df6d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9c5e7c7e6c679a971bb64c0566c9dae7e374256d8d8aa89c2d70c0262032600d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "da6ebea43c01a7a64befc4108fff7ea2cc45478e1341638b387e48f97e07e521" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 13 + }, + "commitment": "303680219244c897d9e9cf5e8a0ec570fe8515e11d1b89ff7c4f3a3afcfad156" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "504495cef8377c7eb5e386f9cfd515eb97ca3cefdd1fd4e2b2e1ec4fd609d448", + "proof": "441e9381d59a8fc64c3dedc77819ece573b3f9fefc4b8fe6ece2884e5b31482c4a667a141e450865c2fd54be55cff2459f23194c135045674cc2dfcd5ae3d02d42b13d3549226dff54ea4249788bd7f65a39979fcb93e981502d463c2d271043cc8185bb6e49940e532cbc0d3769983ca04a975ba1ccc6b1772d76e883952817d2f67d4b2131969637aec9dd327edadf3c8bb94a14d4de8fb5dcb52ac9cc370b4592742f92f488df295d8578409ce0ca66bb7910b3446a3aa5d8e1d6aca15d09f6ce4a2a3418d2667c29926f331990ac341c493b71ee3924db4d78eb847a1f085409953a0eb3ecdf2d36392dd33452b3975fde8a87a39cc7070f0be561f3c24272f25fa31709c5d92e3ee44526cc7b0d10367c85f567a77c5b4863fd96f39d096284409c16d4df22ab12e576f601c41e59be018188e47d9384bdcd685865f871fcc2648966a9d9608d701e05bc5600b5a69f0c740475c41d38a664bb878e1875180fb2b8b6476c11419bf0fa203e678f1b08bc7763d06a0125c817766acb7b6ad6dbfd47a43577b9cdbfefde1ed57f8e1c6e18bd250f54472f6b8d36b1763d1bac10d69b913d104d03477ef57c3d757da4630043c8146da97e7f7298c3503d7dde9dc3ab331451f6c7c39c2dfdd2541cb68becafb49ff100140ccc3a5eda2814dc74500935278f212bde194163b1716db6415858c67f26faa5ca0c498100e02ce058e8ac9b2790d170a349678ae07ee3a445e066638103c0854e70a24525e34e4e2020381113b3249eeabe312bb03358b4aa0392c1e5a9c72f18feb63cd556192c279bed2fb4e10a51ee5559011b9133d05cbff933ef342e05495c966a9f5a743fb92d84eb0081b9e2ea89111da50d42c587653e6b1ffb1422d62f5e6b6a6201c80714ceccd8e2c98606321ed4679bde59db2f62f5ab042e9ca0453cba698d06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "58645de692f2f6f02e8bc16d74ff1dbd63080d0ab8c98c978797dcac7b50ea4e", + "proof": "787cfe8858fb71b7ba600964dc6433b72e87bad851f7c2302c5587b7ba38083f2c98ac5b40d378525e20ab76082e26f1fe469371eb52d7bb6138e806129aa040fcf0c135d16d0f9198ae722e43fd45fd16186397d942ecb0f654203239b955629872c1bcc73e3b397374e4e6abca7fb144af74db82b29a701bdc925133c21b03fecd75702f6717524e6e9fe8ceeb840be07e3d4add192caa6c947eb15ff94e0c4cf29e2db619dd213e8bc94988cd89757f4f45fa449a7132fdd5575f27f19e0025079898da9282156ad5d6026b3399cfc04ab61f794eba902280f6e8c736f900e04bc694263b1fe02c77b071474fdd7ca70205070e00ea575353ceb41c234810ac7370e46d521d8031cb6e7c18010c806a903bc8660b34a7019eb6452b3e2d0b1cbb955a8b209ed1715303c7b8d5e70b32a663923421f501b108f91c817b35072cd3c5e4c402a3fe419d0ffd77e9932da8ea01a0213d97026aead8a16f24ae182cab7aaaaf82c4caa0589044ead9cc0093efa19770f329f5f8ab93ab1db50503745ce8283d000d7dc6eb4a0b84ab07f992bd16a4c44cd1812598bf922169fe28d464197c5e2aacc2de09961976f10f247601ef8ee232e40800501d26153dc212c45ed29c8c12234bf707016fe43698731fd359cc189bc525ea7b562e56ea701168a4b7162dad2a1eed055d2bd51ad5eeae3f402624d754f215170dc789673c4b96caa466d310ff170d58bbfd9b1ae0c55bc07cb0c6385899cba57cd741ed087e5ad6e1aaf7fde2caf1783613135b3600f5dd9988b0f23e146bc6669dc3ce8034d621368bc29373fa867a784bc86b4a814697f11c88162fae24e0ed967d8b901c9c0ca98ced50a50473d4b1e18f5a0ee95fdf2c0d6e19c9a67bbd0c6e16e3980c43bdf8c10a1479223f7d1183888630b6d43426629435192b15c45685334d0003" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "640bede13d498818cee74cc7e08f476fa0b9a7177d04e282f40f143952f88635", + "proof": "dc88d269d42adf2d732bc7994890f195109bde0f2632fb285da51e797805a73b701a5b1ceb7f03579cc553041c3b8d597f6182e56b394942f977fbd0c7d67027824857c797c7dbaa4241a352f2a0e1dbf180833986bd5b6428ec118c7da10b1e1c5d8293d22c9b4dce2f628da4a399e8ca59492db894f09d2569f2f035508059b115fdcf3f905bca48910068f2bd1b4e1c1f8443f8745fdbf1d1c92064ed8c0fb1eae32b09cedfcc898b5fe38ed759dc435e79e196a4684588aed3bbddad0f048c3fcda88f964fc74edd1eb902f5b03bdf0db536088d137fd1e713086c31220874c0bc811eb031e24c6dbd02be348fbadc9f8733fea813593a52704476160133b6907cc63e0641067c6f6a0942e9676f3e138780d1320b566621d149c9092e50228b7c2ea1075e53a4cfbc62f9eeb676033a69fdfb708a1fe14797c3bfadde4406d62864fc2babb6d4cfdf695bfb7ede28dac7e1f3f246de0200fa59a0713057a07c1ec1a0d674b73d908989d136b5292dc0560a1e44dfb7acb73574a1fc5b2b6eb0eb8e6263c456f1a18ddf7fa43ec0fd80994b9cfe3fcb02aa877185db226dd4ac5d8f3cdad4471d0ea3f81869cdebb431edc98e023f212d24e08825feda3c2e68b849e2d65e568f915e1efb2240b9ed36fc2e1df902b243d77e1697bef02a245d2391a5cd57773d32cb59f613848daa9ea5946fcb0f79a47434844b1ffc3b4ebe0cd7a8cf8e08c20fc6d002320c135569676a6bc2a718e00762e2875674240c743bb2860000d11e7f26ccc04ad38f9d3798dad5c1cbbc644cb710e479f733c645c5a0f170fdc7848cdf1ac8dfff7c0477a1f7fd68366d1b09757a77a9b32d93e519912f41944048b0b66265469381a15ba61696b67a4700e746e015b9270db63dd510192c5e361e3be6914a3491dcc0806c75f78981fd462cc53fe99ca804" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "86857d6460217c1356a21bfdd78dbd6d95934531478992dfcafc547f88c82d15", + "proof": "c8d36400248f431935a91e3e56aee2c4bff3ecaafd4554854d9d72a47eb0ec4c4835da85e86d212be4feb9a125761ab114b9a235ebe9dda504e3d2c52254aa1f70db9fc0acada9f83ffd05b8faaba2df9f4fcdb61f4c007690cfc21550692c5cdaa022717ee05f102845d691bfea8ec24294dd5b93549fc30ab1146a0ac3b57e70c5b6ab8bb89893927aef5ed90e89f1b4b4493ca890cb75807418452af5c20241066093b244c2c8dcf4ea3f3877d631eb63b284bd0d994159fa219606aa0d0d2a2cc634709276cb9df00b4667320951f2ff288599fbf628af9868621576fc0a06cc437de916b42b178c20c535417625d6f361de99ce0155d1af9774a6e1b6218c6b0d18dd1a1a88a5ca313bbfc0e22ada34759b888db246f81e87d790cb03426e1aecaada9e4f77be3eac683f0972cd13fe79c5152461953cbb536a58b6767f6a52d84de6833ab8bfa30aa4def51b33c1419357749fadc9d4d671d07c270f7562ad896fcab75c676b0894900fabdfe9b5a003109b5ea4df53f72ad93d4a3b71846554c1c6c4c2e0c82cceeb94c986732446309f85d698bdd804c07309e14466d48e02f8f4b76fdabc3e29476bc33cb47039d86f2e7e5a900322af0373656229663a6f73c78699f8eb5a732d94ff10ebdcdeaac0236f26dbfe91cf34dc85433642205d935b68939901ceb65471f7dd8437d2867fc12e666bbc9ca4f4a8feb84450718221440ea2e08b58275a768397aa554203c0130d6730938c073c958afe5b6ae2e78ddecfcfd0b4afffa1857596b2de9e641cee691d671cdcec1612d7336068ee026a931c9a9f32bd36c5a57f8c342a1b46afa63ef0b4efa4cb807d9989415d2ea7fb2131b2ba2bc46099081229fc489505fb107690541f19584d3b888a0e6b2e557afccf00667037e49ee64b7f5689875ac71e37520be72c7a6cc397dc0a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c0f5267e2aff682fb9efe7bc6bffeff47ec4555cbb1fd195f8e15efe72f5d65f", + "proof": "dc5e64bd8e8542f9d2cb01221ae141897e0056cffc137dffc30b64348ef1cb62e8a073174ca9bc86c20e766ce160938f051fb8e00524dcc0185723150be722296ab9cd967258b268ad0d42615bfbc800594fdbf1ec93a3b6b07011669a42ac5b4eae8e70ce9b242085b5088281f149a1240df1336eebfee5a652be8029bc132a092bedc35e03ac82702340ca3a0988bbc52b0c8c92ef7edc2431de8a1128a706d389259e081ee05d9ac5af50009fd827a48528eb74baa7b1ea1582eb7caebd0537535384cbf025801eb8dff68837b5646d5a4d9487277a754da556fc7e9d28017c2454d45dea3625a3063fed2c5449b0de7369cdf928a6b958195a1851731e4a3aecca2600c9b0db69c2482a071f5b011428d1aa0802fa904d4f7a21aa8cfe35182af47cfc62c441a1a258f4fcbf36bcea20d00c90d89865b6f3b679beb3155e5cfa0431b96b7edf72c3c335a54abfd6884d491db5da7a323f56217d53c981774839df426fc07d3424410a022e9b981d951baf6fa6b68d6397f693269041e6316264bb01d0751512d628574cfc379ba82b06ed36cd0a4bfe3e7dc1ea196403276efd5f2a6934291328c213c63ff5b5d9aa9e9be5518825f9afc5c3610b505d5094482768a08f433740f0a0f1cda148d5698ff1fbd8f1147a89a0b415ccb8bc5782275a96026cf520ae1bee1ebc2e43b5a27af9e8dfc164a1d0feb1127176694fe01c94445a739b61deb16d767e215c5ef17e8626e1b6fffc6ac98e5d7e34053358d60b8f25fc9c5192d478d3006c0fdc84edbd2c931b19d8dcb1d16fdc7bd266d87c70ccb13f3290c1059711739d743add2aa88e99dcd65a01032c0771a516096a15eda537bbd7934da734e5310a1a26927c7360098545cf3b5f540af95cdb0ed7981ecf2d1f80061255d87f9780de97d2f5eaf8c1a1b79310965a6e8e12ce0a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d2e1726b88425d6ad36bbf6f8cfe382b6651616c0d90e8428bb8c75043f4d067", + "proof": "086bfb7a44e18d88cffed5186e57759703be66fcdd644d36ab5e4511aff11805e0d73527c5e9550900cd6f568cab343a3bbc21c70d484f9608ab20ab6da6360e780575d33a681e29624a58e7303af478f5b308faad45788fb1df4985c8e7a078921c6f71c6ddd923127f08e67695ca93b2afb183ad01360f44a5587ca175766f675ff54a90dcbeef37cfccf07b6e361c5a1bdac0274ec344272a882a4db3db06e07dd9f8a8808a0dec6b724e1f987110a2d537b71343f2d5519aded61e29c40ff27e87e71b95342449a6e3273ee4b5335b7d35a0e45b1fd1e4905dae716f01050a425374ecdd0fd550b2b562a160ae6d3c988752d401f2785f94d621af2cb15d2eacabaa4d1d6da31f1f2bd9c98dac404e289f28c7259f441676ff542c12a1063019b9f4f44a097793210eeb0721cdd1f19bd8c057a422b8a483e3354e43957b08429630a0c95cd84b6983ffa17013fae9227c774ed0faf7db27f1411be445180ac6bbdf5272822e12b7f161c097f7bec3d46f7ef71dfd2d1b278be4844322491e4bd57edc1e79cb88130d03c9c71b80edf702cb32acd687ecb6b840d74c1226d890a0831b7e02ae055a766eceba59d74ceb0838c5029b70820d81c0b36765012e23c846b433152c18b7d96a313b123fe9d4c3def87d099bad73d0f4d6dd6b6634a3aab7133dd1a9747cf6704b62b9f309224d1d72fced3c34df6ab565a19e07e269629ee02ec5660d2d8657e47e92c8ab9000c86a287e0f1428a9302760087d0a7634e79ac9e9dc698b14aa992c251b47ba876fbd13eadab13e0bb1b0dac409de3f4c3142b715315877cf0fdac68a4893acf360817ead940349886b0547be6d7d19cd1a87e1f2cbc5334831f9ac8a4e153b6afa038252ea8c679b70291c94025426e9d895540059d1ade207880e70c497f5a4dae70a6ec35efbd3904f930b06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e4c86e76b5b52912250c8063c51bc53280ee5b24a9901036699c6376180b2138", + "proof": "361d27734746007b19f8e3540b8e675493ee5f4bdca9fcc73085be7b5f966f12ca27955bfab433868b4566abfa07e104e59e86f78a5917cc44844126a954fa3ca498816a2bf56101e414976aef415a957e0a00ff85cee8bec0d157bb0f98c85e6cbb34ac57a61f703e7d789469fd903bb602c77ba9c474afbbba09730190b31187cbeee27f3f0e74fc391172bbb815ce7244894f67170067682a1406bd0b160342f2aca072ae93323ee284e0d6a72fa31ee6b11611d64983180add74c3ff920d50896c76ea79e5d237943675f600608ec7cf476eb868faa679b86cac6ce1500cbefd23f24f116df719625d7e9f874f461ff06f01a68b35b6235613cd06c4203b7ac1ed2a0580da34d0ddfe50d707408b8dc3f48f323676fb9cb78189d88cc33f9adf63635fe28984aea8e1478daf626630c7424bf525e91c02be8f143bdc84652aaa745799384ad9b38388c8c11753e63d68a4abe09e07b6ad85dfb42a81a8397c9c5dac4ea9763a98d48ab6f7f5b15ecc9e1736fd0ddc483c2766ec273f4b7b92b272aac53788e8638626c985c146d72faf05a5f095297ec9df70dcb84ba741acc8c2ce347a63a1c2989705a8280e38f10097b43f619277855757ddff34eb168c37b9d1541a1df22ec496ffe31dcbfc9c32c58479301b942e05557db05a1730c05064cb68866db1fac9a4773307b9f68ccea66eedb32939c0715a8ff29e0f034c30878055cc6325f92b8dc623b12ab6eec5036b287b9596d3e3a235e31cf01236e1f75789b7b01ae7836e929185b7b4688bb2002da66311bf63a5cc43ef732fc8007ad4278b62d224c5cabdc56a75f453fde8507c2a9a3f222ab92921a63d047595e5f0fe322c18faf9a59adf6bd71a819d9e3f6597d33df519eafec05e6b067a29b4d5dea0d4202447cd38cdfe8d83c8a13c5dd6c1bba7c0bc4db9e19f6c09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ec10726fa7bf2f35ebe687d81f19ff7c4c07306654b67aea6136bc5d8818f977", + "proof": "8ee61750b9aeb88b8b503c09706efb79dd177f3eac49841fa5bcbbcbbcfd526d3e6014313af2b29466e4b3ac1db6422afd1ddaaabe28e4000399971c478a034cf8dee34fb74ae4dbc79d6eb95f95b30b8e93d45fe9316d275b05332c6dea582fbee6e49975ebb7b9509f34ce01aa8adfe1799b2581a455767b81c550ca6d0c26da23a7b29407f0ff92f3403b20ffe159f3063cdafac55e4c61ae7c08ada5480402a1fa2cd8dad611d7ca8231935dc3c93cc1fd73ce3a7405d2da4818acc91d0252328a3da1a4ce9a9f1e054f9a0f0d44e2abb369cb97b0c0215bfff576e9400120e8a193c4603912142b8ba2e0e1f71ce3fec54b477a14b257eecc4d29c9796558f6f8ebfbc065954dd80a2a7a1a4b17631ac4555ab6dc24b940fe029874ea3ae4ca5e453613e3e445987bfce2f8005b70a2a0067f559b487f0876aa4db6470b1cdfb552153bb86a037481c9bf3145460a3dde5a6359ed9b78a82c727186d008126b4d5968dc250728f1dd7e0dfb2e501c4c7c70225e0a75cc7f91e3e7a7403c3843716a75fe745993937772103a2766f36fb4ebc95bc5038a45869bd5c03b3920efe7b0f13842a256369e39c9b8ee97d33cf41fb61bb24a81e4972d2de7b93cd4f5cac591b6cc0dc1c4dad1ea275b1b85e37432f1243ae1c66cf5e2a7e73745fcaaf61e2935a0a5ca958e6e8cce4380e3eefd9c7e9a38b13b970c9f5220270fb4415b13df4b9275ce24466dc179240de0123e6650dc23193a33070a7b49b763ae7df24a42bd9b3977dfc8d4f5707ef98d3408e1dbf172b6ef9396477da2154a1a6766ba06bbede6df50642a7d0fed32b399058bb3b355184d1da06b6e5bd93f5d7a99fa56c8464770e4c09861fa2cef66f77e840fa857f851b3966d11939a05f600d8947a658f76b10dc972be0069bed1dc9aedabd44a53186163202d089b01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ee2719c518d34a159f5d4e835431854b7ef9a929a29372b8d75349f8e23f480f", + "proof": "d2e60154ff093bac69b9645693810ccef57382bedaa45d4b4b2c2a5075a3f72ce815896dd98df7a4c67f8505dd3f080ecbe3b76ddf479158eb26379241c1ea1e187907eee8bf6d80da4c1e7c003133e49001b9759e3f6a37e52477362ab6ac0b645ba954e45afaffbfd27d361fba632074aa578bbae18aea55132d4884e2ca7a4f453b519267dd0a164ce0976ddc6564a00bdcace360f1d7b5f8d697df1de50213a291ff42c24bb64c73e0f4bee9c9b35655a6d3e5095701a7eb328dca597006986258ce45c9bc48b063c69dfb2c255efc9c340c9135ca1983249ba165c83703ec63030255d84fe53ec38679b9acd8cf032fd582febdb26192c63738a8dd0b12767b998f5e6765f715137cdf661e92bbe564db18984b8090620e80c8dfcbce05ce6b084f2991e2c58f473a170625d56c2d1b0f8213fa5ce13323e7cd5eb8ef1d00cbd2afb01d9a1a5ab2a2b0f18dd3cd40d24fdc5911668a33ad9a7e2045325cf2fbcf47e9924cfbd8fcf23ddeb4be9c3591ef73785f02c14cc0a9fd97a112387434b805b31e7cdeab8d6d94a33d68293f8df56ba831686e45c5b8cf72b6e2665291122087bbdc9c75be9dbd8611485527de256250f29e14910e95296055573668609605e497b36c37eac9a97f017f8c5b166c10dad482490babb11db3808f15fa2ab54b6b3b60c331e6b5306fff7e47388acf187a700c600f96e16ed28fb9330a69b90e21a94412cc1682495dc0b50bb0ad96d4c890272f7278afc666247a28ba2175a6395d4ee50716aaf1ffd19f9a21adc7a9fe900faa3782a07dcd6b1b48b6f188530901de9a771600a588e822cd73d1dff8b17b7aeb3d431e5183ea06105be086d0110df536501499d39165d83427046d3e3bbabe13ce1844593bab7f0dec4b073ceaa9b7fddabf843e9488c12b8090e531ee9dc36e328f98a59b0d640e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f64f5be6c9ba6d0fdcd1549c38d08635b1b240022d387fe3e31e2970486db40d", + "proof": "76cea243362166048411a3c94cc8bff5bd99d986921dc45709945c16e0410e0b66e84701063bdda914bd4d61c7b32a2856e18e6ac30422b1c2fcfb13107435107ac34deab15bb5560395dacb42e202b3d944cff362203c7429d7e18b27b4713ef018af7b5cdad09a4d802c79717e87e2245c4e446126bba67c17f16c0d74585c89c82eb7907e744cd870556f33a70933fd0628518561a088299d506659ccab00697c27422ada04201b0e597355ceaaaea8a480896ddff44aaa608631426e6504fdaaae2eda88b5e4e1fc3ffddd37b0b2984b482217b7b338f84e8d71482e2006a0ac36f159e65826d3cbbff3be79d18af0e28830cf525863f0924a60b0f6cf1c063f8210ade3b62f95391bef6835d9679996d994d2f8e5281fff7e17b7e79e6bf4236a62e07c80c9573f5ed2e946ef0d923344281917745c20f7461e4173440d26a4a253b7f889a7b48eea3126799a5082f2438af80f97f6e8aebb04a0962f3e6c2d445349a1002da65687a9561d1f84cdece844e4ba71fd4f0c53b8e9815a0ee860174695f1ae8f55faaa4eaef185cd8227338a0aa2a6463dbe5a3833e36522be25ff44421d65a37a0d68c870097daa85da21a19146d4471e985ebf216d473dc0982fc758f7c50bde6d57a1199af485f5ae9b4a50d38a278caac512e0d4b132c6d7d799a4d08355ce02128365696ee930accabd513783ec1c33764b107b433fca5f5a7c202512ab90ca1e4515ec239023fef237ea0701e1f7dd73e6aa2db42b2a37f637ebb49d17cfcd8e076b6c183470a60f1177f76a34b2983b5fc7352c0f0852ef0e8d7ec370d1706ecfe3010b14b86591adcfcac7506be7ed63a3c5580d0c7c40e96e6631fcfa7edf545ccbabfdd779af454b216a4194b185d95d672301a9dcafb100d10d35dcaffc6ea10ea03a24011b220cefff952cfcadae4757360e" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 26 + }, + "commitment": "b23fe569b3a3e62db7cb733bc4875288618cec1a169743008353a9f93dd15545", + "proof": "3617de119a31326ecd8c7c2ca5f0f4206cf5fb44e5b3694bb35919591395df77decb3d5ef40d627892118933190b9e206687c161b26104717a3bfed0464267173abf3aae56648c090af3d9cc69912c4395c4919c68d2209409b358eaceaa1618b6a228fcde91695763bdde16acf1fae8e5cb945d0dd1026dd39e3873614cde31783d14f1bf2426777a67b0fbfbbeffa5e1b09eb37b50103d30e3ceec9982070550b6eae2515b690a70e0109e03262cd236e4a013f577acb156adee2068ced8092a1c58f8e8021acf3d8fa180c47b00ca02f1a38a5c5bb403189d850f0c3f020c98a40a1bdced2a3890af59bbcd438b67f5c4587dd63d613672301adab57eef7398742d2678dda309a02b2072a6d041056f41f8da79eb75bb108239c112da555db8a7b895012b749195606baf951e146a3e0a37dd6d591883faef51f1776d94285c554760297f92888ef8d9bb8185873385e3d81fe55ffb6270f1bb38f4a4a128fe0e533fbe904fdd1759eb3eb0db8290b7fc548d215951fb7dbb9f6b40ceda0ac07a08d5e6b264cde9ff88bd85c8b78947597355bedac515c21c4515ce6984050af4ed5f97453d7e4c6e567690c84b43a2b65208fe7959239e0a572f29a09b2a5234bf6077d94cf580b0c101feb3c1fd6d09479e424b5df7c08f35abd06b2a631e03f361aee42aa1e541944fe449ec5dab51ba2db20635b4a6046e85f194f751aa351ef7838a0f3ceca02d763f7e0929b3ae34a5c2b06781bb29dbd823786666cc92e3c028e522d6d6a382d83eb688f2ea3c2082dd5246bd40925b2668ff3e4878d0e179abeda45fc1c21397084b0182277137a2f93956cc82ab9f3582e6b940387bbaf2155b82bd11742a2affc22fb1a1841074d616cd87976c45ba289ea40ad52d3e68d92b08f43562b35ddef5749ef2b13656d1d2eee3cbe678d8b8409e05" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "04b942cb7ac9ad300cb118d402478a417b0be85d0d59ad68678bb5c7a182df15", + "excess_sig": { + "public_nonce": "d2fe8827fe8ed2f6bdec08b8c38f983b3c9d85c0e4c950775574413af3a86857", + "signature": "f07527f129811ba2089b9a1cc4da4253c2d02b8202bcdc7c5842772601432405" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "4ab5a7f5973acb74a76cd341baf386f30fe45d8fc2b1a45078bd210c99cce752", + "excess_sig": { + "public_nonce": "e8aba5b7c33e5d4f2a453943e69b953f6ec41b656ac767bd37cb624e7d676c41", + "signature": "d2efe59fbc5508f5908a29f1788df1a303851570677708b38e210e6e837a5e05" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "6c28ae769d5285f83c40f85978f032bdf6df0ccdb32f9975ee7513582784f14a", + "excess_sig": { + "public_nonce": "825ace444b7cf28b269b5bb7f3a1db98d1c0fca6c8f5e2ac71903ed23f51b67e", + "signature": "6d116f59c76b0a64b11fc84445d6230b0843c968f3623bcfa73c52810af57302" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "948309ee8fb4dee3d10d32f6c3ecc3500e9c4d26065aea27ca79cf2e1891f40c", + "excess_sig": { + "public_nonce": "5cc2e16f2b9727f4bbc694b430cca436b3bf6c5574bf25825ecd4d571372387c", + "signature": "8c1e9ab3d550785be5d4f279220451b86f2d0c71b70ad9e597895f0f2334df0f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ee0617929a432c12715ad2c5e738101831bc4afeb8e43ffe2b8d2395ce18d84e", + "excess_sig": { + "public_nonce": "c8ea368f8e1984d1d72a4a2db750914992db7f882fcb2d438a2fa44c5abf6149", + "signature": "868b06618e6629725eec2a942a7bd68e7f5977d1a2567690419abd7d99283c0e" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "746a5ef37e323b6efe8f43fb3c0ada9d06d5cc9c34b24112cb441ab56d3b9e0c", + "excess_sig": { + "public_nonce": "9692e55b526c85287ef0d4ff75be10b91de888a9efe597631cd8463f7158bc04", + "signature": "997badb65bd40d6978580157d2ac725ff9c6cb5d1bff3e4d615db978c25eb404" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 26, + "prev_hash": "80a9514d8aa67d5bfe18bfa847e68bd707fcd2075d6537eadf2bbba545654d0e", + "timestamp": "2000-01-01T01:27:01Z", + "output_mr": "0eb071a321fa73ff29d3647e0ff9012e65a4e5a61e5825da8041522dcda38cab", + "range_proof_mr": "0bb7a19aae8d0e41bb407c692026a7b483992ff3399638a11ec28ecf82cc9a6f", + "kernel_mr": "4b5412be78ce603df4199bffe7e48e5e8f5aae1220f2c4402c506c1d296ef655", + "total_kernel_offset": "715b3e604725cae15af47313dc05fdfa800e623a726d7b9421bf3f42b490d70d", + "pow": { + "work": 26 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "640bede13d498818cee74cc7e08f476fa0b9a7177d04e282f40f143952f88635" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "86857d6460217c1356a21bfdd78dbd6d95934531478992dfcafc547f88c82d15" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c0f5267e2aff682fb9efe7bc6bffeff47ec4555cbb1fd195f8e15efe72f5d65f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d2e1726b88425d6ad36bbf6f8cfe382b6651616c0d90e8428bb8c75043f4d067" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ee2719c518d34a159f5d4e835431854b7ef9a929a29372b8d75349f8e23f480f" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4a3635272ede6fbae6330d877a5bffb3afef32f2e21b262b8de85cc52134814e", + "proof": "0036aba4b861f1675f7c424a1628031e1e578f007a172909ec5f964c79387c4c0ac4d2590cf949649414a487337202faf8aca1d7e67a5a5e76d86b96462c751ef2f6130f235d79753ac20cca025f80b54462dd20524f1ab2c490cd9afeffa05bdae89ef268370a728676eb8c5d404bcf19f41cb6cb40b58d5ad1d4e10d971337bbffb5ac6f9fa84e1c00e7d52d5c3dc65cb1664aca066fea93f702ba9abbbc088075b3886e3357a031486baf8633c5281196e93bde8155e1d7435108441d5603ac3925dc356c937c946ea00da15721f706f67fd5c3b765d14af2b256d0e2230746f937a6059a44669f49631df59896c864dd8131a7f575dd70f66ff90835f8285e420d7632b20b7d165406e2f70a88b898910a87178c6d2673fe188ad2663f288a673da864f29440885457556d9deec15bbbe9f7b054667af8c6059ac62d0473fce71e926303be43149bdbf556307ab85b47acee181c623217815e9c2bf20e03982d2b9ef2029b27158a3691ee6d3154be25e6433b060f17043305986c2aea560855f34f49ac34ff9a891ab4b8a14dc0b8a42754f12cc2d8d4c5551e2fab523842c41f01cc8caee74eef55ead656090e47e6d47bc7c83461daab8f86d8d274543e7f34ab17085015f6658dc7e721abaed5091c647677403fcd3538d7e5ce35042c54e5b609a39aecc18d99d97950aab7695868e8b8e710c552fad40441f8e353da3035badaad8cecb3b7b4da938cdabbe6dc12fed7ac5dc5e07ba584a2464f22a2eec6ac1cde73fc839c8f76874c98b81f3f7fd66eb93d2d82ee05a5a225bd407e6cdc16920f3e6293213210e3393d4248d6e09aa9b80812e56dd19741a5567febaec0fe6aa7aac7d7b068687505a52b1812df0c1769cfdf42deec6e170119018bde1baa798ef94fc1226b1d111a07e9a321455608add785ebbf0309f323460b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68402edd17e210bad42d074496fd49facf33b35abfbf8991e760d3ecfb11b425", + "proof": "7c5b76559f7dda8421e2de1c04c89bc55067a2692470901cc73ccf434e7aed6736e0f335d1af5fe8e4a703d3b103c7eaba332b215aa1432e021cc180bd8a602dfe2be2d5f8e0cc6bfa5c53a152cf794eecf0bb8489e919ea8f3ad1c7a574531b1ea227343f5b20a233b84c804fe6d6e73359660cdba86d6f2056089343be9d6ab38f0f7e237679f05424d75865edd3bee2aadd71c772e0ce5958b90950b0b50c3d6cf617a7260379a4efd5779d24759c57338f594d05e5a650d889119c37470c0dd864025ef674904bb630745c4282b124abe0088b0e2079384da93cc9ef080966dfe8814d41d248afa5e88b6b99c38e15af7616d4d20b31da2f815aaf68283e2439845237895d7b0b483bcc7e00bae9f2a698777df0ed97727b023c2fa34a6a8e1b96accb7f97246690c8273a753f524bce5ab183309853404f78d718f58a31ce47a89323698e9281b64f924355199b9107d49e3d0283ee1d06adc8b2877d6b9e073f01bc86f7e576fd7d6a7723ac1bd7f2ef0ee19d3e33aca80293f11cd71792000daeecbe1a79977b021ba9a1d169040f46962bd491c023567fc99429be5a045db1080ad37376a30d2ed28acb2632c361754ddb0855cbd26f91d4f6bc6762d6e8525dc3f1d240aae82bed45c4e6131dd4ec0c8da513417cb4f6470cd3571d6a9f191ee19326ec04093be9e7f1b8e40c46c201fd4812429400a00a4da61317e4741aae43a3db2f0677ed8f92f8bed1f8c91b5c6268a18c23ce4a849987ed6e1063fe2ff5eb3a64f3e2dc713c03e912e9bb332349a5911a88090ba08a2d0c244ad293d90d3f864361e8b65504ba1078e4de30b82d4f90c9ce02a3e3a3a69809c9ac4b3074e2fac742605aa9284b82bd80bd1f85d8c002fc1de3ac138afc040f20b928a7a487cf9b024c8589c9f4d76476dc0f233cf0990cf894c5ca24940e07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8e741213b2451177b649812f8b42e9974d4dcd8f71326f604283c1e63124e750", + "proof": "04b3d07fa90ca1c4d8806e2119ff68db228b64df9e60ce4211869c0a5098db6ba29df51ec846a3e4d6b587d836c961f5d573a813194344d183c1dccba162f13a74cd4cf8f4e3e70409087c043c3aa9f7299ae708e403031ca0f55d97f2e40f4e680d63f95911de26273a2cba2d3a47054ff8098b63a8f65252813c4a21429f6d853a3ee4eab5e821fb08da5c58fb3db4cf2116b9f354865109f18cf83fb7d106ce31bb1805925b74957a87d8c6d7a675d65f8495f27927784f6713a135d95e0fcf283748afd85b70b338aaddbd0e00836929b2757f107f08faae4dc95e980e072293bae2f8a59dd8ce5e32051f4b09aa9c90c113cc08a646f287aa27cd0f7725ce8a2fc36673608ce9edf330e00e200b2c6118347d60abb50c77b58abb1c9142d45306e7246e99fc2112e57a2aab901614cf8c9ab33f9648b5e613200feb406234b81a8825bf4712a18467dd025c175957505092141b655de91d3676ca37cd60b059d59e2b2bf5d1ba0cf87168a729102b6fb19d72a353b43dabed3e0bd70526a4cbd1bc13d85c77a7bc67307a6f9ea0ad25ccd58faf5974f700f63473588034b4a5c1d5cb5fd0188fc1e99d18b7cec66e1a9bf2f461fc218b30088f6291dd130cc01d470dac44be303b62c7957677d71337c72ceb50aa93c70775817909841eb2cd519200b7231a01f5e0a110f5774824ac736e1cc4d3bd2aca56618ab251643e3a300bab9cbcd20b2b3d5d144f7de9213ae9b40c2c9c52b10e0b6cad9d714b642d137ef2c7585a6e7e518402a5f79f3d1bfca9f7ef5aa4f161069a0f5aa5725633608e56887ffeac7bfd60235f6de2c3357b073a800f54102b9c1850baf83d149e24d64cdad30e694ea08f8cdda5ef2edbe4fc3bf5bf17f4a376d72eddca08174dd461f160dc0eddcae51c09c57c50cce4328dd9f3b9a59f0133acc3ff490a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9c80afc1e5f262195472878e6ef4807dafe15803343a653985340c46bea9bb0b", + "proof": "3a072954376c2e843e7dc3a20f9b25e226d6c09eabc67e93536e37938f7156096e0d1e0d611df20412828f89ffef5fe6611d333516c5b4531c394b54f1c915557633c29d34445da337ca99bdca4475a92b8b95cc2c0c8aceccd17e9b0de00e24fc8ce5227fcdee72feb15cd53c1a8e9e9e04f7deab58edc69da3d7f06ee2ca4bb53954fbb9f9c883d5eaec4173a33a50373714d4547d1bfee8b7ab539ba2c6061c145cd8367f96ea6d016d7d3a5739f427503a66e1c9590728ff3fe7fb76da07dcbecb079651c5e92e0dcc1d9475412d17dd195e26ef82d517c8f5297ef58409dae61cdbaa2f860645bbfc08ed4f5cf9ede7226d33069b6be2d6b9c9aba95d57bc61c5deb7077d36e6867b08ea8e86eaf4003a7a7a7abd4237f2a67031d3e85fce5c451243faa8a175705f34e2cbbc6454432f66a1bb1ed3b1dbad8bfe9dab579ef8b1659c9be0558b04814d83911912af08afe3da3d249a3a34dd4173db3560a0467ee317e14a8a0851cfc54abfbf239a01ec2d24c3644f1243456dea59e73a741e031f25ef6f9d0bed50e87709bb8c55617e560517a7992021ddab78fc6345f6585106f0f2cb4a7a2ae3a4e5ad8ae816cb3c42d0c42f2589e26dc16f0436385ef5c7234b08fed06f25d2fa8d2d50218ef7ffa7b46b2ad5d84719e1bee8c6143cdc5901b2e63cb867f8f95d7e4fcb3f213ef7f09a6a5d6094a2366e9b4d587dec1e4fea20e41cf4ad80f1f1f14bb10657e621c3f825807d182e5ee0074c270a5ce218de76c18f11e3c0f095cbab04c48d2a832af3cc086f85fbb80a2b15a65d18649714f231bc3df5439c7a3c035ff6e3f0ccf283f287877ddf90153af10e07bff677740396df2bc839631d18eabf67f9e0b5ecc0c0a88b4506eab34def420b06cda1e90c0604c3fc80ea7fd485156989e6c42fec38f62dddb1b8dc46cfa201" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "aaeb52e8cef20a2ef7e1ced7280fbb545f8b96eb0a7384d80940f0833fdb403e", + "proof": "206b6c342d7bfeb0665b3e5171a9f062b828e12a93fed1495a3d061cc26ef84728f75078460e2bf63ef97ad98fa2b966ac68b3d16b3056e9b1cf169c49f956713e8d448feb393ca416a8d00f113ca97aee20471cf48fc7b088045a3279ba416c647af02323ff1705b0aa24dfa118ba9e38bd3d5bf1dc73f7236dab6aa3cff317574a6b91b8766a2211cfa27c84f97014c10d151024c965c6307c538b2819570a5463ff35e33b9e02d58e3255754c97d71f8a192fd209c85b717866c9b1209509d1d17d59c9b1806628cc3c71785f9b46804a24f86b84fc1552a1c416024ff606a27f731b7532f57935ae123d902e170746ee83715056f2e3e50cdc0e4912e767441d04f016458a0d2456f2cd41e662391f98739993281c84f7d9bfec35c6131d36c96dc4de73433081baa916d25d94360aac0e022dd09bf7bff9430926087e381c49ceab802b85950c92205df6ed4b24c9b3dd26bf6b490c143a9d9fd59704210cc6e213485a32beb01120818016a9f38be6f9f56bbaa7b6a2613fb65020a13fcef47e697bba70b36b757caa5d1fc61dc55e763db138a54289c335d3657ad04342d5b6ab259d3562eb7d6d7cf33b1769f1e4a526c2f57eb3fa9b5ef361280631b8bc14c76c7612ca65d66d3dbb4cf4138d88c7eda5164ee2a75a1c072b5c02136420d941229a7d51fcaa6ee65256893049206649b088d079d3394ee85f25be598edd7d45c3cfaab67fd0c7ad4b4c60139d0f288b8518c2ca3c14fc08143af71e484db68c229bc19c9793731f7222c541a5e56dc09cb08c2fef430ac445c5db1fec9bcb8f4718901c304a8a3b48eccb2db6f35df40d0f268ed4700c3103563d6bcbdf10eb2ebcbf3560ee64821a40b1faf022b70c3b0e5df641e41fd343007607d15654d91150361c7679377b1c4266175cdeae16bf34d2d4f7296a066d8db400" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c66dc9c09ae371c6dc0277a4cb02eb70987293c42c0bf399fbbe998f9ed0b963", + "proof": "3ae6f417403d5ad7709ad957ce3fa1123bd21bf7d9ed8d9ae6f03d1da4f3657884ea2f97b919dd5b38da2315694b1ec9cb2e2f4fc55c2ac963219aba3216f8194ed534a20eed4958c62e618226b9943c29f1946309d373a6fae2dd0f61caa4738e77027875307cf2c7f1673aadd9c7cac59e3f76681b82bac4c8b04a6f25667514b9044809f1a2c988d7985a159dd6c033f22137abb291949badb02327734c0a8855c99f7b45f4f98cd0113647773bd2941a62ecfc0357b4243fd65e8d448c013e83b8bcb2055dbc274023fdbc4202ed3ed7286e16a9a585819fb6876fc8bd003aae8b359997bfb0db4b7262f7fe20c127e92b1c653a382364546253e4cb1920faa36a5bb5245f5ee285fca15fd43b08ecb7f046f51c4e55d56153997d4e387980e5ef363277e795fa4c6d77b730af300e9b1270f8ba185dd1f01bbbe4be2f00b866b564dc7f6fb4c36db02991e5f6b58561eb5a2b2a52fbaceb36da05f4020680b8610c6b58e668388a78181a91dd2fb2aa4a2acd7aa9d8393a69994ad5171ec837be6ae7ffb0f250eb57aaf3726c3b93b0f49958699bc273d971f85d3fe91a46020a4fac15c727f9522aa565b62b528546ab26f9fbf3e736f6e43e807bc764f29f8bd78b0b3c73453f995c13156dd33521a3e5dc50676ec553b7214fbdaa470e71e604fbeb320315b83a54db84c0f38c168092c6050aa7561b64d3c8d990412211ff80969e88a9fda9f6d23102a45c2fbb206dd1fda12fe78095b48b6a215dd2c265e144058d915d3e72eb0038ed26d5032b4fce35f20c798c3f625414a137d8673ab6d5cfb7be575bb5d0c0f7dd9c1ca16bbf316b21c2b87d0f69cb919f10d5dfbfb1ed157cd35cae66e8432587a08c8e0e21cfc813fec896421d08d81c05bb0d012b3668070ef4d04c3d7933dab803b935aa7430a1f7449a1baed1364601" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cc08d9421073875d06e8d2d0f7085ff86daafc5f57b145cdea4d6dd8239dcf7c", + "proof": "424c9d0f60496aed67b3cd1b9e2f8473400685378e1458d99c80e94ce16d5e68bec4405d706071de6daa4440ee67c1f0ba84fd6edc1d4cf681154f709b2b11420ebd0f1d3bfc0f01f1640dbd01580e520c701f86ef83eb0b57b86a574bc81d12cc40d844b97e64f2a936587e5a94284b835d8b6f46878d63ea76b1ef78f64160767b9e38a14bcb45a7f29c47be6ca0f588aa84d0edb49cb68f196163af225b0575f5c8427bc11057e0b233a2035cf56ed4535d3fe8915faaf6a9f7a7566d6b0f2eb895949655a907507e420eafb2a956c409396960d1b620fb33b0ce74e120051c917c4f655ab58fcbe575be4317815adb79876e510521abcd5c399352ba6405ca8aa70d55a71202d7be663fad4b8591adbe547206f9b7b2d031bcec2899eb265a89fc70d5b67be954400148b6a640e76e18419ccc29918aa3d93a4dd81bb055b0f0c33742040b142e050a84cc13272780fd2e85b09d42ae51c9e9a8c63cf50b40137b5038d1d9325d983d985e878ea3a64f24dcb437b9409fd120dc672a9040406c713408216ba8583d656fba142f1a5077d61c0ac99b98c6570165e697931610530c6896a278ef2bf8556a57c499c32c3304cbfa36da9cafa3958dc5bcf331b4123ab9e86d6c93ebe059dd7fcd6a1b2aa2793fb9b603f1dbf01cb7f3b0c115187131f2dc42fdcefe19210b73cc9e63fa43ed71be4ee29feb334435eb73d43ab25e48fa049059f0f751a159da9b4e6fd0e0d3dae3ade7afbfa8432495a2034e5203d3894fc803a4106f1a1736d2805ae5486fa78cc0725f84447ee22dd9331e6a2b563b8c022397d9f2b242edab08631f5c5bcd5ed5058d6ea40f0a2fcf503dded0ca02f2c5eb2dd70826f6dd460bb6d831613df1a6838887e6e6fb5854710205a97f3c5ec0abdff284df18db52e1f21dc1a04d5dfd9b8623795398dbcce80b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d0ae446d3d3fb224bfa4fa2830516bc11c954870e0c8095114cc696df0e4b707", + "proof": "1ec0aa78a7cc66977916a2865db5e5584efc68d5db8a05700d0954a1d45a2058ee49df2d2fa38e092b3063a3b47d0e88d70bd29c72583d5ff5fa5ba6503404727ccbd8a70687ac1fdba4993e76d08c054cf6472094b2f58442ef1399cb4f6f17103dbf0f6ec44074a8003498565f5e15382d368088adbbc319948fef6cc9d02f4fc0dd4ba4a8d4bbf3c3a86e01837eecd3771e20b5494fb5a61aaac8faee370f49ba456cc82b4aafad252936a2799c02b6ac94f3559585fe32dc1b77b1655c0dcf7d6f0057ea81071d66cdf321d4f155c6681734af083ecefcb328f5c1b6be00ac3ae4cd3388da9ddd3c1c96cb6fe66ab4d8d84a7125326c1a5587699f201d1da6dce1586030d51564406f1c02ffab287f6ac174a67f900c8aee47543e7a673b923bc513ea4ec2c6bd08cbaf99edc7ea56cdd9a1c873f80e19ef5c4b5a9bb15986b0a33aa8c17b3024974bc351de3563fd5b8b74e999229cac59147f821e9206a47948ec84d1e7ce39ac871d408fdfbf2af2b6a010514fad9a6dc72fe05f3943f8c171b560147bca3148c3bfc17db808416dcaefbb4073518ced706a81033a3922bd52ab8ef49f5d5028ab110b884cfcbb9e25fb33613f83822df436df244e704aa2c10b1769e306a327fd7aed30de427155a11b6e7d7f5f294eb200ec91a7727a415af35cb91ddac7251a02f58bc3718221244273a55aa6e2fe06f9d12f54536ee5eae76c6576d6cfd076d40a9cce235a79d173b023ad771563fb7dc9168a6a50eb7873d2af93eb6c634982c9a5e6087d7799fd2d4c1a19c1355fcb25d77a568cff4c6a567aa84e7a6a1f138c3f64868c5e61025484e7478c6f1e8811c86903a1c799df3e38fb6d4d9233cf65a74787444983c1b882bed17c7fda588d5bb708e8e3f5834409c56d73924e74fd4f4d1f87ca9c99f8c0f2b064e05453042cf206" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e4029ae41271eefd499acf4c958e83387f36b89a2084e39fe48bb53e94ce5f4d", + "proof": "cc419f6f22c63b19a4d77e7df51ed9ce785092fde046be43e96fe87690fc645000578b11d3dbac5b36fb84aa9cafcc8a78ffcb0ad2ce5aa9a996f636fadd2f5a825049c57a173a409a33501b88ef8ea8d886c83b6a6251067152dd1040d8f54b26a983bc9e0372e62332c8eb353c07afcaacf0eef965d49405d37816ca73bd7979f4bd13cad09e29a54334daaa95587d6d1f550cd9a605272e9c11c145015803b947ca0bb852236d149381b0bf6341c4cf8aa91f2710d2c899b7f60a515341085e9b35e61fb662a006f35c5487bafcebe276d220cc363463bd61e2fcba41380ff04a050e3f258079f9aca326baedb1a5652ce064393bc7531dae4f3aeea3812ec6e48407a83333de0fbc3a12aae1a25eb84704200769b6bf15ca82bc6cecff424e5b1791118230ac3a2dea7330af8758ea4625580779533be645f7408c03c4745aec2cde9f856578a40fa95004363c87c240611eefeaa49086872b08c0e740640a0bfbe48409ddc77e15330145773bfdec580f5fe18788cf723d98404d18bd00285c7854f6cff903bb256707cbb26f12c806cd6fbe84b5810214677edace0b52826af8440d6d630e4f9c40391d7b4f08573a34777e752a2ee40cfcc5610b0777aa9ae7524763efb68afa00a3a422b91809a1e7fdacb4949437be65f56520d8128c35ffbc3c239c36df334d2842df48f404d8b592bbc4a0d7ad8ad8701f41e816b0182e572772fdf938fb6e7a64885a9f8b082ce609a96599030525f2944b41798cf10e7de90fdfdcabe1c90e83cf7a2fb29276e88af16b317515c6512570525de493dd5e9d9eaeb60fb9dba667642f1ee727ef58fac9db9399c3e5bce054187a6ae04b47231c01ae2ea51166bbda85e4e32cae5ac5cf20a4190631a3132c4b0d5b4515df554451ad590b082ceebf81e494a7289cb6d987455b99561eb1ab0006" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f466add556a9a3cccd308abebb7d703207d794595036d5888ee2aaecb3e2f06b", + "proof": "6a8734d54dbaedf0c283e3672df249194690a47296fb5f5f15286e7ce6a7fd5e84e59c9c244f9141fe8c907d0513078b7d611061c3e856862973f62645161b2e561ff2bdd6892676ae84b64b9bd9f287dfd1bb3032942c0108d2c81a708e895ab8d3f478911d1f3c8356d4abd9a3a209915f6df73a10e9b09ac474bcec553148c9acbe8343c2c201e5e86f1680690b5f5417240d0a4e9e3890b23e4271972406414ede9f3bd69752c7c0da7bf4def4fff9aa1bda6d8587fd90b19787e016150ca1427a6ab6e163b350677ce54a0242f62a73af1c4e4775c872239d83cd9e36092a871ea1fe7f5a363fac09d114be9dd10577984d788497799884fcb37a8fdd24900e3fe1573f37f7c315bf4422a6e56a5d147d735cd2537e82b0180444418f7b4234ecfd042bc170b39986417b2d4f58c99de9b2ba7482980922b4600f16677e2e1c4768560adca5d036e09d7bcf762f78b1a6f8f5b77f47fa2ba15aa0cdcb7ec647e75defa06009acc2554c21e4ed13b305ce1b50e5c427e7d66e61d89993671a55aed43b88cae633f486d34a9edc3c9e04effe153832e1e97c28a1fc24ef703cb333b3b0c3898bfbf38fbab89462880f60d6a0cca08af956c7cf588b2a6746bcbf77684bc9a8b17110347f940724a8284c086a0b66684e346f05be9a407f4816e98a2d1b6060226ecbb10755297ed8950039815807f0bcc4ccfe5c3524cb240a60d92bf84746b93342bb38e2c9bb4db19b627f648b0b65741d97a23d8bb8456ec051baa851c77a8534fff291726e4424195c8ae81b0eb840ca23903571a60c5c6dc61c5e9237f5a63f49e6067c01114a822c8ba36a70d091e0280ce4dd864a16a3dc38ba0f73829d1f0b2d9b54ca8efa6be909373f8e23a26fdca58b250d078c2dd691356382ba138fc5d79d7706c52dc970a8ffedbf09d1196da08658b00f" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 27 + }, + "commitment": "4ed296117b89981ccc07f8ebe1df9e18053d5e9e91b898c0629eeaa1435d3776", + "proof": "46fa3bea6f58fb943bd2e67457176bdfa808b1df1fab39ae3dbb6d8100b431773e9463afa8bdfc333f11395916215db930e71755ea3d47656c6cdc8d6279144de244855c8c7fafb4dce7431541550c797ba0d8574783063f6921cfb0719e8f4d5c8b4a960e19989936b698fdc31f8a9ddfb80cd792ca8f20cc83dd43bc5efc47472b69680ca0d890db2210194e5825ab2754fe43ab375e6fb0589e4c4812650f259e12bb6389923a48bcf762a458ed46cdae503b37222fd1d1af5905d1661b08765703cd20f9ffa07197e327999ad7ae62d156d13053f8a72d975a4d1b26530bfa5c0266f18772eecf5381c29190cf0446726c9e76f34b88c96214677ca56e116aa8f7cf7e0baa4e6d02ac4c56d58a3f53e29c75dd0feb23dd0b643051e7c532acf69be045c9f9097b80d9e2e4b5bb159addc70d696f48a1bbbc33999fbd115a4c2f56289649573afa09082539e7e2378374d9c9db65ceafa3ac7f9230633e74c251422592203cf990e47597a9b69716794075dbbbf3229d046650b453cd1e2df2af0dbbf1ee4b8122c22dc3ca0ce02899309d0d859a9df9aeaa2fc9af42df70ce82e4334e9f81d65a31c742eb24c0d17efc96306ac909760dc34ee48bc39123befacbbae51dfae86907a1b99cd2256103f46cea3c3bf135afa069e677fede6f68d5e5ea16a85c7e278abfc2bba31eaa050302d0cfa1f102426b7a900410a569dce2bd63de424e6c47997ca878659f54f65e59277805a30849f9af335ce2bf733a96c9d5b38bf325a2bfdf550f1ebe62306afe38e5c3e4b11dc0210fa34b1706ec9f2b4e52021dd4424d29d3daa1c6ad8e4c5983e7ffc5ed7b75ccb2f33a0f6f431e4b42592df6b9d5971302c3c11e72b87e9b41080ee12aeb8cbc1714405c0d1afe75f23edcff0b601911803b24cf152649d978a11fe1855e20761a91c4c80c" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "268f579e654f6ce699dcea50958d1df035c8f88dc8c4fb50463cdbea637f0278", + "excess_sig": { + "public_nonce": "ce1a141e79d803bd6699389c54c4916c5249778d7199949da6de83ef56919c39", + "signature": "a044c6785588a916ab65a6ac019d9b14f9b351a6600b2b5838471df34dfee907" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "2e26211d880227ae3be09c290e399b1ae1fdafd09830a2d70f44bca2beaeb802", + "excess_sig": { + "public_nonce": "18d9319defb97a21f82860582f27a36539fd00805d834ad9164a847984188075", + "signature": "232449dc618f095a8653c7fc6be3a97d227967b346c6129fb45bb5198f54ff05" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "96d87822a57dc125c9adeefbc287a150007fccdb805094ca5f19dfbbe7543f4c", + "excess_sig": { + "public_nonce": "8610ae78fa07fe3a55eb0f94f7b251b1943e7cb201b36dfde9c5343648a6fe7c", + "signature": "101c79dd4eae4649ace53cb923d3a6adc800c490db045827bb8febadf09bff02" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ac1097c15d7edf13c34949701f1315068368ab4d25852992e084f201a6b1c86f", + "excess_sig": { + "public_nonce": "509640a13850601d1c31681b50c3082aa5e12973f6ae2bc7d27f50895fc6654a", + "signature": "2fab39d1f81860acc5d744985c9f6a99f1a49b702a4e3d0c3959ff6cc301b503" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "e6b2e2e50efe4a3757b7d08b296a2c9cad2c56cf15155690dc777cd23e1b8601", + "excess_sig": { + "public_nonce": "841c1725d8213da80ec218e63f5a0c97205274ee4743075f515260531e455e29", + "signature": "484601febc8b06f80e831692b546702ef7958d09e57a27c75855bc940e4abc0f" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "6641ad31a0678c7f857a89142c0234aef9e18c847b3624b735fba9022d387926", + "excess_sig": { + "public_nonce": "381a775d781090f5d755497b4fbbbc33cd76b0ac700cd46be26d4f43d1c67605", + "signature": "f3870af86409476daab623d110fe6702b94673e84d86ee08e926a61b25925201" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 27, + "prev_hash": "0d5098ae22eb41dfd53ef690b64226d04d49b92ee9feb1d38d9b0b920522f78a", + "timestamp": "2000-01-01T01:28:01Z", + "output_mr": "a237956159e33111143936713b92b0e4dc74202baa618c3323b796ef676da8cc", + "range_proof_mr": "938b6733f624611a39d468115445ed02feb37efe4ebc5ed49ff81a294c3cad32", + "kernel_mr": "7665800cebd898ad984423ace5933758b18c49bd2846deee382c117e5c498c48", + "total_kernel_offset": "17a62b64204423caeb1c0efebb74b8f808f3c3d26b042d4c0c79be4f90383500", + "pow": { + "work": 27 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68402edd17e210bad42d074496fd49facf33b35abfbf8991e760d3ecfb11b425" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8e741213b2451177b649812f8b42e9974d4dcd8f71326f604283c1e63124e750" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cc08d9421073875d06e8d2d0f7085ff86daafc5f57b145cdea4d6dd8239dcf7c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d0ae446d3d3fb224bfa4fa2830516bc11c954870e0c8095114cc696df0e4b707" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 14 + }, + "commitment": "405bf2c2f984f24bfac6b242f9b51560b1605c2a110ec907799a3882aa670e39" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0417509fa1648e60c22124e78191c277652d4c98ce15406c064dac871140d877", + "proof": "e25d31a661d71b7a354c9e0d65ab6b772065bf7ad8234888d4bd57cafb42f4186e2e36bebb8c993487f1399417781b807becca8d7c0b36e37c7c504371f39345080e3cb70a50df82e7ab3fbd42e6768e455171191d04fc37583179e05a9c6430b8fa800eb6daeb7a51b6179c77410a847eefdcd5763eac97e54f7d223131060c6ca4f6fde27c916263ac285183fe0fa4a70085c4561cbe931ba4e25a8cf1fc08d4225c7eb00a4c756216cd23289ce1d5513ad617746c016c533538cdceab02053f76fe9d5ab9a4fac44e68a624354c86e617ae89d7a43bed579899efc3352202de5d8cd8bcfe61dc451971903a2d2835208f0697700dc9001e560a4c81c20d34ac4df1f26e56f7896e601f2059829953e2988918c75800b22eeb3f53d1bcf02aa07bc6b797b8f678da909827b78ca6a97de80dc9417dc6fb348dde9d1d66c3289028eff726a91b3c5a51d410ca7c8f52011f1ffdf0927132a9db555120e6fc01fc619a3672f7b1e6d28e985212c15becd68eb9c0841c387f4b028efe885699014880c11721a1b5a6237458ff2449598c2f6ce44bfc4309292670563f43e7975d52aec2dc32adca801724e17614ecb774abb0c738fa07032860ed15727eca5f352468a328ec3962cd6bb6e35eeb86561d4731ce9c1e91ed6008f245730f5c845fd2f1caf2166c01a76112727f97479dcb35630415c5ebffc046f8bca74a47e312862ecc4f4b0727006d73fc1c1d9cc961256395bd6acaeca43edba2894bf5a95eae07d62ee3cef6ec5c982ead7bd1ef29b9f8a9eaf1f62c6aec9155b124f91f4d32312e0a9e52f1074cccef34152634f4d7a6690287f210005b78c82c9289815ba3ec523b7764a0ac99346bcd762b0c7e521f4dd1b7c96c42ef24dc66d60cc608a552790bb21ce501277e3a2e3a4682cc6729f1db4aa58ae309a4b3cb207e7c00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2c73196bd7bda160056edd8e7ec7d5ec5ef3312340ae4489c41ec3285b8b266a", + "proof": "b859016335b5a1ad6d25cc6cc7377f445fd09b2bd43ecc9cf1cc1798eb50323f30ef1c64eef98211c5375fc1626a299ccac5f0393dfac261d512265e26c0427a3aa8c058a6676ec523b38a23d9345e7e994c2eb2e0cc32b933454348e4aec32cfcfe55fb2eeb105af1a94014e0503d68d88cd890b43e9166de86ebf876c40565d2816efc029f186a7694495ac4b8ea9596fa865ab047cdae0c31992675f9d404a8c4917f8fd394f373651f75ecfd4cc4ce3e350591bbb2c9a02234c6e01dfc08c4db64d36211431018ca9efa2e2ac91ef66a74eb3c40e6398dd764ad2bc4eb02804044a7baab1529aa241662be7d91196af6a9ff88b37ce2a2040ba03edeb71cd200e17fd450dd4497df9f237b6df58efa838b7093419651fe2ff8ea1de5081fd86516e2e270ab58df816a09a185620b65cc0c609b1c15996375cd70bcd86f595c04d5eb84ac80cb08afc53a12b19722272046cee9682c09d8a1a08796377233c0050502ac4b084b889f59ae3688da80131359cb7c7d023c3054a9496def83153c94c962078218ef8919f893b203e8c8dd49d90506f817379ca57272b000173ceac056553335845d302d78258627e82e8e44938fe6d82b5a5e67589272dfcd6bc8d70f8533ccc3e8a1d9c9d534b4266fad8ad09cbf37aa9fb0143cd2d16efb50a87336fb9197c864bb57f3242fc48446dce163e212fe92a3a748f548a265035e1aecc4f301b7a4fe6f4aff26feb337e7360ee32488eebbf4545c020d4c936547a2b758fd16e907201d622af7b3daf08bef65ded413b84cdc4ab13dee5dbcee570855a5bbb168d2c8a2377ef4e2d90961db4d848c2106fb5c5a739e6fcb9b610f3add9cb2caff241bab81fbf81b076212feaa0c6ea95ca72c248a6ae54c7d7408da742a3f1a6317543f7597d5a3dfae090bfa33be23766b3b18a05ed5cd0c9502" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "34852042f345a3a72145e17f8e26369d1f7cfab55f9124e21a3ca8cb53604909", + "proof": "8cce68fd9e54ac46a40d42a66e227223a4152c978cc8e9ec340b1b211f7b307d2804be2c5b305283673d32cdffe0c1d052031a34858001b21b44f5b4f3b8aa737011eee78e3cde807ab5c5af089556a117dcd59da9cb16887826beaa0965eb59387e9be82de38269c2ba8227e2b6af9087fc397ace34d88d37532ff0d512f842c99d4ebfec699a067c39f78ac73d56ee5dccc4bf9cfab53c1815be3b3a208b0cd4f66d08a736771a159b702a0fe16988adc56b3a53f27f9cc06741c91129460ff2c82f7d711b1dff5a34c5251f1e17145f13f32c54a02ed773453df67fd7310814ed7de952bab42a5d0bb705aabcaf06b5faaf5ec9886f2dc267f4cfdf3252238ce02651232962256bb523cd9a3a160631966a08e61d64cae914288e69c46533b2db1ef2937fb8b6f5ed6220dd4686964214ba60436dc868ab8d1718e1e86670d840afba20e82aaf71121e94307c0eb0555c7d1551e1a8509a2956dec245eb4c9cf3aa8337e9cb87f6cc6a9a9560dc045eaa4bd6e756072b5bb4b016f7c67412aab8f83bda14089b75b4996a0d2c5229d3700a6b4651863baa98d706040c22348097cf87213d59a3680d9b9f5082cd185f19b639602555a5298df0190d137401100f5e4c213e674bdcd622b2514b3e2029cf3b3a6dbdd5844c9ede2892a139460a19d9e5572993a51d89a5265351d55e80a0f4bd446216612199976dfa603952621cca9a6378222b8c6a3e7ae59af0e538cf783a80aea0b1fbd36518e26ea84a02567665a5401de8017c45a5184c82a2a433a12f3430b40ca4a1ae2d364baf61d666a13b985101d10791338bcf7ce7544ae521ab0a90e3365aafc7123450310ed284da201abe68049d07bd3075e31a4682b3f50c6ca11ac898472faafc33790d39dbc859876f0ddf43d6b114d0603e05dded692e9e43bd8aae8cf591894a3d01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5a6453c63caf6f74ae7e64ca406bf1bba813b323c0eaf8256bd238da2908536c", + "proof": "160adec245e1f837dcfa1fb3b566e0aabed40c9421b7dce357ed2ba86e67c84e488ad62465bd12d97d8e163b337bb5b5ae846a3044a8461fe4be2e16391646708ab5209057169c2973db9beb8b2df580215c796c6e8daab818f271ccd47ad528e6bc9c64036daa5d2a7d84af5f041047e0dea1a7b163b1083dfa822413a5ad5fc144a3f19d0b8da8f0358ed6b72f9e540398bc5568c0cc1316ff85c846d3a10b8144c0702c81b6f801b37f7425445a166727f7deec42d4143bea42aa7e126000a0fe83c47b8a4b249e866db59e22974b1b7949e4fafc4fdf53b9ed5b4698460a441106ddab55b3dd2fbefe891a5c3361ccdb0e3592351eba571c4a9ee1ce8874aefa33bfc2a601db6c4897c4df793aa5d1c470b4cad48f2b872d4a35cc04b05072e47a3d330ce0ac753718e8a71e93e17feee80ab2666956906b0f948b59712616d9902f4d7f28381a72fa58bc6deff111d428590267ff8e75648c1f743ab2398879847cb388094ebd7438112666262bf4ee8391399fae6d1544fd0a30f88b6d4cc0ed32f1776caba67eedbdd748de02dde40ec340e955574386327fdf5cab76b47e9b4fb80f7a2fe1a17b7f2427c60a845932099315877d6a0c5fe2587fbf03166bc54f7d3a9f8776b3ee02e801df0f2f8d9456dc20c67d1539e4435e6f3215b687a0d6b380bdbe3ac228d840bccd98217e9dffea80e6b7d5ec18b89d946949644a6b4076db855ab0c6757d7479a58eb4618feb846b38a8bcd7801b688c1a37da3c5b36130563413cc36353ac0001e4813f7f49928e4e9b753ad7d2e7573256203a8e0fac1c509e3127bafb13cec40c5c1dc93909eafaff63892288af19f26b5cb25658d65ec8e6b40b8527c841607cfbff00ede11f38ef805bbce5551c1a0ca7e2bb16ea91445329d3b984d10637ac3659d638d6ffb323fd2e2c663c11bc0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b0e05b46ec08cb25dbb532d5048f077ebd814a83126765e775f0cbff04baef33", + "proof": "daaf686b04ba49de56fea5af459f2d72f4836947a023f07a19b5113ae141fc5c22ee7e378df51458852f3d584a9f2fbdbbc61a3dbdd9818d628b96ad119a384f5ed0564fbb415401158c9ec1900bed04d4e1d28d533b47a8a607a7980b958f434eed0052799d5f7b289e702df3b8009928b9b8a8e3e16853025642ecc1740247862ea210f34cfd0fa6bb52065b338c4d260c724fed37531a7080047d269f79065367083d21cf3155df4db7b3dd6aa8cd4e3d98ddfe5fcfa602609c3528deb10636a15a1a8bbe0f87bd9220542956e3389f974374db962a98809417d493b5f303dc9227221cf89912ba5e3a91f3690038e759edf81303693795fca477ea2e6862bcb5c3869d3cc71a14816e81884827459c37591cf7bf641a34f543ab3a010404e25852cbe2e1d67301e9c83bf481fe2b873c2e4c573732fe0d995ab588e6166d046a1bd298d84508fadcc0bd71a4240e3372063473f31cb207b62655813e5541dabfa2676f7217b0f9ee85a3a2626dc4bf65d10b993bc533df0e953f0fb30830a6978a139c592625c097e5b0507f025f46210ff070e2f88f8f13b382195f5945606bd731066566accb98ac6304c5447c727c39487e2b64a2aef385dcd26ae80fe82947282b079e889aec337b3d23a98bd244ef337dba1c255ffd6d254ec59d520e0568e76274669366b40a3eab83132515ec1cbe531b087861a63b345aeb0302107c808cdf7607e696dd8d1c31c52018078d07e4fb334f931557fb14a811802276b2f0b08f6f0e3218401ea13a1f4ddef69575d4b8322e6661c51d28d1036207bc957dcee194eb7ad948a6a67ef329ba7367938ff86f0d1e1e397138af73131395747285a8660821962c4a1c0bc0e5dc7230593c3d9b3a6aebed3db1bd26fd0d368aa6710a80a4ed8934819ead49b1d7b89e88a123e5dfe0e2abcbd56cb3800b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "be21cb182a38bc8c5d6ea15a0bc130179cb4792e70f0c35d194c9b4de7e24c37", + "proof": "d2562320a02c165540624e7e7253222b6ba8dc2b2720d4434031a27a4676f96d5c64751ae4b5912a8d52394578885f4a183a1e27b369e17d13c8dfc357e21979fe3b1b255f93a1d9da5a6f8271f1e319ac623a64b95da1c08c7abbf013ef4d20d8a428b580c06a3efa8c6a2f5a5850f7ffd13e0020907933112eef1c3b42a93cc11b36db3f0a0866b32a9b993a5e6eed8e0bd141381c6ce102cf377c5e7f050967767f9243c4ba140130904fb513b11e17f36183cc20ba720682a585565ae904d9cfc5dd970ac774724c735f3f40df1648e72ff113f2af1386c6e0312dfe3a03f282a38c10351ec6b37b1753604a570eefa0fa1befe4e16c1d46026b537c480e90d98d4d30310c5270175e58255af09a99c15c0730c709434650c34137290106b045ded0c084305c7d2790da54d5ee63d6759a7252bc442f14386703b28bb00f006f2e5a3ff95855b67c315ddf1b1e125ff125417cd1249d02348f5e257e0a54ca794bbb088e56da6cd833464b67f22aa26229501c5ee41f42c351d9c528194c362cffa8aee062dc57d4b32f8b69403af908c6583e9ef26f3cb96f027ee58763827652f1a8e8eef95a38f7543ebf38864f7b1721d3bd618a3bb977cc535b93195413960fbe0f38c98fc057723f80e6a284d6203ab98329dc2d6717dd846c45153ab73ceb8c29cc84a97d2d1907a4e903603fcc155e426e5bd3c4e9275988a6699e85eac5236bd65afe233382a1fac3f567078c9d6dbd3a756eb5095ad455a90504c7f985d5d8a45fb24ee954dc8a7199e33a751a2cc259bc1938fcbeba7a0b2218c14ef39f3e8d6c45bda855356cf099e8e73d91d4d461eba56fb8ea1a2c821a20fa8cfdce34c35a1d17af697f063ff3a576080a3bcd418b37973b7dbd087507ae0224323561f15faf774ab817bb22d4a2e4d3acbc8c193e65e4b78f1e1a9a0b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c494b14600be1cd074d77d837faceb466619c573efc76ebb5de3a95faaa9ed3f", + "proof": "d277739e92a6ee6d30cc89316926383797d9de025351ed1749ae31582ab7a219740ec7dc8ee92060e3e2c1e3e68b7bfe97c82209cd72e2551d8c92baed29721b34d2addb83e06b8f8aa0f7ccbd9508fa6ea0a8b14341dc57dcbe6091c260e3193664fb7bc8ac4a105d5df93a893215ec2e8c7ffba0bfb9c0dfbb959b900fc23c1a0687fbc5d96a48cd94cce70a436732e0f4b290698d67bb406db4fe2114dd0199b45c854d427c1a9f753ea0a29fe7823e05ab670c5613b8413cf447e2f4730523febfda8efe9d09bd75a83a65e192f73558ae913d0429091c03919d20af110dae8c47f087f57bcc6b3016c1db440ef7bbffce6b5f25ef89a147649f995b524c5cc6424e4a21e5794340d6aada8f9580126c8361cab408142aa88732fdccf5467ed42a113e829458d040e290f5e643397b3decf24656bdf2f3d93efb5ba3576746f10fdf63dabf60615e0650a4d5e1d883a6a73f09ab418a232e93f8e82bb359248ba0a9ba7b2f67b6733c45b9977c25e53e150309b4da296c92e861e3140123a47ab0750c1075103bbbff7beb83aa0bbf5a1f5878a570d34e98205550a1e92d1ec4dcbbdcdf355e32f5a48c5a48ca4b44c49e14e741d28e2f7d9c656b3958181abb259600ea86515482ae791cdb7f5dd40440d0b0395b14a63d55dfc0ec51203aabf803d98597d7b741370cf742ad24027cb9228589af630f31abafc897f004a08ebe74f0e4fe516f8396a949de952fed7e91bb25523ae173108e833d2be01290d7c3f90f05dbb405ddac4d5b6ededca4d904610a304d39be87906eeccc7058c29678986bf9b29f5e9e262351a1d7667ac45f332ceaf04f054b56ad222d327aecb5d1f337cfd283893ab6aed51a3bf7a42815782faf540eb09e503bf1352e074f619dde074583d1d4b37d47ec6c9e6bec3a9340bf05da026e652f2351fa840d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e8ae31ee949785cde2c8a0a2f1c3a52d0716c397217a17d354d10384278a6741", + "proof": "52b4630d0e70ea078d256067d73e21fb0ae6f8bcd1fd41b4c8ec5d284e029e6a3cc2dffa7d7c296e0fffcf1e355b07f23c5f090c571500bcef2e4e6ccca1d94c64fee11fbba24f77120016b1c7229d4646fa4dead4485ce5288d61d856db175c6c602c33a338fd07603e8dbcc253cb60b2f6ee5d65e84d459609369ecc48d03e3d9e40d60609738f2da6e0fb2089aa6a82d62ce76dccc7d686f7dd8ac5fbe20914bcd29aeaea7172b5086406591d29919dd83c4783cb2adb7527e11db3f1cb04aee1aadb713939e8b1b1d8eb101b5713ddc96574286d3ea2241ddd262974c207a80e07c44737828a414be1845046580b7cc4b8e244f2a211f5ba115e939b614c6aec14deba54199aa2e80672970e3b60c5d1b206b26e544ea6a1445a03ab3b3ddeafe2ddf022be47bf617103b7820089d60f9b15b4a9acf0dbf43127bc482f31da07c54da6a149ab413f58c1c5dfac5cbc3bcfc2c76bfd9810931609df502f0c7052e01823248cf781af82da377011955efb707e54f5a3f82d1562340eea1f7e3661b8eaa74ffdae365886292dd81fa13138d73d5cee5f12a11fe1a18d50580000f662489f13dc1886837839abf970e7c4d3015da7c2b5f8e78662c66a809a659a0fa7c24df2825aa4a1ea4f46b397c4b5962c70ac3b2bdf945631babb1a385dccca0cccdae3cb248f9922480dd235276d42d8ac3954632aba504dc5364342696ee969a64b6293be980500a247e11a119986bf535b955d82972688156a18cb400077c348e70c8d45b02270f9587fdca11bc21134c77bcdfe2c187d5dae616a6368fc53d70bcae12064c902c69f837deed6c7a9839955418caf05fb2daaf1592446417f765848eb7b0e82fde23be90f30829c720ee8730673221df79fa43cb5067ef20c1aca0dbf506fc2ce3b1f63ff49bdea25bc604d8a78c662a99722596b06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f67466c12f7de48f30f7e0523f57d32f75356fe40132b288e37032b22f1ea640", + "proof": "dc40571afcddff6e7a423d681ce7c89c0d172508e73a4e250e6673066dc5063c76d53fa556a605ab0b42142f90428fa4847da05988619f5d5d98f94477f79947d6c3e6d35463977ffeeb0ff06490598c8eb405b951e828dbbdbfe754c7c6826390cc93f0785c1e071025eab7ee2b99ec3c11556c030cd3df97b4f3d91ab9657d5e5062975545d23426d84ddf7a0261eb428c2c32b776e182610f70fe62b8d905d2a48ab9c1e137cfffabdb48de96fb4273d5560a29b52ec58fa63da21708190fe03aca2a68de1e4b4d71358b3a3b4cfbb777628bb65a8ea018d9c8bcd4c5fa0372b22a54a656364e68c0ebd424d9569c057b250d615dcb0991cdfa3473e8604e706ae645f4b547a9ef9af55144b37969362a94be05c02789532a34b392b3ee1ff61c61df3670346b5170bc52426b0686227cf9d54355a80e1236a0703adc146fe41d6ab9a70d55b438b4d411f436d0b195542df4d9bd6049b9f22714d727425f20810e792574afd1ac9ed90697184d5a6ab9c65cc2a6ecdcfef5f12ea8a2a971dccbf8e5597eb01f41dcda964fd2b48490a1f453152761d511d8076ef63a554c069d56dba25c23589697e6e7330b3ec7f1013981d82bf7a1b8bdcae0aeb8a05236bedd2a976719cd0bc91b933dd23020ec2b560f7c7568ce9f527c98dfbfde5170ccd77d6976f0b9e4a01fff6f7f19a8e424cb7e3f44eea298b094ce64b76e12009d690e95650fd841ebae5383f5dd7ed74bcf635b6973efe827cead12ce8a35c8727410498206489709a41d7ad68984c5bcbe4f4235aa011fcfd50cf79e68261a0c73d20eb319f733f27fc680bc56ad3711abfd6b46e49a01c4e4859b7102565ad85e1948aff6c18bea8b0247a0d871916387e3eb5e6baa72f0593466ca590df0508e4b83e3bd66c3a289af8e116a31baa8e35606d63c6a8ebab379d8a3830d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fcc39fef062cc728481e13872d68aa9ad6a8cb3e1a2d0d175fbb1acf0361c776", + "proof": "a04a037b08340789ae986d28e9ff397234646bbb43f6f50084226df89f2ecf024a6660d3c3407a80c0aaa36f8b23b8788e78eb55f61d7481f0b5e7deb445280748a9f8788b8f9ee17575674414e41917bc524a0fc15e0d2f13c2a3f35c27fa4cb62c12661fa1245623c6fe23d8a7b22f000a1c1c987ddc597e2b46f9d086f618bf5ad45c3326b5f4f24f61f57ae922e366777bb3ceeeb606cb87b55a2811460c790e062c2e7620daeea0372c177e832cdee8cc6076301a2e6c1da78499d09c018125a127a9a91d9ea8cdad05eb01954b8567c9fdf6199aa04ac3e96ec99a630e3a149eb31f26760b6b9ba5f5c14ba9f607bdc4c237086adc7c0baef60b0b5c4bb207e91654f797df81748a0367c64db0859832cff6ffb141ae217b72c9d89914141a7bd0d94c8962d4b87c204af1cd7b038d88bdcf9de0eaa5174124ed22f874d29cc7d9d80aba5cdf57125b8f6361eb8e24197ccdfe223a9b7f89c563e8c706a04e256fbfaa8b9a5f60b3b98192d3e094d33cbaa803669cb53d390d6098fc74ea1e3985ebc26b2738ad41a82736f097df603c2152738840da8e37e4e9bb994b1cd4cf0bd3afb42229ef29c03bfc3d6d613a95bdaf1e97ca2e36b35c1b71e15c6833af507422c4ae7059fdeffefb29910d86d50a6dea74e3754ad6400a546b6a68318dc5fb19c04cf7f8494ad8d37ea8a3c93a4e11bb4774f0f117fb2f84681a28a1f491c27893be68ddcee314d835eaf23de6085e1b9d58bdb5b1e8c250267012b9716f5ebd84d8fa8c4ad18e3f78da6c0eb33859e4c74df7b5b1cf17bd1b5e2e908d270f46affe17a08819c498c4464ebec61f24eef0d5a72c6d528fd53657f079274299d8ebf5dbfec796637bd66a3df262dbcf54b78ce07f8eb175ddc701faf03962bca9f142e0be632f088fc5dbad6d3c4e27bc477442fe131b6630c504" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 28 + }, + "commitment": "74565a884e6e38e2e727318fc6ca629f4e929715648264d262a3233e3c4da97d", + "proof": "baf2a41eeb8b61bf4493f9f75f447bbef4c9b9eb9678a01c3a44d251073eae74fc5120ea88bf1403cf0bf271b76e08d0a00585d14dea34878105c1c89d59384c58b70116b538ff3157972a77a790dc324117a1d7c64708e4cf7b12c32f39054b84a64f7fd38136cb31c6221b46101d0b6660f9f1e3522bcc05f7b48ca32d5028f04882e52a28330e513099500e7d0c424195c63fcba6cbcb4900092343d6550e733b904bbf2d8ceaa76df4398e695bd137b02deda4c1381c38ee9e6f91522f04de5ad2ed3ca1f1872ef5a74342e769ee239701440a01d944c08cee49a44405057afb3cd64cb0892f2f27a97680f1c4d66f359f7721e6f7538e72331b39230970d6251d7992e666e0b96fcf885618707d1f3b8f21722957adbcb8725833026802183a0ffa0afc1b58abe6658f877e3a367e95f217ad1e725e5960aeadd4201c7d64c46465de293c10e5b2e14ae1ad259f4df0cd56a161d9e91c6f59030115987a68c57e40a2de132c40a0ce592b35ff0453bbf8715567f424ab7b4556027ea05d14bf577815991ec0570151e42557af5826ecd32d1b1a0a6fc31477738f090b197889663bc1c2b2c0cd7ad450d4b57721a4c0266bc979beba4c01388dabab633f5c954bb31e2d6cc53335b43dfcb8b0c62e4f8b95ca8c2f87183d5abd85a5a40ae6b83140c593ca9d30282ba6c0026ccb3896afec4e15451428b51d61576129427a16782f09cab71fedc939af4e53fe66801327e07f7ea75fd395147aeb16d76666403544c92b1b1840e6c34a85ad72d4171da05bd94384eb677216eaa7649b072e0e0dcf268dbf19ee6da7cb457b984be554b67ce86dd5aee4c49eb169d5c82f2d8d1887393ac11c6cbb27f431dae964a078368443feceee644423bc429403096fcd3a834fe7da4cffbec916fe23c8e6a58badb323ef969019597148051c7400" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "0610a39666e400d8cb2693d38174130de496fa1a6afaadd20aa64e9ccbe26a3e", + "excess_sig": { + "public_nonce": "b6c570f8480fac8707b8280d9e941aba568766ffd35b63c5f4b1dc320c8c5948", + "signature": "4c781494e4374a34d5ae38d9afb779f74f1e8df83a2727733b2224c0c4eae807" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "5c9c3f889cac9086d7167bb473f608f8ab8e89d900d4d8b40f164340d31af64e", + "excess_sig": { + "public_nonce": "4220b3e5bee01642ec640e62b285904362fbd6e55bb24bf25f7836fbdab7b119", + "signature": "6780af90b89f8dc953683236bebff0751099b2689daa1eeef02bba747b60fe0e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7c63b301ca24b405348d315bbc70d666883d0d58503ab2d679fa5b389ee3507e", + "excess_sig": { + "public_nonce": "f4a38d631b07372e6dd111de4e0393f6b6bd470bfa055c39b6fa898ebc393f44", + "signature": "29920eb5d5e6283c02bad4013e23862680f08834f67fde86e65fdba8fe033301" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "c28d3897c6414761febede32ff8641b8f36984aedfdc44bf85734c2b28e9a47f", + "excess_sig": { + "public_nonce": "5c854997e5efdc396f5c82006304e91d2b0e91804dbc8689ed19fae43a8d6937", + "signature": "8307c924ebd89dce46cb45be3d1c0e9b52b34716825e24c085584df550286707" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "f45ae8c9456af4c1760ded4c88c8f1ecf39d98c2cadbaeb173a53b9b0801c335", + "excess_sig": { + "public_nonce": "0872f9a9b933300db71a1d940923b86c967a178a1af9f0df56689a28ea953457", + "signature": "ca30e0dc281d9da82b215a54f98eb8967c991e98c027b8b3f6fba663ae621d02" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "16424c8924a2ed5e5b3fc688fb1792609cd948409bd8bc53b546906e7920584c", + "excess_sig": { + "public_nonce": "b691f2f22dc2ed9fb7b54950602c4bbc6b5433455289fe6172bedf053c9aad22", + "signature": "8f837c7c5a6f93498a873f9589c2e11fa2e1713b2c392b7043f727371a6a020f" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 28, + "prev_hash": "fa18c7ade1c7d0811ac711006b3d2571c38d7bcdbad62e48a4ae71acfae9b697", + "timestamp": "2000-01-01T01:29:01Z", + "output_mr": "17f07484e818f1d59c788c59d61cb06a451219fece925145c91e77680e9c66f0", + "range_proof_mr": "2409ea017485cf043381af40c80239ca6f28624d555a958ab17878c5cc3137de", + "kernel_mr": "be136eefe6176abaead60ef6eaa250162e3352d46e43611b674ba9ec1a073349", + "total_kernel_offset": "c7eedaa70c09244b6379619db013bb0e76c120b670cb18b4a3e3ee58be428c0d", + "pow": { + "work": 28 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0417509fa1648e60c22124e78191c277652d4c98ce15406c064dac871140d877" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2c73196bd7bda160056edd8e7ec7d5ec5ef3312340ae4489c41ec3285b8b266a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5a6453c63caf6f74ae7e64ca406bf1bba813b323c0eaf8256bd238da2908536c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c494b14600be1cd074d77d837faceb466619c573efc76ebb5de3a95faaa9ed3f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e8ae31ee949785cde2c8a0a2f1c3a52d0716c397217a17d354d10384278a6741" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "10c0f7e11cac4186ff35211e604a132c75bccb33910bb4d3e75246ee3f80a520", + "proof": "b2b7abcb2c5f4e03545e6c472608ccb3eca57fc5b8323306eec471ee2b353057885b9b7781855508eda632c78de5e64d9cfdf9f116531a9fbcfe3cf3917f52431499dffcd06cce72752eabcabf3e6baf3b21961003a06fbeaff225e5b2f5116f3e5b7d0644f729059eeecf111a545f6e1988df02703621734d89a22e7ec71a59c3088cda979ac22ef9fb3918ba886bd5c745aedbcd52735432419ab0c6574606e8df586c286206f6f1edce068a91144bdb92edb3910baa5a9d3f4ebf1a0403023d7cb23178ae4275e6ee66849e0a098d957c0627fcb1dd2164363403bfa261042a61facf03f80d122ef905bcc8fe04395be74672995430972178d37619fded3fcc604369be99e6c9e8b6a2f212ebe44638811ceda4800b87dd2140fa26d8496d6aca4db5ff484abc9c8e8ad6991092bc9616a6021698b2ba52879b92620af55c48876e08f69a8e26b8387a2e1b378e68c085528c3f2c323e3e2d6df62339b643f8179d48ec89943dd5aa42d8cf04439b40cf46b14fe5dcff173b5e1d29733b53a8db4899ec5f4e7ffabd5453af19342256a08dade8c6e976d2d2088a2e40c3149ab6514cce2abfa2db555c624767b84cd7548046f104c0afc062fced6eaccb1b224ddc77d975a534a3958995dde546ebdf5882d89ad3debf8081df0e0089b727a820807fc6946488739be6f3e7e50fab7868bca5c826633edfada8647087cd6a5a8e8d268f1918665c0ca8b9e3a37141ea1212e0393881b71901a0ae76c16c715ac9ca3a678df5f81371c6d5430aabd21698c4346bf4d2b294a3a91b60252d46b098d59b346f2d86583ac5ae7a4647ae825eba4b1b59aa623b5c6d68a6fb18063204ff92ce4d2d163fe4ea9faf448be9115359f1f7b7f4137d8fb830712845004a26f9fabd0f7dacb07718655a8e37ad44275aa1b8f4323d1b271a719d9e1109" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4840f13e4a0550294bc52a3239a57bf646eded62702d039d19589e60df658956", + "proof": "7c735ad33a0e59dc1bef93c416eb1e8c0ad586e3459b09b9390c62e80d9e0c5f76ab367939b83e6a70fb3c78d503c1469d6941613dfb7ca0a35c6a310065017bfccd694e6068ba1de92a8c342d34be3377f0c0b99e33cd8a4d42331326240b2e88dae8bf22268fd6d742f9db1382a0424cd562624d2497ce9b2449a35ac169066fe7e9cbf794f65c4f3adaabbead2320cadf8103321de421efa5fcf428819b079711da232f9d8aa1d7fe62a60068a6e1d889f887446ec6d4792bca2bf29b0d02b729f2f3cb192dced7bd1ff132be4f46468a27574202b17f5c4ea5485b6aad0a1a06aac5d6d52a2df2deded00e83fbe19aa242dd0f8439145c2a00818ec4045dd4fcaa29f5b346ce6e9d87174c16fafd0094005c1ebfdbd169301a80b1074675ac03bb0fac2c7f2f16e0787fa35618a99aa5993463e3cf9fda36efaed89e8a6e1a1352cf928eabb59eb4107fe20172fb7da54cec9e1fc4cd4ad7182bc11a4b23344f2c1e76358779403fe8d3294071ef811ee33a0b2876de1fbcac45d1d091226c7f6fb2496c55a0b71133b0dd852daa30c02694944b2aed8b1613bbe54ed3379efaccb994b46bf7df4a8e0f72ba4fcccd5f4112b74502575dc26b3e4edfc815b0b622f86fb6d737c9f28e91301f4e1866511e9e2e99c2220f4b883aa6ed383c8ab2351d3016e3607420587321755e887c0b62bfe03c67e8fee40ac8e76a0b1f285a8363a8d857ac9f6019e471bbae319df72435db602cfc60e0b2e01d891625c42781dbe821c04a89bec8576ee3a9c311b70f6edfa8a7b0956e552550134521807f87ff53bc386345b31787d2843f6d03e23582cf8f47f4d01bf8eb51423d5fb2db29b52b475c7e25e9e5f0a8445cfd1749f3a3449591ee84761e2489afa70439f143a953cc9bf33b2f36f1b8d307d27138f2df4bc192e83be47ec77e03e00c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "74f8c2f68515a731fae35a9daabd217f26fdc2f027dfde2d270c48e9d8686a61", + "proof": "26d80707d050763b2e7bcdc0e19070807f820a9284b5f629c99eca1ff796dd48443fa22b577a327b66aa2037b6557d18971055735984b93c60d8a22f2ae4374cae8b5c2ed3ccedaadb4afb2efdd1f5acfb1e24a0262fdfe3f37b7c35c4e1fb27f0c0d04e98b64338a4e37dc0857381baa925c61f7c989d281aad4ed4f498e929651e0333a1d1d9ff0ee97f0744a6d2e2e2a1a5b0a94b49ea69d8d2b769ed1504d84232317f82b2f4a6d359e281cd963bd1a4e51ad15b4195f307e4e371403a0c416a6289e7fc39caac5886c0c8633cc2034ac8e502574803976d8ec82697f10f58aa89292c8ed3bf1b903e8b07733c67fe54ecaa605f349990b3343cfb1d2c0e727511a7aba72e82614583699821162df418346df5f7f737f9dee0d722cfe715f411d91b8295295ec588d0481662e61e9700fc86ed68d9a9d1cd625f66560b107a994a1401fca09eab6cd28f29d4661ecd17c8b26574faddd3e1042f94ad7724ca0c547ca96a26ad6029b548c4d7b098690656b61a39af0371b71a1cfd03d72fba12017789ad07e8086af0de47ae36a118f99540b574dbd2dfb2249fbf157145561a87f6c5e40976363124903ef72b68b4872578a87b5022d5e64a38fed3c477d604d09595da1f9b94f771797b48c709854fc7711dde9cee6623a05fe4745e36b6c4bd168ccc0057f356456689808bd4a064e56a303948061fa92829910d84752462db44836d5ba863dc7412d86b37bc4a8355ba340fe48fff6e5af52970ce0164452e236b6415f763053ef5c7c94e61f8088e860459e9a33218c7c9dc043b54f098b741e8b12d454cee2f70bc7e09d5b2f7aa415bfde80cc64c85d6e6048f47e4655b898e14d44e702e614827131f7aea6ac358874151448cf0d1abdbfb380ca9b4500d20564e70b2ddafd0f0e77362b9cc7379987d1de302c5ed1366ab2303" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "82e4dde26d40cb5ac9a90406d6f5bbbd7c1e78cfa5d40ac28d73a7e116ae914e", + "proof": "94118fc83d361d243bf9fb8095a59ee55294965eaa7bf925c1be7ec8d13f6771aa501faf08de0fd815f403e8bddd0262a8cbf69a7ba0a8c0c4d234a2c01b8b5a405f4b0c1705c49cd70b80670fb700c8680e886c3238da27ee952855ee655218182fb871b4ac81cb48a12cce96cc29fb9ac5aee6f27e49f018bf814c4967a547bd10ad41bade5ab70c319aa58162eab3c48741ca31d4d676454bd68413253e0164874fb46855627ff3a2dbfddd3fdeb9f91580ea0b58e3dd8c4772d73cf8760a4366ea2afcc139cc82b436ea7b29ce2ad36fe6c01dae4b4b9232eeb4cb9f030db4cb7b4dff06605083f81323053acc3dd7222732c18786e4d7e442102623693b805c85f15e9b95151f3b04e39f8153e9c7c64b31494d5651d8f1a2773bd34f55ecdc281e526f882dc862bf46ad005b70c7485af24ce88287d5ca9ac903059125805308cf75a7dc4b9437dabeac9c8c26c2ff85ebd13ffc43276048654c704f2e6466c0b6f2103d5fbd816988831692fc89880839eb30d8cce17b1865ba14ff3a6477a9d5f9f0836a36c9bdc9ba6be0b6b029ee3d6a17a2bf8e1d49d8bb74e02ba65651e49720bf4fe3dc4d29e84844a8c7ee613a50f3d5b0e8ee8ccbc4e4556cf691ffdf9138e61bf4954cd99439b561c5fe639d0c5bbc49ecb24a804093d0130453552012f014ec4eb3cc3be2aa3e61b1191d80eb3cc9cc9fa70638e1f6027c4ea79076bdffca908703cc27c6bfd6094cc8f980253a705b6d6511db9e9d9566bc90a46dfd1769a473f0431791f8aadf26b5cc7373e734ead2e0559befcc8c6794e66f6f81be3d61562b71d232f013097723736ddcac3a8106937577c360fa09d41c96497a341f8c5bc55d6e76ad2b37baa56a9ac68a84a5cff13a0dfe52fd07928fee96483d0a3af87499c4fa8cd8e1ba55a42a939b41e19e184eb6a63b4204" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a6a5de66dd16b8d1371a8b729872defc14174da05fe2633b9b0856f4c4f91d78", + "proof": "c8b696fc2a75bdd08c0240968239da915fef7d623f559eedaf9b7498a727577bd4f6354d42899c7db940151b5bb196b7aad3ab74475972c195493537985c06456ad0371cbd6b773b0c325044972575473376304ed97802accd675fb56098876d363dc58f5d9afb1e3cdd22586d91c7b3656cfd05874a632996d15142692a86242f1bd138643e72a4708399208a17f76198359f73733af049917728593e33af0f35c248d68a1b6e2823a05faca3fc5fc4bed45bdf66f1dd1127e06365501ce207475015e3892f0a6038804eabc923b862a7b9d49fc5b5a8bd016e97d8f28fe50c80fccac44ee03b70527bada1e8d520952ac44f8f9c67cb288bd50e6f67016f1546664b89f02120deb54ec0710c3aa6e77605fd560e350cd3916e1144aae1f36e16bf951ae2780a1dd8c9ca9d82d16ddf898bca0f0fe8b8035321cfc1ca4339319297c553a20050e94d717ce219835bbbcb154031b7068d97a5002438c7d4bc197eea1bf6dbf5810f4bd2ae252e83b71e23624e425ff9aa432b2e436e1b6e527868d2e2cc0b239e8785b62f20eedc0c1176c5429fb9a8f72e05bca6b13f2fde49c4d523e9e5f66b503f17fd72b72f70fb08d72bccab73829d51e3755953bffe2c428d441baa111d855e2812c42a72589bbe22db56b7ae28eafe49907e2945c163dac4db6130d56a2f6cb8771f2e2649af33424e7e36dc8d76a576cc6be82d79778ce046a097e52df446d80af41ac9a0e2cdb0b49089756e58c18efc6fc805be76baf346cea89400cedad66ab88b4b3f79de388d1998065035b5943f1a9f512d284277ca093fad907143bbb379a521a04fb5e970f7bf024f0e46e30bb2391aed0375ef8cce644ea3de70ffc1ad9d7e6acaab771bc9f63e82516a1bde8d42947d0aa37071353b1ec709a86378a4ef415a579fe85c1a8224bc14c16ddf4b0ce37406" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b2f9dd565ce048fcd282aee75c68bfd1328a5abcd26f71c9ddcc5b967d927a17", + "proof": "aa3a8571dbe156c3aa131e6d90011b11dabc11951b0d92e3b9a32e5e68e3920b1073549bcfebc4006b0585fdddbd0a57025ee73c74a3101c684ea0a76b7d2a641c5159e799cf1aa62b4dc46436eda3d32e8b7586595c0cf3fe930b1060e97b5668bfa86b2ab51379df613469cc2f5180d2e4e8963e51a669d68d5280aba04f783093020f82ccbdbb34e6d694a3e956c2fb723a6c8042064618f0115b9955820026c2146d4a35dbf24c0ba391100e105a30e77ad8f27a66cee6c5d5593f0889053d64505d028659647befde40658af49ecac663e0380cd608b35d95c8ff71a30112ad6eb9702344a0427f3175532373f507eada891a0d69cc1f97fd34cea24055d8a12d47007a055480ab7dd3a3a35e6c348fc4492635312d0b692b64c407e259d819e8bee8f825fe6aad3c6663d26c82a09b6238d7adc584a2e3e69f452c874820bb656d8d451499fdf4468db6482036071d4ed5b62dccfeec2907b7d9b4ca3d9a20bdf10937ef424f661f92f107573f3daeb38b9672add04e89abc5ecf21b1592eb3392d79ad6ab9bcae2d986096eae4556131acb096c9fe42c710dd0f17379b63a052a09ed3afc7292b6385c8e2691956549ea44340c1227867ec59552043dd232e7a008a732dc0a26603785471d83ac17cbe9eaf45a6dbec3595f01a30508b6ba5e9ecbb72c6b747ddcbeeaffa211eaa4de67bde8f5b2579d91644cddbb4bd81ecb8a1c1c2c4f2e6713ab19bdeea049dcda51a8eb266c9d87684f535a367ec46db451858c3853aeb07942d054c4251d3f359ab58f35057f5166742249f67746cde3a505e5896aab923086d24ed255398ec3b06c77377ec3aaee05f6a8c227c5c6373bc00b5681f30a78d953252e43a1528312eff0ae962d2e36ca7ffa1700f2d17d33cf138495e05e1b4104a56dcdcc43049d8b5644728ca0a164bb9fb709" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cae65fbc3036bee2dc041556167f80fd9e95c7464449fd5c582d4020986ce92d", + "proof": "70f9c5bec5eddaaa278ffd22ff200cd74c09ea458c88fb891ebcbd0c0567e9617ed3a9aa9e13db4b8737aa273ba3a87f0b83a3526031cd3d8bf6fba2a0208e58003140fc6da7069a54bb52fd41d29a7d2dda975b6c6e0b4d3a219cc0fdc12010704e0473c338a5a0f158d59afb1d60f4f38809aa2a4e9f4bf83a28316b474a6afd8d4b88965e60f82dbfd6bf9e41c4e4f1b195e1728008d4548d75b9a09cf60a538e088b9ea3c4715295df37a3d1668f798f90037f99da7d47f094acde297c097d4b90290e3896ebde1630a52e03544287da0d86ea9c887f89ceee7064cfd40ce43e36bfaa1ad8044f26d19f6049d481b0d076ada2f8fdf0c671046ed1920829fa05d6a4737c2cdf00f30b884bcebb3bc351a65f2bf2115e1d9cb2639d25f3373e71dec1b02caf74e5a5618ece0e973b7bcabaf357fe6a1ac4cb12a31b51bc5d047e8b87df8358afc2cc4320038a0a9b0d57102ad961cd6666a58e669cabb76fdcb5e90309ee37bae234e21e4a0307e48f74b9af79dd83bb9d8828a0818f5a7ff2240251afa4c2895ffa0419ff4b4935e63f49a844ab5964665b148d21548330966f831ba8b5c6a6374c78afe40683d04a853be96706d73c6f04b89b3c14f432006a1d548b80ef7d0925b8e3531b305f8f3161198cf40095c0f66c1ff622805198a1e4cd554cb9cdbadd9b709bd33aed722265f3993a1b29452c1ed5984ff617725f671cf1db7ed369bc4372188b5779c0417173369a56175d347a08e7b0f161f06eaba04989fcc8f51f3dff6f0d47048030d758e017b0c98f7d9547b1c439782c5adcbfd234f7da63b8a54ca1833b480d09a5705d77afcd49063b28a4d17b1e11866f44fad390fa84085a6f1eabf5b02fbd4c72b8d36cf2e35e89065d4da205cfe9cfb72f7c18346d2e271b72692506d410c789cfa8fa354c9cddf1e4030f03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "dee23439653f81e94cfe4bcedd82577693b44ba38224cb4343fc40b5a7b82103", + "proof": "04df0f26b5927bcb1d37da37d396e0454b765a5b078f0ed1313146f6af80820464c313dfe938896614fb283ebf58930aed353e7e433909abd9691acbf2c0de483a7cabe7ad22f8788cf5d0b4ee272eae03813f24c795a7c95367f8eb533a1c336efbb5e2d28e7872bff68536dd0fd9dcdc361ebae012e14e08a496fce2a96b07e6146e2e47e930cd3a5cfcf90608b7ba7590e560250146cee5ba546ed313e2095eda8e96b4fcad01275441dbe4d0493f2ce7cbb015f5250ded870bb175ee3d0a08fedd9b27bd2d2ae1d6c9608d93f753494e26bc01cee3df3c511216143625012ae5f8694493d78bc5634de0c8e28c902f3be54b57a14f7d085c8883035653771ea2b8572a00e9629e8b4a39accc6fd27843c3a50d06ce561fd3c1d47315387232ae7ff1e0bc471049a848b9b5aa8d920538e507194c28694e82f2519af6571454669132b7dc4bac5e4d0c54471f26c1d66e9d567f9410ddd7e835da4b3fc916c0aa9b4ba46c457da23159811b9671cd95fc6c2ce7b45e98efa9b04c00c809636c8cefc1df35476f022a253f54e12f061720ad8b66c030286a31f61a1fd09e4cceb119cc96d262f61713b1ebf62eb0a7fa8eadc53a49706806c6a8b8ffb4b569de431924bd8ed308b570927bbc44db840053e6d73b3b38309f1c39d88ee764030e1b388ab465d920655ae94f3912e593b42f4420118d5d0af1ae1b0c1dab1c74662e66d5a220fa59b4dc93668883135bf065a4c23e80dad30565c77bfc83343bf8a60188e9f3c108a63b7597993247bdbd07bc00e985e7ae00e4991e9c044931b8c08830661970b4867e5bcb8f38dbe5790a3f5c689d1e2983dc162b5b04c94b9dad198bfa36ce430611f50de56e818a23ac1cd8b57c0982ab6fa8486b464e067ab4314e95d74b3fed0184f7baa76cf2e8e259d08ffaf4288f4a1d7cb791000a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f4b5af29a6ebd5d394b174f5283baa0fd288ef2670c53afaf0904bb9ce358708", + "proof": "2a30307a9934bd2db36465f66050f0c3542fbb978b23a438691ddbb581ca80015491cb6829b40719ce0a9f5dc7b53189fc4fd0bd4c397b82531a5d8d57bf9f421298bd811d3c0d49602158152ce65ae0ca9490e48822a17cb013ad80b4e07f6904dfe7e4eb1e3ee82ebeab916e8e1b26dbe9aa8d538735e7917e5b4bfb19153c04e39804548dc91f648db8975ce8a01f14edf1a4804115e530a0264bba15a00096f21cd24acbe9aac31f02108e6810e08082a4074fce9d36cea78b18f4f6ea02ec0de8d8102026274a35bc4a81a580db3dfd4fbc847bd89006b06cdb04aa25084259c72aff48de41fb229568fca5ae9d5006678bdd2864f6cc9c878ace1eed33de840403dea376848464c6e14fb9251e6761454e9586b557fc0bb4e527987e44f0cd04d3aa5074eb0969f44aa5d1ce89f3bf882bbc288befbb827bb9f57efd2888769c13b342156776bd791c4f6cdd3ef11477e2536109ee914a31de64d8762596675f4d609f617452a03d77533abdec688561d037f5d1012628b8fe0bd9ee459806d35cacc5e113ca5518611c0b49e60189e3e6fb54607ee6cc20cfc19ed20370b96a5368e79a70dfb2b31ec2b61d019ca2296dee29c6db91c76017363f32355a2fd725083d44e602c0bd65812287759520b2164ecd8b13ad52a5b6287556054aa8011702cc9d0402f3b9bad4f53bb03d4ae1a0b99e412a5884692eb7c76f15e4697fe312018be72af1d2aebc721aca6da6a883fddbc49334929f9cd0ecd12bf6c671aac5e8fca4b9ec2d3bad654e8d88823be1547cadb4eebc307364eb1e4c00999549314cbe9915bc8520663d7dabdfa28926d681578c4836b2583cd74c11f6d8296469a2675826878734e0b3ef4b4b9aa991deaede6993c8f92ee41bfa06118df428ecfb771183aab65ed502766140d4860a1d579e1f6d000a106a25b308" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fe40dce6e473aa68ed4adb76674917ae43bd9d1681c965ef57ea02d27fa43821", + "proof": "22f1e475bc63062451abf2877db297eb1deacea658fa0a40467c2459a42b2073661d5098c555cfd7ec0c23f42c2850259b34edfb0042a2546b6e87cc5589202d262063ae5ac70bb4fe214750ac6c80e3775a248ff293cf0c71c35215e9c60a042ac0c1f6fc7403190284638b9ddf959ca5c6b85e6a3c9705eb13656fb34e693da76b7318a794f304312eaa6a75d01281042f8467298765064b3b28ac42c8e4042697abeeab3dcd856b7ded0197b312a5f5b3cc16c66e36ae7ad25c14e2ace800e81d33576ded1227a080c5674888dc34f81e49732eafc068b9dc7de08d800c0612230d224bdf44c14da68c8f55afc17b33fb8556b5435c27a0d846cfd6fa5f4282dd29f5f48e23a72c1e942081859cc4b25d35cc59e50cf87ecec19568aac0299cd832759ee3a33a126ef25df3b429531d4a38cd624b040d8d6f649c4a619c4032283de7f31b889611ca7c367a468953089fb854c527613a588525f042d25809d84bbe322800373b4b21ef35efd739780b0025e84d2b4987299cbde99f1edf299610caca40ce1f354479bee9988f376da8c59bc217675d7eb4c16fc896e2c00ec6f2a3e1a90e26e9c784ae1244780ce7311ec8f7c0990776d7905eba979657210a24eab0236de21b957398299501190543e531a108047868383df9752c58d21e0880341824d9f557f9e1ea0d21ec901d1d50ff4164a2651c40bf71b6f7a2ef08daf2fa43c7cd84d061689bbf2a373d9f620a8399dbdc35026241bbe4a2fa0e152c6d03bba5ef8ce6a3441fe14a68eff03c48f1cb7032fb5d73c0cab7cc9c46108ab1bcdfdc26bc6dec2d73027180263530533c21155f9932b611f08323213b5b65382caa1b51f1145afe94ae51a1983a68e81ea51fc77ebc188d8b1f9ff39001743040eba9eee06ae9cbf7a3892f7464ee110876269dc458f676eb89ecf84804" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 29 + }, + "commitment": "665d598ab0b48051c3f006d4103e87c8b65ce2875ef09c698e54913962cccf04", + "proof": "509ea796c51e7fb6eaab3e4e08de115c274d82a031772a805256e4e273b0377dba2e5809c576158c57e3389339edea6cc5ed39c9153bde70aeff938b3cef1b386c641772d905f473c0aa2ead4045e47cfe81bf0f256c5526d8664e789ecd3107dad34f21975b81951913924073603380c547c568d1cc8a1a8528ffc43940364fcf34c1206142e850b18cbcef2e46435429a83cc2a34482e7190be34e140bac08be4e4207603f30e1b127dbe71c0708333c61ad3062960fd297b37506a1daae0b609bfc20a25816a1edfeda95e71f1b8a1ca4a8eb31d12618a6007eefb28efe0302e7c803bb50a8e22aa55f1ddc2ca83deb79862a03af928245f380103b466a5ad2b38e9f20d51b0932684afebf74e419b16da538354fcd0e33ba084791a9ac79ca12e35b1e0886180d28acce8fd1c1342b9269ec39ee8db95e9c07c0a05dbd4a16040f3285d115317ffe2a9b24bfb16b0c9136f44269d705a3f6a5f4c5b579505c9e857e665fa62251724fdc92313385413fa6d797be7d418d73e8536bfdfb32500d5abccde6f6fc227c0b6423c2f122833b1704f2fc214f6c340b8170b7904b36c089d9ce3cd4788c7acb627567b82af275313023692c8df52633c4d8577b594ad7aab48ba45dcd9a7cccc4e3735ec6907b5456b881d9b5d080f23fdd4f2c7bcc55d18b3bb67dd5adcb228887faa5fdfeab6cf58552072de6908e2a3663444e3c56a73097f2fab95d43caaa973d20eaf907c1a9c928fc06a81a4c8e2b2aab097cb24a5766bf21aee0aed519b6bc35ffd99bb9853da7ae2ff579b1943134ec6fda65b71f5de987b9c185d6f0de63c1402db37eec73811c1dc1d0481762e3571ef6f3642c7e4259ca933aa354e866cf8971f14698b63fc03b4ff07222ec001d0ac465e1eb3e8c5995a235e74f1b43e4b866588af24b06ff9f86eb893c145df30e" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "0c1a4699a50f7fa0709233855734565590d8ae17294268f567d4cdb34bf9a349", + "excess_sig": { + "public_nonce": "be1ba0121bec92a3a36b8303aaa0956b41057d538fc1b7c30494d9b8ba067c54", + "signature": "7a6a66d49f4d305a027c734c4a8e898cea2be3e3cde45f4b3c0528a82d6cf101" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "4c9c98398bed774253b6225e0b24c667a713e0f79b7d1bcca06660594a809015", + "excess_sig": { + "public_nonce": "62bca37f129f9d571934b05f35088c1da4869831036d37d0641572e44b34a628", + "signature": "42eb8324a791d62c85276ea4d02d1c44b6ef6cfe08b3bdf01fd9926145e14307" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "6ca350d07aa0ac852a94ba7abe1ad92a4cd083713aa173ea29406412e3cdc956", + "excess_sig": { + "public_nonce": "8691e46aea7ce3b79d4c997449c59cb4c244c4ea2168a2235fb680b9eea9ba37", + "signature": "e515dd2ea1c04a995e4219e2d401304edc404a4015f2bbebff1413893354d209" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "8e9ef454d70fe46fc4f533f894726fb46b6a4c85d0cc12cebbf425b1066daf64", + "excess_sig": { + "public_nonce": "36772b4c4fad2c76e5146beff2207a2a0cc3aaeb29f156c6ba216eb093b17e40", + "signature": "1836691078de037f863dc07f921a1a5c35f19b9b088c67bcd1f66854bb28760c" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ccae99124923198b75b57027fd344dfe327b977a4db1ff8e83b0c924559e4013", + "excess_sig": { + "public_nonce": "96740dbf6c4504fec05fc74eed11e986907c0a0fddaa62e417e7ec8a2299bf7d", + "signature": "701abed35c405f6cd12d9b40c4ac111b834a560a855ad2d2903669e777470209" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "7e64f752326d20abdd6fbf483e0f735f282316e16dd7f4a38ac5b2345b620349", + "excess_sig": { + "public_nonce": "e29121aba9289b5a61858368de8655be47478645c3669fb1fff7809daf61e339", + "signature": "2f5109942723b07f8efc7ed9ba228956f7e8d1856d38feba0a9cdf0d94636708" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 29, + "prev_hash": "157a823282648bcb340444d9d488ad09e1d7e206cfec76b437a224c3fb39e263", + "timestamp": "2000-01-01T01:30:01Z", + "output_mr": "91824240b92ed89c53d24c086915263727814b7bd99c0d66406561719f16da41", + "range_proof_mr": "d5a6b2efc3195054620c704156aa7d2ef487748f61b28ec546cb579ee6943e47", + "kernel_mr": "c7a6dbb0b0a88f1da9b823cdf0106166c3077532bf4348c62fb576155780c233", + "total_kernel_offset": "f58f4e6a0e2b816bd04d4a39eaf328fccde522c529d46d44004c3a45673c3e08", + "pow": { + "work": 29 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "10c0f7e11cac4186ff35211e604a132c75bccb33910bb4d3e75246ee3f80a520" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "74f8c2f68515a731fae35a9daabd217f26fdc2f027dfde2d270c48e9d8686a61" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f4b5af29a6ebd5d394b174f5283baa0fd288ef2670c53afaf0904bb9ce358708" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fe40dce6e473aa68ed4adb76674917ae43bd9d1681c965ef57ea02d27fa43821" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 15 + }, + "commitment": "ee48d8caf578688003c654fdd2cb466ff90322923333a5d73c39411c7da1221a" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1666edafbef140d890ee6adc74925be5ee69e0b96c7d828701305e2dfa1e7545", + "proof": "aa8050ffa18284164324e193f34e3a14d38a590df3066d722e562ffe5534ea571e12456b86b981791a887af757b2f27da5cbaed424a6946d0581ff1147e9c65a346d47837b5d722126060bd361de1e135a73ba8fb24f01235cc1525dbcc32704764507e1e265ef393f6121201e0894e65905fd71ab7c47b7a8d8e4ebb8e38a0783ee669ecaafa2f9aa5de1cb0f82c7908be86ac68761a92d4c9f25bada626e0ae1123d5fe4a9f749c11709ee36ed3d3f508be90cef31557ff3cc536808b2490438ef9a15399882512b75722f6c3d4c84b82877f620e44bba02102beb09f89b0cba4d24352d74a23ddb8b756f5497f377dd06ba9e7b21d808e3c6be7d701bd77902a122816ae262b60e0017fb412314899319cfbb160fac78f211864ba76cd9352e45204cf0f122f12f0d842c1d1d45a1bd82fdf675b41887e9fed257f832e87cc01b135c065ed4caf95ab9ffadb4bc75665641c75079fe4504b022f616a1354d08b61b242ea91152f58e04f1d34a7a002f28c78e26939bd9662f2e35e645347ca2a372ec12effff22a92cf2a9af06b063ea266935530732465fe4adc71c7f70ad8676eaf34945a2d6554a10439ad51db040aaf9d70205e9ed77138b44f230e4550f16fdbfea361d7934335591747cf6d3f529a7ac12a4137fa828629c041a63c7a08a6aaced75a831ed1fbb6a470ba1c7ae1d9c568633d802ddb8706859fdd624cd837491a65c7143e42ed9685947f1129e48b2eb8d071aa26807b75131fd95c0e78bbf21471167d9c093117d5b47b78df5b9d1522a87d122cdc83db58cc4b0fb47f848c5d6a7f56ecd36cb4e8d34c14445518efac5cbc51ab42b35c45580649381370148ec59dbe8698b1ae14def9ecf46a966a15d87e0a0f0c5c33a97f360a48e6e89e8b26466ab826f3fb1e4493ae79a6232cb337cff030512aa3cdab5a00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "289663eca6e58000c1d7a4f7e761225d9da21bf7e7ddc2a621c58b680987c06a", + "proof": "a6b91b74fea2c0ed6dc54de5c12a8df2994aed29dc0958b09756d8395185b50e4e942fd059c4415855541f7c0c0e17ea9b765d882864a75acba56b66ea81da1df0b06ffd4fc775862218708b7b093bb3e4322405e86d4c5ae029fe1fc1d08078e26904cbfeac01e7ca72910374cee82f0725a4dfab8d196166a1945904f9be48abeaef7d28a08752e5c7336b678bd2eac560c7a7696aaf5b06b9c8ea6ce50004f132b061c5840d603266edbac121976c3b6e0e7d0415b34f6c3574b4c61b2901eabe0f1f10ffe16fc91d582515b4b5e140fff3a47a9196cd74d2638d0117510422cad2e22490ccba451c2632f769582253d51dad7e4e1e1d3dc03cea87f5937404dc7f04ce478f6038ec41af298623bbcd03cadaca5ccc06947cead4f9cc0a79b4bc35a818721127ef4bf19c44ab16e5e06e8d9b15f3861efd0512727f5088440c32551e8193ccf640a32e9e77dfade53f475943e654ce8155299f5505635411bcc3c9c55907d70e946c905dbe33117a61d9060d9b390a777e9961af0ffdd677565d13a949ad4091d2aaf679b6291a6f25e249ef5141d82c38c7ab23c335b2262064aba0bc0e2cc52fcdb6229fe56f9987a373ef1bda5794fe45a795b367f5416ec3f80cd90000984320ff474c489d93b1e1ece3d46f3751ee41808968826a137cd448c5a3e3e1f22800bad68e75127731e4a069e10200785cd6220da75e7e169adc2a20c7a71a482e82578ad9c1429d01889cde1b523cc87a7e0db9633cb8304a7da321b3fd677b27967177c3fe2566707fb4e22410a31f16901cc1d15f0c18e20c53ba47b4494b34f0da7e9b27c8b890854d3538500cb0e1dbb0805669d07fde35656f17150a01851ddb0ca3653c404240c60832499f95422a6136d8a17b0ced00e70decbf8fb2dc2ba1ab2769288de7bd1087410027cca027726b88b53608" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2a33ce97d3dd6a727f30e36e40a6069137b442c941607c08f5b9648e05c52456", + "proof": "448e55aaa4ce17d496963384f594a3279cdc803a9e5279dbce2ba6141718ca4d4ce68fc8b263fc78a32ccd5439afb493652d52483a754e73e2992bd446fcb92bf26818676e79c989b03d3bacdef765b82493482ca1c11e67a0b78d07066f267bb62c832bd7be91325d8035f5c9f41c6cdbf9c194692f8976b48dacf8eafe7128f112b25cfc9c1e5575d271ba68ccb499334b48d72ead245ff37ac104a7ab2b0cfa427d672c2411a65cf1f0e8ec90ddd4213ab6818856525b63c154cc8b3321017e54d1f90a95eb1a3373cfc2f4063c7ed48232f2cdd34a74e2f0648fc4ca230ef6f494330509739a27161caf492b04952340273fa94c268daa1362759c244d0db6798d363fe4d72003e03575d8852eec4b3468543336e66e77bd2ac1c4e38e617c28584850349d643b041bf82c0d236049a77f7a280dabbfa292261948e4535d3a40b8ca8d9fd169cbe3244ed256bbde0b5319316f97023f656dcbfa9dab4044263eb04d129e92a658d5ce9eb079fdd41ae2c94acbdbe69bb664f4c09fb41c1686cad12c4430c84f361aee2e13f9f9764b9a30c34451a13ec23f4252fbe7fd5872045d963f56325d4e817b5aeee11a83b49545e3d2790eaa2596ca13722ffa50d85f02b12c7aa3fbcc1980bca0a28b62779bfc7586beee951d982b91ceed1e6114a61738fa6e9d3aa67698d81b617ea040d15130c0d2c13777dde3b5bd22796366a22e3a081ca6de6d2f17b4fe90657ea82a10edd274fdc78eb45a95d2f99801e8117e3e67813faddf22bfc34eaaee16dc993aa236ce72c8c9c665fb15c38727d04c9388a34feb7aa4df9b68fe51ea92c87660165c7926821daa838d2f126238c53e4967c11a54f3a755c42c2f8331a7d02598a27fc5c708c257ec4180c1440ca841588436cd04e866e0ec008bf19f6ed773bc5626136091675438b1959c060d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2c20deab3a22e1c49c88b6eb3fa102abe4ce4c45f0463318fa54ae0d8b2edb72", + "proof": "68363cc5f6f5e1eebacbb32b2d4808364cefcd4b6229afeea1104ad1fb32e47188eca8e1fd950e10eeaa8bb9d3df2b4c0e0308e411c823ebc8927e02058a3a02baeb69fd3255cdc185150175914e036cec520b5d988f03867d68ec2b609ab34e7ee80f496f87b5d101e0c0fb6bc21e7fbee5dfa0269f41ac2653b52d10fdc7019f1a5ab825eb6c266f4023e16e273188d2eeb6d57d837d5f9f5f7173edae8f03dbbf4551f1dd8ba82196fc797f88593ee3160715ade81770a5cfc13bab76e40ce3756a60be702eb5c9c5bbf574eaade727bb36fe363c062fd4525f8d93d8670c7208011cc2c0cebd9700793ad67037bc05b7598c72c7ac323d4539ba7e99650932351f5472826d1f92d7669a7d91d2dfec873a089056a88af975fc07c0c3663c3e44dbb5b0fadc6024b251caaf0bb124e7956158b46e93388178a081df286a33c88074c025ea5c48318982e397cbbe4177a6058c81dbc736d6a7a33a56d3be28b0a4fb54b4ec335583c73a290e66db6304626af81a22d1e4399b478a2411d717de563371ff675b3b74377b660cef707e5c51fcd56fe30086547589d1930b1526c2aefbc134ca82a22f12d20f1ce4d4aecdf049bdd0ec1479c313fb96a99c9028fa4c1e09b2679bbf14b85b9220681037f3a5e70b55cb09aee719e0d3739fd5426887d9eef6f4509701c1627dbdac254e48fab072848b03ccad94b284b59557331cf6c7df1bfc6d0ad03209f415ec4ec7e3e51e8096bb153fb06b0120789a833e1c4eecc81bc797a55b39ef94b8822dadfd5d19a3fb07c94664edf62435e29f1fe8d9b5cd9d76cf5a183255f8a9adffe21a390028da2a9f7c3f1869075efa081737c98a0579ca0d018ddf867685cd4a93eaa21e63ca5ba84ba0475715ad1fe50afa7746228aa6f481269d7002aec9723ccf7487390bd0eab36e86065c9a47210b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "402e9788d94cf81b05956eff6dfb6ff63f85dbcaf0db4970840fc5b584778559", + "proof": "1e162c27780bb60cc4624d1bef8487c7d3e6b3a59676aaff03ec75f519d3a74b44263971aa58f05b8c4430aadf7134cb5d2a8590ad6ee50ea970cb734d3647270ea8c1b85b7aed66c1d545b37236674e6d478b1d6c15df1afbde79731d7e7e19fe7e5b2279212d86449017316845009bc9a529d55789a8a74d6bb3872e6481540c6194e8e660be728ae6cf649783c6f2f21cab321e135093e95ca4244c185c0001805f809fbbd6664d4f3c2b296056bc9520062c2571e146d0d50ddfe9cbcb01cb359bd9d14dfe457be3c5e6c7f13a1f8b6017316ca91485133ec21a200e5109868a40b70bd699cbd65a247fc4dfece228bcd2c92ed754bde2327c271319555fecd72ad30f394d16c8f99fbb234bbac946cc47c4ba22d5880e8fc3fd048ae27514ae96fce338c42f68087633fe998a10ea8cfbedc6958cce80cde3fd0c85cb645acc420c58fc3fe8f12e639dc4449c5d4c9cf53d7aea9dd982267fc6f7ab2168faf74de1c30418110500a61bdd56db91a7cd5942b3eda5465af6ca8683be734d8805ba38272411998899702fc9bbf3d86c38282d8aec0d522df87b2765e0ea339cb94e24c9105fad95a2af6789fe784d6569266750b1c6637b4d7987c1ca171f9241532c39e1d73aa9d305a6c00a76de63fc7e8b25535375ecd3005bcd19e24d3a60f756a87dd8c73d68f04f759d0aab5391f042abc79096332bab09f07c4011dc0071a451fab534d207087d2372deaede3ee866447d5984ca936043755738726c49fd829c1536f8b5c16ca20b8c93818fe342760295373890648e09eeaa4a701eced2cafe1378e38c22fe9bbbb103adb0c224ed4822b19010ffa38f9c4da759de34aff74604d91d6e94f07c839e3d03699f16fa88f437efca5a76938829e50ae2a1f6964026480b3f63a681beff1632c2a8222c6d39934a6ce5dcfc75ee1f06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7686096287e68bc232aab95543eac1c2c471752a181b7d8bca23484bd6a90b76", + "proof": "eeb50bb898ed17b4fb4b7b0b455419d9b1ad9b93b3a9b8f1653393418e40f257b64a3fba2389d476094febf462b6b926dc5cc42aab2238d1ef83a699cb088a5fc0062a0327f587f6eb75b245da149e0c6f89dfd1286785b0317efecf124af52d88e2c54fb3d6da04e5dd35f6aba41c20695c7d9975671de86b23da80fac9c67313dd980d75f546d446a05dbfefbc8f783bf380e541c773e2c056135b11266d0b35d1b0a7fd9d5c078368315db9459be123d4d1615f35b212924d120069160002457b9b34614d89ae57f3e6bf70e8004e543d1ae094380d4aa6503928c535f70e1a62d278816fa34e45fcab4eeedef6ccb285d097ffd673279e521fdeccfb8d4026730024f80e367177943cb50fab8f3a775e6824f9a5fe8b53b8208de257df5336b440e58e7b432c378ab66b01ca7acdf14445cd7837b24306519457c9f3200ae6aa5a85b029bf49a10e00f9ffc1dbe55f4e030da60e22c9a9476029efea6a4f2ca20057b2ba8c12541e755db97ad841054a9b9655d85ad3ca0be7f0654f415ec4d4f9f3e8523fdcddf49fc12ff3d384095cc40432903ff7eb384c8366960e7de86a6f7d32d608e124df994c7d4b4931c54e31947852e828ff6f61c432cea26a4226f57051e94bf860796f63ff7ce44c92bb3a08504f010a90a77969b3136c56481c3bb79ba5d3151b67e3bc6fcc2fd3329512248b62a8f770f4f23fb1871955d2519e9db9dc9df68b677224d1b75711f147b748a28cce286241710e1dfb4f34cc95fbf784e5425c583b390336539c72e56506efa1dc42df3a7aeda31388997a8c9f5f718f318bf1768629886feaf8568823e8945aa4fcf3b3d9fb5a5a7f376b16ec44b32b1eb4b2d9ced4df34a370f80580063d8c7db5488999d238779b1307f996176a7a759b325c5046297b1e9e168b7f6eec6b493ef33c05dc44b713260f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8abf4378c320982c4194b743d075828c884caddc89be3f41c78c518fd8fbd55b", + "proof": "763c3dc2b3676fcda0b7e9fbe5b5375e54cc282cbbe3197298e1f058ec828a498e5bc9640910b38fdfc84e6ff163e54fc9f50b6c728bd0621ada95d196ddb371cab9b88d229223323d6facba13d11071a8da47377504d659b9b1cf24f333f91062f34bf8da0fb2e64d76331d9e5d2dcf9afb4042dec492ac9e3bde72ba977f1079f9170607c718175c01da5d75437a513ff0e24c5cc9c8b05eae497d3ec09105eec20b82436805aca4d0c76481ab1047c4b4cbc1b6d88da01ca23287b32ac7063bcc053dc2baed5302e98669d868aa7b4af83ecb6549df6381e1df1ca3d1d209a4e1dfc1cdba16049dadc277dcbba64cf727bf6f2a6325d73f702d0aa902c066788d4f763c83ef764f5c0e3177304cdf10e05108da94b836fde6128199bd0d2b9af5b0b3cbe7e0615fe28beb43d51cdd97983e2c9f61ef58349832ca8006692814f0c167972313006d8d0e43af8891c52d858aacfdd06e6277b9cf5f7f906246464c872a2715f2a9d95158fd6bc5134e7f243c7f15c354122d69f26ff2492a399a40bb65d367181431bdd5e55a45a0e5d8e44fd31b92545b3516fd843c28373336f7918d0eb05d6408a59f89bfa7803d4f9353515eb96b2e334e7b9ca2e0902d60f115a8a870d6b443a28c79abdb5af0aa14595ac00380539fbe95e9fce2356adafd8c4b6c437d377a0f0cb6eb4ce5ed98c4a2d08e6c72e6bde1930252f6500982a77cfdd22e25d9cb223dbfe2eeee140e5cc8c2e7f77bc7993f1adbfe0f510b4e2d7aebd07e293e71e32b903d81a03deeb068c6270294d1aff4ca44d82ec305cea8bb5dffc32e671ef06e5239fed12b34de3f051043b8bc354c060da786652f0a7450da0554f7a994623c488c24f258c7de192969317ca7e5e482297d3e5203a30cacb2714ea425589de6e6b8113ddcd668bc8944a3f8dd782181c2c7fa3f0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8eb2425fb608f9f0b05236a1754d5d84369a59e4c2ceb8fe5ca0fb3fb3b6d54e", + "proof": "7415c0355ba75fafe98f535f8c33bb32d3dc6393333667499dd60443d43c612fae4c158ce5f360e0b0e0972876684d9954d8006822b24272ac3bbe74f8913777040a6a6b1204f98017cd9c2ace7c3ae739d817126ff043a629276086d78ec0775405025953f1e0d157b65b1fc569a42885e037e6b4b8c7005ad5d2ea29dcb3364d2b50db980f6318a6f2a12a3615a575b2d2bb7a394b8c53813b49d7b31c170b482eaa19c6eb16afd91600d37d9ccaad157ec51af9658de41abb6e195c66510fec519da80f301513840f11b25a6fe0d38b6a84fa253e3e3e4586efcf533a5d02d0a5b4f88b44d73f211d181f788e48b7b7ab381b4f1f35c1e3aecaf14eddb12042abae8a4167c0b2a67c08d6ef156f874d3ed16d8a344dfd6bfa9c41e209f6025c0c9613d88739b04b67d9e7b5fbdd4f38e536d0a56ba6d0e5be3b184265b473e2f7267777e4aad3b71c58859b8ea5309f6490e67862641cd94aee159ef4c851ccf843cc988622db40edf9942ec104c881f365aa08e511438854a804c898695058f6c9edf767e029ae71e21aaf4acdeaa3410cece73c37b53cd3c63bdc0c3944f263b87cabc587e430f8d27597e377f97bb064a44c1656315bc7c55966a25a0d2203e02432cbeedb4cec26af5e370683c561983587f530779530401ea9a04465d45a917d437e95a22d1a01eb73c27f96e2e9bda3f392ee2f07da32018342b7656894b38541f68d3366a23ced1085ede8c384faf827899df2f11f2303630cca3dea81a12f8304410d19e585b1c641e15dc44b0fce6d2a1c3dfaab9bd8dc33b42622fa906d8abd1e48e513eea4d246ed4e3944e40ac56441273acd83eb3550e123ea4e70d2760bab7cba5d8feb5cf5dc1a8cac2db15a2c2187bae0bc9c7ef0890cfd00a4e52269958469555f17323a4aa86924735a0793d9004c08b7e8a162d90d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bc5b6812c196b217106490087ca9d11add7c617708f8e15892b5410ae51fab6e", + "proof": "6eb8aad811254df2409ad3a8d15ec0f49560d5eaf23977d86a96b27ab1980f1b38b7b2d780d83e49094a416a41107be86171709d16110301167096250b16b747806848081723356c986322b5b8f064fccddf171697e4a31e547dfac5418a0109b2d1606c2effee59161d72204b49aa730769a1ca8e360a47b58aef37975ff15cf471774a9a145280cb735ec43659aa5b711499e28dd5949185f08cf6c48b090d8abb4804a52db4afd3b3beb3f0504cd233e1cb75e164aab77251454d9c25a907e9f1b946a7543cc8ea20bd0ec668e27131958a8f7d0a464e316379fe2ac9130ffeac5239996aaa1e43f1d5629394aab11cb64d0295db74464f129f222a1a711d80a04ce4619d98b2f4ff2c1b5b981d4a1048e612d9e6e056129cb2ffa356c60d4610aec3770d8cb0b3d61a6b37af7e3bada7b86fa5686c578e0df576097cdb43ca6d3cbb023fb5faddb9aee56d35b9f2b83860b98f4af1f1ad42e336c4f989573cc131fb1928debe837c565321d9fc78c96157742d69507214fcfe4c5129ce66f41c32211734de066d903184b5124a02c52a8bf8239f4f6be23cbf5108893642fe6f5335d18fa7c39c465330c07b203118c103879849882e0ca12d0e0cce8f2c064b46f7cb890e44c13ac3c319fa7e7fa60655578f1555a980aae70b90cff13bb2e923ab5ecdbdba38ae036ec4fa8100ce0ddb81b2bbce69443d2e759c184f6f56406e7fc55b964789a1017bbf42f66ac680cc47a97ae520b770997fd432c4230c741b74a57280893a4490567edcdc8f3c8bf8ead89d4f17992cac281f7c064d8435b8131f18d7032e0b8a8c8bddeffd3047b8120ceb1822a63908430ea52a66be66330936d8e8bfa31be36546bff37af7da3edc1a8c4ac76f7e0b0fb5e8660719c6aa4de12829346d7e9731bff740d612ce2958cae8151662db52bbae9e4b0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f8fbd1941af8cfaa53a085cba1c975a02edc0fc52cbd49ee9ead30f07d9cb371", + "proof": "6854e2d55b37657749412d2d13a1f6cf24ee17dae7a34e44c97447a44c977e3a16ea6037093fd1becc80f33408c1316272e8b7933ac967977073ad8453d75b4f2a8aadae17447a41935eb22f805b611ff107a041591bca17ce72e59d7cb7bf3252c05eea9f9f456db09db28449b623e11116140bae4b44243f87588f047184795a08491b18859c2cb9b083fdecd056ceac9fd550b124d24e236a2a0d93f8ae07d17713da0f757cf7b55f696e5a4d664c01179f64f5a72f7055b2525915245c0435c4ca79fcf26335dd4394e4cc6ed3eac6da163ba2f0f389fc76482026a7f40bf2696131a013ed1069ae835f4d40c73b374f8d2c628d5de5109a03df1930a92a60ffa986c8136cd3c899dfc5d4bef211269404a7ea8f175d15f30d65d5defe5edad1bb29cc22386f3255818ea8d496ed6d2e094b29a6a5fa97f8a71527462c591ccea6683aa6fffcb6f5af7b93fc16b7969386202077f6e6e99f454aa9e9c22cfccb33b3190cfa096b05bc4494a433d409198bd015b63832f2c95a1b0381bb1372ba295ce2333eddabfd55b28bd3b82df4b408d2037c4040d76eb48cd1f19a31a2caa3ff18989edcff9a01c4872037165ad8582e8f044a2a8c6cd75fbe1fb46454b8fb0d38a0f080441c2fd41ac23fe85c0d725dc646afaeff15f7c0b506635a789cc5eca1e1a74b49fdc8c31b1aa31123774a213ee84829d498022a6eac9c52e235eb3a47e17568186a8c021387e61dc8fe34a67452d2165519d46f6e7c32267ad38b5f993eba21dddd401a9c66ffac7ab67aa21423e9804285d8fdc0746330d09c132768b12e6aace51f0e83a87266873f323655bfffce6b01ddbbe95fe100ed505bdbb41cc08be49083d5f34da5667eb53cf0c4a102ba0ea70c337edf9b00299ae4a49570373a620322f9b6d15b8d64e9890d4c60eb3eae1aa3bfb57ab905" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 30 + }, + "commitment": "082dff11fb3beba2462e363044ac403cafd7e467e201a68ea00032027ded4902", + "proof": "12576d14a01fe3049a0ffb18575c6d33eba4af620ce7c2f86f76e051f22891693c922673b1825223cf90ec18939cc6cba94168b0e8b6d30b09bff4846c83653c22574779e2755697ff6e8add51e153987f02e379471bd5c48da3fe97ca60ec38eada8c72dccf2a21ae6825c860f171bdfd0207548bab2d3135ff1cd36a1294553ea834e403d241902f14ae5032212119fa1ac9d2823df74169b53a566f4c2e034c0f7793ecbf36e12663bf5d2a0a2f4d65806db9748e5b7abbbd29b44af0540bd1efaaac18ee90c7d257f1ae1d599877583559c61a8452cec5759f05a8884c061ef0e5230ee28e7df7a93f811c62cf9e52332f0e774eed678f7f7d168e13f75f30e267ff67e47d0aac7b1d0d284ce022bce57313c152be58f8c44b7080160f78b83a43a08d02f4b56df92ff9d8dfe02ae9e73f45e33514a5b27b1a731ab1d910a21768ef46a2d9b574d844bcad0a42c4af34b126a9125cb1455d709e899e0c23f6dbc086e3087f4f99d4dc608c808ba69cddff12f35780106fab71e959c099262c3232e994543d429131460fa0be05efc8c3e51a6b605ad6a44d57ba376a546e0291e141dd6a00a111571f4268781aeaf33efbfe937a2415bd756a62169e0642981dfa225bc9b4c5f64fecba2a8842ab9dd5ab0e25f89416530e1ddcb7bcb00d6cdd6c9ad4f58145e24f24dadbf72c9aa49db2a3800397cb4ce758321c245d14842d56331439eabad2dd53a41966f91d33986bbd63ad648eb51886e99fe9a26e32836f51ccc74480a443b2eb567481d5729dbdb4edd54a80f22a507f71998d4938b9423faa9a5c5bbf37981dff618b459a1e78a040662ccdc89ffcd85a91063211de4233a2546bf588e68a9ad776d02ce8b24e9a5d94c57bbb32182c70ee25079e8f86c8951550d2355572cfb061a7b724097f3affa9bd18cb1a0afc169a6404" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "00d55ce3390ceea88a122d92ec945c454b3d1f0525355bd6619ec32fc05f3117", + "excess_sig": { + "public_nonce": "42c00b05810bdbc1a20992dfb2cf287780c95309d96285fce2728c3a9414887d", + "signature": "40c11890a840ce5ff3f4e75042030ba4e3ae2823d7f729d8cdae654d6d7a0a08" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "048d7e6460605c6f18f261c56e9cf721616ec8c9412ef1562cefc8e882324672", + "excess_sig": { + "public_nonce": "802f0503032eace815be4f02fc623418eb3951425791b64bc49a745e2d4c1219", + "signature": "2bb4a645dcb4825e438ac0620423803a806d4f51b441fd92747a94bc8582da05" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "5268db1140b743cf8866bc21407b47b74cfcb99bd2967264c1062b7b35b9ee53", + "excess_sig": { + "public_nonce": "ac213a45de69d2d3cb37524e9a42284fc0417e1eaa19b48b972dd440861d664d", + "signature": "20584a8d7e886afe496dec8458d89c7d32e236fa3124d214827a06b92b7ef40f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "6eb4ad3facea88ab7162f81d686bfb8816a11088e4c55622a65d4ec448b0d52f", + "excess_sig": { + "public_nonce": "089a3bdf1d665b5392036209236a9b71545fc8bbeba33b929062cfafca49871c", + "signature": "912970689431517e5378636fcb158fae27b34eae2701456b465fb9d9160c3e0f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "c04123e2e88ae91bf32492890fb888ad536a8e1305f41e29dfab08e6dddd805b", + "excess_sig": { + "public_nonce": "3c877b33124600ae1989f3f29be9d15c45f10e1e56e39643187743dc15dbbf61", + "signature": "225bbe658945bdf155c6035e4c782937a168e923650b5a13e12e2e2d7376d60b" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "c01f3363bf5b11bd372b50d6845c5e654f27a1b1d42bd6c71f443d00adf29e63", + "excess_sig": { + "public_nonce": "08026ef71299187f956cd352cc0c0b80bcc125183209e3eb2b8f9f9dcf39af62", + "signature": "02cc458977e7a7cd2934fe2c3a42dc71989ab63f91bc0d2ea01074d18becbc02" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 30, + "prev_hash": "fe502ae957b5bafa8e7518cacb5b4075bb866a83de3e9951fdacef36a33d4698", + "timestamp": "2000-01-01T01:31:01Z", + "output_mr": "12737e06cb25b2c325c04ba50a58f61c9fd9ec9ff5c9a84546242a11b30be498", + "range_proof_mr": "af395012f83970853c73cbbc1f0bdee578f19bc4a1138b100f69e43d90dc3e81", + "kernel_mr": "12b0c4f5a68fe2cd871fbc42a7ef03a8aeee9bcdac43690e450b06bc32d95494", + "total_kernel_offset": "97eec939608f3ae44023375b2bed19bfa656105fa484213f19d04326d756bd04", + "pow": { + "work": 30 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2a33ce97d3dd6a727f30e36e40a6069137b442c941607c08f5b9648e05c52456" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2c20deab3a22e1c49c88b6eb3fa102abe4ce4c45f0463318fa54ae0d8b2edb72" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "402e9788d94cf81b05956eff6dfb6ff63f85dbcaf0db4970840fc5b584778559" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7686096287e68bc232aab95543eac1c2c471752a181b7d8bca23484bd6a90b76" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bc5b6812c196b217106490087ca9d11add7c617708f8e15892b5410ae51fab6e" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "102aa79268c694aed029565a776f594586bda7483a5437a07214815912654136", + "proof": "5e4641c9c173a0269c7beb5457e7282a60eb760cfbf66f86dd1f116ee46e1c3a006cb9f71dcdf52ab52e004e4d72eccf03729e2091f653ad496e1ff2fc93b6262a508b4d74cecd2be9e5554d5a60a2e20c37b3752ec0edbaf85568c570722f73003536567c690b33256dc7c4cf0909c7790248944765de82831171bf69b8bd7d738ef669937f17f41c7a78742ea76aa70110304294237994ce2b845145dae50d42ec1cd0e0addfc4f5c04e08108494f93c81c37a3d623f3768b974a9e8501301ea15333023bedabb840e868906e19039af7dd34d9ade8c1b7c32ee9c791aaa024854eb974f10f266523e9b9a0ac039ed248b7a8a499500e53080d9d4d51820141ac2d0b76d3befd6115e72c3b5562017f6a2aac88003e27144d0eda40022fc1050be9e7c4aac027715146db7f602509d07ddce32052bba0389894a2ecc3a230bbe010d5d8a0773a0fc31ac1df82f001567293ab3c02cac4f08b0d6d64383e36c80958b20d01c208109aca99c013381b8048d77f38e6dd4cde764996bcdc983233c51e89c7348a8d8a94016e97b740f2675a3f2f9e0f37aeee37c24ec54112159ae90d5c71c84c17df18d4d5c57e0acc819f3dd605ba471013205bd0e03e3877fb240b2310f7bf5ed2a60977840eb8143f6d45dd92ca82dd0957e2d676c27f263525608e9b0447493e31f7c4b423a8ad8414149afc9b54102d15a8cf4a2da36064a515d25381fc470088eb78c53f39cf56fe95e5cc55a27c040d7084879d4a81702cdf113bf19f123cd829bf2c8c9e03f24d76e16076d287eff7ec7509d7621030262ef6dba3eae2ec4cb73c814793eb2d23e6b9235097d2fd91ad05770366a2ab053399600aa344f7fd22b45afd2d712e5a9668e789b9517726b94aa5998df0f629ba16583c4eed742d6d472f886c1d9aa174e7a2830473308836d5e8f07390f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "10f14b3034bee8b0ba2c1d38d317823dc4cad470773f5478886d681c6ec05340", + "proof": "98f1221948aafb6e297d76cc390080284433f2ea946e5bf635be9f79a924a364c821029c08ab791645d7bc280295118a40e6d54a2413b721d9bd744df138b8619a881c99d46511a774249be508cc42c3efe5f416dd3efdb3fc8d9e0779da13155663ae61a8b524e0bc16c48b2805c116fef9666490cd4d3df547415710b0ea2faf8dfd4426a7c93c80eaf023b03a02649f2de74d9b2bb84ce015399b4149cf0744dd485f33572d939d3edf8fcac3360d592e75f60e8a82531984c8619367150f319b1cfea10450e9a2d6063da689571ff20f9b26ef05baf76301548afd927f0df613386bc8eedd0227e799216186a2489de036595039d28725c1ea0b93f0d746c26dd364567667d6f70ecc80e23f2fb8b5f5557c83a8a5d20bd46c01ed49563046b93223358951c544031813f073feea85516a492f8d2c217c8559f5aa9a15290481d8621961b76adb2db3edd4255a13679c0aaa465792ef17fce7e1ab940d034c2898bcf3a5cdaf3b22c1190a6fc48527ddee6457091bf0c094cafd597a4b35c649079640a261bb8eeced0a3c6c5ac5fa3386761a5089fb7c7e5097d5ff0806c64a3bb463f2f3d999e30ee84b42486aa5fc301a482a8f5fc273ea2fe39cc0141ae9e77966fb088e7bc804f4ff8130967dc10a93f23ba6ad45fd62968080c0145cae6e68dbececf3a88bf84e79849ecd0cb2d4f551cce04310534a0e5d10ef404aa3b24764e240655d33637ca0ffa3abfb2b98dae265850b6d0ac56f974e1a6e5a6566bd9fa7b0ab4ca0b2ab82dba4eb6baea11bab62c5d2e74d3abf9f778e303e2ccffeef6ee61aa0024634032c870b543d685b5ce1b1ebfc7f4321a7116e4571d4c60acb1fc480e3ec98dabcd456d5af84a2639016b8238e0f604b375d7807fada219b4f57e745ccf0dbabfff61eff9db3eb78d8d011842f2f4e923c12470d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "367a70f6186eb5b126b70bd6702a16e182fe4a07fa92e5a6edd4ae4b33ebf023", + "proof": "b00cbaf544f8f6bcdb70641e2cac3cc9ad9e5f8e8d6b29affcf8909bd244bf28842da190d3fd1d62e0b83b1436b36ce09ea8757c808781dfac49de183d33ed0a22d5b31c39bcccae19da25ba1c6ce8271cea9f7acc112a84e2818daed0ed42390a2146937e96fb6ac6fda6b6616d72eb7cbe21960125b3c8aaedefa1dad0e3328e0df2a4d18fc5ea6d909d9c54004b079cc8dc290d9a0acb6b3959cf77fcae0b0bb65a0c704ce1794e5b00e1dc3031f3bd2e2bf39b78b64d011d3a4f6b435d0c4b8a1d8a80571151620a4603cc95631c305d936bc3f1c5bd1e7f2927795000092e43f8e90b2d7c8e5d2f7b28e48905ee45dedb92b41003a82d247a246d04c56068010c301990c4fb7ae145b6537b39996dbc126321f339f587b294c389f2231308ba72c69de0655589d756403212bef50505496994efd0137815ea11996a5125c89560918d6d617f3d627e2f4cd09a60c70574c9a5fa28fe0a0e6bb6de154a1378869b935b6da34712fc1f397833c1aaea763a5bb0c7f196de215b479fc33864ea34391e83108dd82242ba3c52bcb44e581816e039164a6a5fae91e685991933c4a98beaa3066698ff4c3e8da1b73eff333d0119aab6924515160f946572881e862b1583a5c697bbcaa5c8ee2bdfe43ae628ce6b0b21b21994b927d4a1b9e8545cd38c0db46e8377219a1d32ef0e3a151c34d82caa1be44f8884942a552acc40b04826eb7aec5356fbe60f7d23f73107f1fdb0f5249fd363d1849f6686a44c43d4030cfd3b6b5d78fe5344c942daafffdcba3a937aa0310595251fad4d9d9f4a747089c73945852bf7a03620ea38558d0fe4cfe5bada84e0b0ed263eb581387f940fc75319c133992e8ce5906686fb811eeb69b1081e639ed7ae72c924227a014439989294ec9b83c6c29bf571acefaf799c4bb4e1989547c56fc42c8cb5bf0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68a1bf93f6e685e900c7d70d8671557e7ebebd5fe34c8efba0e3aa5331d1fd0a", + "proof": "9cd2f3ee58926b5c89df3f09e33e3026f7e1e1172aa45d507f720aff51fb64619ade118b88310d0bf0450345cb77f6d243c1b626e5adcb299512e6603e4c1d2148b1dc3c3f14cee8e540729fa7cf38a1767fa72bd57f2a8b2f29380f467ee1794843d82de184ccfd3c2cbf46a125789c2bd46ab36788edb4ed291cb70c15df2d041fc1442991d6c4d8c10576ab01bf713d2f6e47166e5aaaa1d3653ffc8eb807a8b320ff143aaeb9d96b954ffc4541e6db0fce0ab214f5562f6709989e1d4909d92f99d4c367b8877d4a5f9776be3f4577c7d99260757cac5b535592cd9d0b027878a8f82d4d46250ee91917b47199a16fbb401ebeab0641ca5afe4a9212120baaad69329b8672a262ec6e8234189915773211fd3a73adcecb74de81fe0cbf2d2ed1f435154772af936d95adf50d775eed73ac8a517bf97c583d3863e109a37652d1af32b1023d2fd7eabf03c8c86729dfa834317460b24ec013d09e16469a0a580b0bd5791aede266bdaea8eebe327ef7b0b1a5c4411d35ae48482a688f1a04a832915ad67e572d291c50ca680714c636a9336c52a6a351993d1f2609457c5b80ed49b4902b01d7111b6ae50ca073a2de56fd3e688f403b05370f733d551a53c68a25aea718593b22bacb4754a16fa530a993679f02bc6769841df4a1e5860a4e66271e6494f428439203070b497f035b3568ef5ed5dcc5c90ef74ae032d0217a64154543f2e6c1585819cf529e696a6121cdc8cce57b3c4153e0f09c50377cf209cd0c2601f816c6c72c860048eb25775674c5f9882f4aaef2c2a5e1f9c96bf2e0b217d483b25c8116169f0ea502b2a432222821a2e21c2006a1ba4417f85581d8c7bf3b277ac4dbc9e02e9814b0b19ec82136ae0b7f8b358c569e9b059c03d2b8b4a35411f28d206d1dd1665182a024c1d121482b1f445ba2daaf20d2570b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9c8aff9bab99cf476288da41a7603951d59e62622ec156bba8f9aa4a55e5871e", + "proof": "02a62022ceb3cd9b0b0243cbf81a55dd03e560ec47c9e021e5b46a4530b6ab7e58c61bd1b10ed24f183d1260412c63429847f16a91d2ad18ea74943190065746e2fba789a2f4eaf5ba08e937dcd877ebc14b25e014eb02b5584bafaf8d4bb577eed8e01e48df17638ab68f10afe8598c2247a6a100787f3ecf2eb21035fa963f13f640597a7c9bc6d5aa4e8f9d57583aa09e92ee9616ce023d3e836110fd710bbdaecc30ef36ce37000fae864a5498e4fd66b908b4cf771c0fb9eba1e0683d00f25e8afb90d56f3bc030a049ac4d1a4afaaba5b22e7dc9df89bdaa52c52c730dbcdf155ed53ab62af621d152b8aa9f12839e8dd631ed84d86beb97bde3b8f2301eda98b9d2ab995b15cd9c99d2ba15989c427ee2a42b3cb7214f68ac7d99b525e24a0817c04e784d39d74677d5ec968a6211dfdb515f25896721e551ecb28f332ae729a8b6f25b64b21388747333b732254594ae0e69d9e3a225e431c8ebff161422c75bbd36f2e1ee4714eec681e22ca94efcf6f16c362d7c69774d5f7a34048811a764cb10576dbab365c2de64274977cce0d7c29109b1a7522da869c16d0192107ec7a20ada7897fe5f830f93e96bf143bf5a8be24cbae78c538db4cb512c7a076bc0658f44849161a2c66cf3772c5ba74c2b1c9fde078ef72fca12999674ac08cbccfd64e47066fd4d31fd50168b931089b98b57d781ff45b8238fdd0c7fa6df91b5048443b71ec264684c096aaa2ca3d5d04b7b3186796c92c9f179a71c3e92cd3b01e7d7b0517a3de00f5651151dd6e5cd96b49c8b7c6e58ef0d0ba17de681bd2a3622f96e83d3136ad5c2e4941d9db496825fbd6f84225e14f2e6d751c3304cc3bc2a09ec2771be5ec04b08159450f1fcbd3158aacc3c5182b51ce00a660525ea39c5d37d45dd9d84a1fe6108c76dba75707348a4681d3761b722dd07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ae9eaabf841a731830a809d3bc30304fa556fe8941dd1c1b0a4ea8ae927cf921", + "proof": "54bdc0f7934c336b14224589b2b91b6f30fb05e4f2c201deafa871603365273c80d08d0b52ffd09d341e6d7389b6c1b73fbe2506a32d3ac31ec768bec703993e124be048a3b986fcb4aef32b9546f937c1c2d8e57a83ddd2a9131e2d9a2e4c148c2f6d3ba175d30e9195146a9deb9e25c078ab55619b0d8558a5ac393042ef6ef704f9d0d2de5541f8b853c7c919603426c40fe0352292f7531065d5c42bde043fc6c287fc5505e618a821bc15b4d99ada105ad8db382d8787d0dafd513eba06c9a64ec36af7dcb89d3523fd1e7819a63df5302a8eef337ccf62231aa274080cd46308ea5fd50b3ade5746159f4105c30f26a425b410b7cc0d6da43e6b2acf4b0a46689f7aa172b677c6363f1a9cb260c0a33b62f869b2ff77f20bef48d8d359c08814429c61b98bba6a895e6040f12740e4aa3e31a01a102e8f699f8723ec4fcaf2861be0d7fcad0e7e502bf22204620ad79545f93fde1f1e61974bd76e4217ca4566d3e2fb1d025212ec32a744f5639dde7284b5f2a28d725f3fbe42f7852cd6555fc567a25e7aa1fc18dff312af84f5c329df2e19804dc1cc7b08e1a2bc64ce7e61af92b0c1cbb779c3071f12c211bb77774363f6931e26316a274fde5776287aee08b4b7d54deb292068a6242be03d335a996a589c06279a5c7aa4147d1db8f492690c8c1d94fa53b293ee9a8b8582fbc7a0ce1f9192511346db624e9d06e46f7cdedbc0e050314b61ebceac75227f21b7d8528c2cc455164530820a24258a73c5015451ba3ee34449901f16553915e060a1c123abe700469eda2455e30e6c9f172e8f79ad8424ed2b0e5b8206626e22a2e3a96961f6baf2e9f6a62249371cab0f851a5ccc3895653b756691d18913ebce5ae7044f4d1c55c24265d9fc0dd375951d442a463c67323cbefc49773e133ea744c76b71791d9e89e85b61ea03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ba355f405c246db6261e20f0cab0512a5f7fafa85072dc47798363e7e7c92d3d", + "proof": "6833f5e14ff9d6d1edf34f99c8c5a7545d88943fca1097e11849d4b89d0a3b6e64e79a8e7edf7ae103831cf59ad597a6c758b78b6d1feae4a6598d8546b0201054f8b57eac9d854b17be5092bf523eaab0a325f007308ce7a4ee029376a92e3994bc5f7eba448f794116f3816b6e636322ab07469fe85263a35f0d896e386f4b656c64de0fb1466a90a999acb6170f55f3c7b78c0af791597c3ebc7114fdae0ec571388e21b3df42094b22990528abad19c6f531f7fb03797ae555d4d8f16c0b92697a32daa58a0c25e124fcc5aa551660c17bbcd9d5d15e292a9c413eaa77032ce81d278471ea41b1e0631510744f6e5d1d948da035ace29ac63d63476a48666a625cbfea6c28e4a50db8dc0d2bd0eaadf848c7edd5e773d6acf31f751d8167d66163ecba1f63d248b65d52e3df8343abf3758750bd5b5d287f0b5f89aa803c4468c8a0811873e107c46778d6acd840a7306c22c4f4403c7304d6b4e11022768e69a358e3b8e0a95264b7acf43e498752f5469299bb112151fc8261c77f377d08f199038f4f5787b80a8a2d345983298f6989245cf7aeaae32be0cbc225f25934c1e58d9b62bc4378dee693e8ea2e19ea43bec695b9be4651c4db01cd180a0406c0dbf475721e8fd04aa7fe8bf1dc7340443e981c92ae7f1b09072db3f6fa40f4232d7964f55b67ab3544bfc5afb291aaf6799597ec8ca6615465d085085d27cc626c510bf46de3d3b13dac1989bcc9327d7717aa11093e32d5a2e178ca3151cc53638ac944464c6c3cdecff62d8b85192e1f79587bf9ef7a5034a875716c21aa908697cbbc181d9aacc6fcc72c2228c91e931d4c2e6f11b25417b52eb56b30ddd81490fb01dc73b7fe1696fc6af514e4ed0dec68cb5b3e9ca95a9e78ef5500955c9bb9a991dd27643999c55f7ffaa50c96070585d0d034fc9706ea9fe80f0b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c0bfea0b38f5417c3d02b96e7d238089e7757a07e5915e5d250e256e4cec4d64", + "proof": "32fb9391641bd2c10d0e160536c95eab9c7db0ba1deb75d8e6e8308a9a87127fe0ee3b0c1d775d3302941ca1b1980854b3717a172fa7b99ab3eadd79f1a5d70932b3dab3e0853fd8fea6f260e888e0b9213c8b2e23daaadfc5cfe3ee525c5e073c395b8eb31890f49c9022d91ccbcd90c0d0ba04e26a3744a62c26eb99fe486d59b1e745578e6250bb8a08689788af7873219323e7647e0fdb36fc146362ed0c05059b546d8c21b5f75eac9bb3233546f618563ec0810fe70f42417f2281c00f402ad70ef92ccfa437320e25b315343b56ebf2b16950c2bd9ffbdfa7f518810cf29ebdf1cd3ad11ba6f1369f35e64947c3baa85f32de462c95ca334c74cf7512707ae3fa2bca465f4b4d98f12a9a2b1db9e2e79a76c53e163afdd06278909a43583dc4d032d97cbe75174dd537acc7ad99e7bc2db63ff10bdf0c777296213351ae425ea35805de133ebacf260c2025255fd46a5344bdc725d32c1ed07ad58c40dc521d81e06d7033edd4c15ebd19f4b88fd5c691c0f99fef1bffc96da109db663a2295b639ab4adc6ba13c85e519cb581bc99eede275599b277092a2a069757ae2a1b7f161c5ffbeeda91fcf43abe4831232670de7ebd777a58904066bf2ed44548e3072666dbcb7329a812bf3584b3cc0bf4d3b7b2bd0117b233f58c447fa75dc6e8bdda4d49ecc5d73aad7d034cdb276cb08a3938524d612a96589e91e785546608c9e7a472cdd783c2a445a373d3a99b5d5331e2e3f2bccd784ddd521e423b8696274178ad0f59c38d0941268809cce3e311741daead5dfffbbf5ca1f8f0fca0da809b9853232d95fa8fb3460b0ec9e7db90848ced3f8da877cecfecf141db10a63acedc272073b3bbf1b861d9d3ada410ccb65b41c2eeca3ade316f7e20da697a8a8be671b9ca7d3214acd36284f3bf1c3c2c9695d94d19786eb6c479204" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ce2e977e1c2406f0e43d2c72a2b8f654e6720322ef3922bb3311a7e3b3ad0575", + "proof": "b6349d78d4433f3e49a67c2f7c7a5d3f1cfe786f902232305b4d70ff23e3f64422a520b0fbf7d41640af3bfe62ca33764606e4c13bd171bbe79279e328b3137362b6aa326cad538f8019de3dfc35ef40f89eb4424ca623b043ef54ed75715f235c89eee548f51e47a4da3add44c1c9be24092ceeb0b5177089a9e548df38686d0852758dca8b5d2295dd7a558d0e87168b3b4d8c6dd2903e7f4d2926f3e899009f3c76208d43435edfc9bbc6f901fe8cefc7d640f40615cc45652d24b440f806d6f7a9606fd854f4e13aa28332e53a105db72b11292019090ca1150c3ec4fd0cc677f6e93c994313ca96e6d7a1a97a4262ccd811c1a92dd30cfb29bf8cff0a36b68b4888242c4b38dd73e5779f89e1f8ed043741d016d1c4471b90f83f8f3a54eae83ebc830dd7a9c05dc183db07bbff4e5cc74b05476030bc25452a69390429aa2b899097b39fa3b4d0868d5740206f792182c3151fd9e8b6ce497cba296b0e906855cd7698c5c1b818c30da17ffc5877f2528e024ce28fdf559e63412bfd31de1f3ed54c0f7b411ecae929aa7def4bbbff6459ece70ec59dad3f4ebc5a2f7be2ffa16620095cb7223ba1a24ab50dacf608f289d5c82ca4cc738bbbcdc90e5d12189c0bf591b9c90165e33b268693a4237edb9c52dc61988b5b3c883fbf08321651b968c4521152bb61cdec0662fbfcc6d657c68b0708f65a28008bf5692e00fe981cdd2f053cf9919273c1e4eaba33996343f1d50233087b927860aa3cca630c78080d22da1cf7eafd83ec258060bc394b85d69907b6694f737ee5feadda45ee8dbddb1543dba812826389c9c5325a0e00f239377a72b8799e35dacabed54b08ed3c082a814983ca23d4b901430fb3f96c1bd075b5c7e941e120b9a1c9360ce16fdb82dfc2f7da485f73e894aee1cdc6cb26731a8f2d9569d5cf2149f20206" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e2c297eeb325e043a85a26e07ef6da3c8c8b2d4ef718ad03c61ba258c9c2c649", + "proof": "f8f311b208cf8b6f233dfc7d69aeaa5bdb1084cd3ce266b6deca4515dfe27f327ab589e3f6ab829a9be1d3c88d3d3c7390690fc594740427c6a05729fc927916584dd4c78322562937a6d4a0310203e45fc395b101a33b4e1a7738e560a27a6a3812f30a6628c7806496b465f8e668f0cf37b7fc5987f5f3b91a2192338a142d4760ed4ef77c1a7b99f4efe5a1d37c9647afc37ccb940bad4798b13bfab8560ec5ccd2f56862d975bfac26f58eb665165ab32f4f7e3def33a4fa45a575d2b00f213a2acba43928241b219c99c51decc8926095ecd63cb849427214f33ed08508a0281ea0581a95122d506a472525d2b9403a40908329c4888150f833f040242e5a6f3032330ed55f5a010e3c74d86ed71f580ce9cfbd6d3d3f96d79615966c13f211eaf2352aa149892fd32257aa371d69befbc942cd9cbe1a8b89a0e0b1532b22fef80c1837589982b27d3bc27cecfa48c6652bd53f92dea4d4d0dfc3a979047ae49f9ee83e3536a4f71f05a9cc1ce452cf93e84f7eb9f64e67674a9bb3232abc39fa97d8183f5d882249a6dc1bc0a490bdba525461b9c814f0471e68dbcc03dc8209da2d5a3641d35775720793ea1805a473306183359eb73a2b8dee67c06958048b991bbb4dab98786499e2748d551a7758dea7ad02da541aa7464778c558e297d1086b3de7efc60b90e08db6e2c90578a9aaf2704ffa20f557635dd4af2c42e5c76385ddb2efd293f388b73fa4f9c3ac55e8a250a6d6875f118feed0b8176c00ead7e7d195ff98f83a0b606ef545082a582cd12e70f2b092b6c8b0603350ea7e91f4e6611105ee96f5a57ae287390266704fdb0bdbb83a2b9262bd63145172fd10b99362691c53eedd3cb0c3bfd4e8351f498a1715dfa22a3922c5d4260a53b95d3c54361359b151c68c8b7ee56732ac6c4a33d3c7819e8192b6d0049d08" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 31 + }, + "commitment": "aa2f6505920b1daa39b2c084d0029580b926fb3f8061f592f257107074cab814", + "proof": "6a632a308c2819d65dc99dff30fa006278b6ca6525190d2c3280647a7068574142e71b45fdee7c9e20a829a5ef4cf8e319273afebb41bebf622bcf83c7c545238e9a2bef77945eca6faf93eb4a2d655a56f1c5cdf248209c78e19aa2cff3c46c9af7bf58155e9ef1b1f38fdb8743be723490f1c6c3c9c454e577f422354e92669b497ca92180533ec5088741e8f6d0ba915174a57d28c828cd3b7e99adcd350129d598f3ffaaf59a2a9a9761d0cd722e6b85ead6350a384e7b667999ba8dfa08f829be9b2ff98c0b7067690157ed10ecc546edbd4bb816ff0cb9f67ea1a33807b0e0643b702a70f0e9530fc5c088b7d4cdcdc58174293eea15aed3b30a8a082c6e1b37464e5f25ffeea4ee65ac829e0e3a0c436bc059b2f14675089880233f6f6033d4155836d73eeb14ca0dd70b075b565596c1d3ed1c686d709aafca2575462a97940fd946a87635612131b26e006da4d3c28ff78657dc65094e73aeaf7d3cec18ba9ac2f4928344f4daaca5fdc07e4fafcedd83555a95fc5aa4e2dfe52d459a1c36bdcacbc9a43468292e5c7127a28349a5946d266e44cfe45884baba5d0da4f7b7e4a024326e42b3412b53beaf6ec6a087d5bb79a9456183645ec6d1350e54bfefe59b01967a653bed0c0a626a18f3da704c55e22bf71519bae1a923401cce8afa4a60971ed9561bb087e7e63f356f295743ae80721e56169856719abd1e18428fe144e9423ca25e3069168f0b65544dcbfe2775c48a96b88afe44ccf6190e7268b677da867c6460dc0a67408de4af9b5c790a083aee396638ea904e477e32c15d4cb6798ba3c77c0ffac87b75d51737dadf4326df198660545693eca60e087b2fd7827feafb7fa83fa4670a7c8d0827f56412efa2ecc7b0462a2ae9150a849e4f826bf40ecbde2535e6b3280e91c8a6dbea0fafdbf5fbcd0d6c22ffec06" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "40c6d6d42e4105a3f1303994532d4c70c31ae9513d0f585f5c8be29e9301ac6d", + "excess_sig": { + "public_nonce": "18481536be894dafb924c0d84dd99c956a6f0879b7811a789566bf5358b73f7f", + "signature": "c06aa732110ea0f61e3e6090024233f957741e9adf094e3b31e7284cc077d701" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "567eabfdce0901a22bf0a5c12083776eadfa82a82e7c8848bd3d9abaf8c4e203", + "excess_sig": { + "public_nonce": "9ad1104c021519ab7d7bac75fb69f6ed8894b368bbc85499769914b3c4cea206", + "signature": "958df517dabb414c648e65b2271361c76ff601e1d46cfa1f9822611f1177d30f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "a03790172f644280c56f4ec29e9ff035248124c475588ed57eda90411759786a", + "excess_sig": { + "public_nonce": "5cc33fb436d94860297a679bc093a4136aa4ef7587f0d877ca0474a61499f857", + "signature": "8f30dcd51c7d917750dbcb86219bda89e9c084b09c8d61a76ec7d0b0601d8907" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "a2ab7e54774ca87c3d71cf0bba6b6c0300c0d1a3fbb888230529b94d3545f675", + "excess_sig": { + "public_nonce": "3a021d9bd4655df1cb81f94a37bf754c49a8a95b6dfb143d4dd9aac2e2456e09", + "signature": "433628c35edef44c048052debd49a65978b1abe1b15ef63915d1fde2a596540a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ca0c6329d2628bda5a887c0b0f064995ff0f1b12c7a9df49e9109b750f2dd57d", + "excess_sig": { + "public_nonce": "f030802522eb65c944bdb51d5ead88d8af26f2504ef1136724f282fce15a0c31", + "signature": "aa633c88930a28d550cf1621ffc1ea8afc1f3f60da66b205ee29d40dd721700b" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "7e6113be34552e853eb240d308320a481a3f40f52fb8063b014851fdb70f4878", + "excess_sig": { + "public_nonce": "60853c048a37859fe50853a5008ee4344a6a31d36d3d8a84cd5f963846c24235", + "signature": "cf806a45263416874a138007a9139735638f61d86d745bc3156e8f5db3717302" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 31, + "prev_hash": "15f9f34846bcdece04de6f4f18057eb0484cf424ddc801ab2bc97332f2f23aa6", + "timestamp": "2000-01-01T01:32:01Z", + "output_mr": "580398d806d5c57eb67c21656b8d94de45c61175ce6270fcfdb7b8388ecae2c8", + "range_proof_mr": "51459349b15b2281a55e9a20f0876ee187f2dfd64c086122a738a9da6a89d4c9", + "kernel_mr": "138cf88492ab81237dd1027fa36a912cf0acda2419af21c6796d010588ce126b", + "total_kernel_offset": "1d438416d36bca5a85b474a9a04478bb783c5f881bfe4cefa898e94883f43108", + "pow": { + "work": 31 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "102aa79268c694aed029565a776f594586bda7483a5437a07214815912654136" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "10f14b3034bee8b0ba2c1d38d317823dc4cad470773f5478886d681c6ec05340" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "367a70f6186eb5b126b70bd6702a16e182fe4a07fa92e5a6edd4ae4b33ebf023" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ae9eaabf841a731830a809d3bc30304fa556fe8941dd1c1b0a4ea8ae927cf921" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 16 + }, + "commitment": "a016f012574e9afca414948e17435a0821a1bb87a5bd5d48a809109efe388d6b" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "040560f505f3d8d6c94ebe688b58bab7735aeda0db88fa101d11eca6d17c535e", + "proof": "a02848950fb8d40b0c711a35f86cd58417862439fda3efa1ef34185ebc98c57ab2bb7112bf0bbc55deec7a38b345da864c885c2b2943b01fc4c0959434ff0357dc1db9d4e954ae1b21dc6962e44abdd452b3140bb9f7d16a4f30e5d6c804237252e68f86f8b72d58ee3bcbbdfd35781730fc8e11aaf8905024432fd22589a678d0cafa17ee8d3c74e6f2931d400ca4219de02c28f6b34bf3ce3db3f1e4840d05ca5993e528f5a20d2af9503f68a74b63bbb42adf3f317d349be56cc22a3b840d78f117908c00c9faec4d45c8b82da4421df2614c3921b24fc06db13d267c6c0da4c42d67bfa4e39c2949fc382dbc8d9ec9cdb68670fa135dc55d0a7d53aa9d6f225040c4c62673af307f72e254980aab231c5502bc4751d7b551fa332fa81b040c6e933e91ab73c32f440f3b561511664d5a4e9c9454caa1084f2eb024031973867e684230124d7f4d6ad9be57c06f8c1c828e5ee0b5251c213d5a4306be864f880957390f62b1550db7124c23e999c786c3323b92577be8686ee24a95c27255fcd657aef46d5ed05dd24e4f7139f4734e6e4e5913b1724d58e13721cf85dd1f585013161e72a2f9707e9ba8c4337e56b41c210f229664e82585b0848bb3386d22f32c5dd7064e03cd1fd59787e28aa19716f72780a31632fa4ce976a890373ff64625c24324250673c65c190ca54eb995c1442af6e5e4c28290ca146304ca1d1875f2c5612ab8ea9f9910fb9a208252553c3103b3cf5b837bf7ad63a87e886cf0dbb78c5cfbfe98a4b022e590a323e22456d9a7c649247174f54a2d9cba3d468ae49728622368ab166e8a69c27f3496de201fb25571a41307ede6896f4ce328011561885d7e8368796faa575ae7afd8e99c166b2d0c8a5388db195608a8bd0adb1cb51688df0c2c0d5324482029bd8ca4251a86cb6431b3c1c3d02e7ac9190e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "086ca2947a85fd6f94fa289a0a5f3782aac55e7633479c3b5c8c6114339bc213", + "proof": "eef85932660ea0c568fde0f73fe468ef40443b735acc3eeaaeef3da6ec0a2f20b222f13e887be571e82290d567ea64f140b35d65f164dfea7785937f88f0e235b858ea3f66af35e3c5f16e0bd4ede6e938d9560470d57d8268ac54f8d8bbbd1136d067fd0c22e486a12d72b24f300b3da5f157113c85f0302605c2e83825d17811a5ff6fcc163e6a8af79397d3e330b2ccc9546c2498c9743f174c8b1245c90c3292b31b288ba70102f897b6154492824da9bab444f79a68e2b7579289d042008e6d04059881abc317bdbb78d5a4122c3a23be4ada2036004247f6ba42aca908f2b740bd7321bf61b919f7bec48294002be16f306d8baa9e92804213b2750a248a3345cf244f814caf05c574d1c4391a348379cfe6620207c1f3a6f59757b77a38141e2c1146608d4ac35ca62e090af8671b6fc31f85caa861a414d46ee8467f6ee2bebe92df47ca87a53942e2254f07be62d7715fda1bfaf026cee7014a7e79de9d3823a8ab56873b2f5691f70bf159cf854343069bc39fe9562589feefa318248b6832e7ab030f99e7c4604208ddcd73cfb4ef5c9d137820d444b609d77e21c0320a38baa6974029999cc3182a8d17a93d2b72dc894e6671a8663828693b66e2afbf4c98488cccc650c93c7713a3916ca5971855b86ea12a8168540fc5f4187cc394083f5de2db3e3c799718e20991271c3c3de29c0df80c2387f6e6ca0f6f94051e20b8d1405b2c9a1a459ea56c974dce62cf9d38322ecc388c941b5e9872e47def3cefb964a217d7a2f2d27c8dcd6781543e8ccd287021080fe253bcd8102097993d4b9b3258c2f97cf9464b3b11adf86f8801c47b38da6bb19b553c660f396312558aa5292e57b9c574b48b8fc85b996ccb656e0b34b58def8c0151ea0164d8551018fba5a20a64a947fdc50bd2482dd88635e909ac0cb6d41652084a01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "12a226b93d01f3e780c0c6346ea906b8adba94b37e6875f60b8c32b46561e64d", + "proof": "f6b77857eff13d761d51669d5b6e93f8682cddaa493699a967faac89cb982316205155447f58d9ec244c0b373122bb668d9c182ed5ae17af43867b0557415f143c93e63ec00b950e65b2ca09b0307b92459fafedbf012c376a5e60a5b352e06828bc4594c437278f535542521548d2da3ba413bfd4d0361e76710870b47e0960d77518c3c95bc1dfe2c08df9b3ca699e6e6b57e4e0cfa9ac9ca036f712cdd703221068b2f44e3fe3fca8379fd1e02d286817dfe8a62f12a5f3b4776204f0220037b0e3669d205fd02afc9c70b587eec187bb408995ac5952832825e8f5468f029252717075c5edcdc923a864fca47509912bef97f97f50ef88bfd4ef200aef3196d349a697adda338460698846e252fcef8a0051e95639cf5d52a0b7f5c0835712d6d99668ff7a29c106e7fb82b5d1a87ad28fa64598942585b6d10403a1287c3ecdb004891c2a90b1bc28a3f5811d0fdb686c11997c9d5fb3a0afcd2f6bbd12ded3342e264be28814d85d06a742862726af2fb7a98ff8d97bccfa645b7e721836548bbec9fa6fe12f9597db183b1605c5c84c35895fb053c572f12a0eb75c66e6938b88d5a4bfc9bf5b4aee0ab8b831312d219150b70e0a7b6383d2c81dc473a22995999da800060243ac0cc4dda6e590a6b8533bba67caadc696f0f9d51d0a68408ac36631307ae11bc47031cb2523aae90499e11102a0d1565e773c71dc31a8dcf52e1d158b7067de91ae15258006225d6e9880ceb097981da3adc497893fc27b2b81f1dcceefa908ea52f8f1a9644a39d1fdc94fed7d660366f34c1ab1432cb3193f50f7a0b89f6b609c91500dc634730a921400a38ea3d62f2ba06a953f82a7f6e712da137bdcd49e6b1022bf7e80d10cecae4648732ad7462fef87230a999183e728e12a6c45b936474816c88e04d106b23b4ec54bbb5a10cbc878960e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "882010b75b9f2023560ad3638fd3e1180ffe13476e2e37f0cb9bdb7a6a4bc408", + "proof": "8e73d452fc012faf83b647a9b99b1d236b3edac20000561dc095f3b0f28e045ba0345a19cfad1929d3628e365dc9a51efda0c2514067ca3ab929dc037e00bc49823446f9f9a70e71d062172682054730009da699e841fa3ba2db09083eeb0b6224ea0529af942581d480caa88c92da3bfdf82e09b9ff8ddc6c78fc239cc34a10e15ff81c0a3bb79cce428dadf750d93dc9c0362e9d7261241497fa0145b2a90dac9ce4ab092dfacf24743c093e8a88fdaef29c391d710a7e8fc11776f6d8d50a50910ed6b85d7f7535b107651057b435aeff003a888150500c8b1509d0222b0bf8bd2b1f943a7e69d4ad2dd2c545501362078e1d28462d98e2a8d41323c9d65b26f2a0dd5ded5dc090aa73586760f7193d29c571d6cfc432ab676d9ef2d8e3283255df77b6accc73d1136de5e00bd4c6006e092c28fec89536f487dad31526141a3be54ed861f417c013a38830cc49dfaaea621e739c6eb778c70a18cd12162e74bc0884bce111c514805e99171490929c9e05893c1a4f90bcb57f3de47127678c4e05e9cc0dbe4937b2d422318aa04cad0162ccf34d2b56fb73e2d34284511fa8090ae04145ae15132ed893bcf1fa556a103d14516ef1c8c7ba17bca9eeca2da4236943ba3acae57697c9086ef797f90f590c6cfd769741e4fd6f35c73e465f8c190952a321ae8feb851d6dc9824b2d0e8dfc033ac822adddecdaafa3504c0e2a1c281c4294aa9acc882875203ed6c933f0a6008fe79ea7a1099acede4f974bb8f9fb437c974986761c1d2ad969470015708ead02d9949d4cfb593339d8487bd468af84d9b0ad0893180dabc1acf4ddf6f63e68e8e1dd84ad82645d2482ae0f164d4e1e1d0251bbbf49a6ca66133c793e71d241f9eaf52383d88e2ef057eb0e16a4b205992f540b9636bb31a72dcd9d9e2f33e8aa8fa07b8905b1006584ff02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ba20b0baf53f5bef2fb9c78c59a797efdd67535771a9f4fde723e5b1c4924513", + "proof": "d87cc68622ef5558e720b9727f5d16e7c016d41acd1a2130ba3946df9efdf85dea4867f0fa08582996a63c0e3b45c21d8798140137d4f2c616fee44194ca3a5dacc2698300dfc2b72897e0668f6a8b9ab6d5cbc0ab751383d19e584cba7abf07b09ad38260079b1b630778251699e425a70f0fb2ddd37695066c1430ae2ce11e46795e47c938e7592843f81f9bd81848b85b891e2f2cf8912aee9602c95dc9004963bb8e98e457168ca82830fea1de407b83ec7c91bec75388fe653f6532dd0c3afee384477785c14f6a5949e67d66b778366b6df75d640684d5154b8a6d4b0540704d6f4ddc2e57a74ed21ceebfae27ce9d8e852e412a7a738b42cc16d1053ec0f2dc54b4eeec96200d41aff0a245dbb07a66b807047e67fbfbb0c933461729749cb51b57428bbe3f97c3d9ec47f36aa1b2688fd8cad507d759212abde18771e4d35583dcb6294b02b1248ba14844193fb3ec1319d8ad1827ea62d8b355dd5374f9c84dc7fe70161083d8546227414a08f5bf5982d959375b35c583215df239e085005a9d8b3e6059cff59a73321ba10e68973508274de40e95c1abdb7e4e6704acf6195460be5c12601d98ed64ef4c1ec18d49aecaf7915f194e0cbbd5eb5f126c2a732dcde1d61393516d122790cc761273e04c61e1d8a9fcb4c29f98ca0ba6e6e753c253f7c887c0e877c1ea4ac5949d0706d3eace64e8e351a8aa5d311db0089898a4b0b7dde7f8af4a6b14f0d370df87c54b82e788e64a2fe39adb3f76ac289dfa9b834ddd9274782348fcc4dc34d5a1264f434eaa10e0b2e247b32e17e497511f593469150c5d34f64300e4218ba11c0d8aeaf9d163a8b8474bb1a006dedd2034facfc1978aac8628332567691327b5ce37b3e42e06590e3dcd586200c4d11950991ebe1aad04b977b1504d90f4cb1433cb85ea7150bb7b0ead142905" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "eadcec559f12a212cf7dc70e5678e88112413327fbcbbd93c48fbd9f90443e13", + "proof": "7c53803a2bebdcbdd5a1456c8b0fc4defbeac5dd2885733e7def5fdf87fa0e7532d77b83bd3d44641c156bca4e2542a7372ecae1134decf559f5ed1889fa932ec0bdf665fa3db5db6907d5082e5f2e7f8c7405b935d78d07cfcedc4ea675007f1a0e0d504646932bcbbdd253e224d8c8ada7bf9c2dfb399d025a026128774060b766dc1645ab528cd8440256435efaecaa1f68e1e5e274d621660146339f94003ce97f29fbfcd253c282b456e8a0aa3a3ddff47e9af0e77ee89e7b7a08eaed064631931adeff35de2e830404ba72200afede0d5084636cd7e2e2d35d84c65707bc058666928aaa8d4bc85406d09ffbf19bf815c764ede3a39a21599543496f7ea8ab4a9656c4c81304ceaa6ea8745b9106bdba7dc3bae6219d37c533bb32593be67cff602d9e5d6fb5b7f1c127abe6000d5fa39be9360506ced4a5f012142a2e5e7a46798fb50beb06a68c5b3dc653087a3d81746a57260abc6b89eab0a2290e0cd0f7a7dd9059a4e167fdb64ddfa0753ae94ce4cec5a5b0e5e3fa7ff06f3a5ba4f18bf9b0804792464dc332ecce29cf998306e36d00b87b3f02dbbf4436596a2203c2f27d199ca1b95a76ef3b743a58741d45c406ee0c39a06b28e226044e2a88c58c3fc836a062e412791d59b334b35ab55adcc64683dd7549001957d4a63184e85667b5d0da9e7732a55a184ff4a8febbccc9663f3bc8a9ae73447394250008d75ad81f0bca6f2014b6b95dd1bc9600fef9d510f2a6179b32ace49a503f0150a83201072395bade51a66102d90686ca41f119cb83497a121723ad6167c54610ee9eeec2671da99635f5180ed3e66ad6b93be4c67545ccffaa56328c881a2e5f676078ad3722c494af39d0e7db688bb684daf1ce7c77032d5561752e65bf08534e3a1f59a7d1dae607111047ee93c0808a767459555a7aa3c38a724566bd0a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f07b67d62fb7e21a94f9ef7ea7f0aa09c076bf918c0d9419f7519af61a963959", + "proof": "4a42059db68f09a85b22d777abce5829e9231576713a2f399c4b9bdfea28025552fc10c3c105f8fe8d0b0166d4d1152bf85e1390a3825606df8c9c5d9562a713f8c723670072c4235c0a3aaadf67594f561270155817620ef07f731c6e353b781c959d43aa054f97dce7d0bf38f467cea4dbae411fa944c0f309d8c395d5b5431572e66a7cc0500ba8bebb434247eaf27bdfa404a70a2d5b09621844918e7e02d5546dcf381ff7a08b370ad84d849f45f73c300fd3407c1230cc14164024c806e6be557ff4a4cc71b2e9775176667b87abb9a254719ef8dc2146e948208582027ae872f3de5a195e2c6c568787dc78d24f73d4b0abf5a0e50759612a1cf2953e76bf0ae81eb590f82dac5a612d2eed1c26231798ac798de71ecb89eb4289be1e1efed87f15ee5f9b9bcb15eb5b2e24e6309e1571685658468869d014ef3242589289e5177abbdb0e8c7816344117cba22b122527315f284a063214430295b2179078625ca6bdad60105998bd97ded3c9dce39c616c76d48b35179a09f7048a07bc3298281ae6fc1306a93c7bdc1b5e670bd29576b38b7874e8c6a4d1da2f444cc21fbaba3a6a01de66d4e7f04b4e0c0c0aa000b80e7c3601d48809a4f2ff9c74faff1e7c4e86c54060ee520bb1dcf3340bd9fcaf66e728c9bde4fc61f80030627a19f3ad8754fac6a56322ddbbf958f7319824c67376235ee3b856d056b2d11e1035c5f0538eb9fdb4b75359f4a39d19982fba6afc2bd9c4ff17fcee48ca410aa2a9626dceeaf1e1706b7c81df4ff0e6b687f6b51472d6b7c604c6e1a819ba04e26aea23e863a0042eab61e1808948fecdb91084d273b8364f781bd1da86eb7447e53f6659f7c20f11f6ea8a20decb7f1b686cb5cc75c1e4700d819725780a0f83843623932c6e8280bacc52123d9c45e4220cc2c1da8b41fcb7c16c2b59390a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f225f8352ab7cce8c3224ae987560ff0806b493249dfad04998954fefd38ee59", + "proof": "0edae264a6a52bc7efb6c3995c283f02f7f2750ed63dff22f70a0461ec53131fe4eb49605a9457058d78239b6324b18175f47d9ebf66e639710a5992512f1955d23e272d85a0ca80616312b5e25a275dfd06ac524c9f9262d3f6d95d316e5474781bd8e468fb137fba3786e5de9f2d7e938ce896c9689b1b044a0e60e39d16154d3efcbe64ee74aa81826a9c1260ab5f0a0831fdbcd6dba00eedd24eb632f100282889e106067aca7b2c140ea2a7fbf00488593df8afcd0bc16b313b9b26a20931dbe3377e9e8076126725af3971d2c30de630d3337159eba897650ee83bc50c760dd92a01c8f145783a32a8e36a37687180ceb736c2048ef913dd844232ae36d4b3252b8fa8a6b4878278e7a83bd7ffa321cd086300ba4c50aa1b132849393682a928bfafc3547909e0fe6e7ffabf3b3d5b145e33db74f6ed05dacd015f7803fa20b33f9de2d828152f4e3b04c31d4ea5ef7e906b1695f4c85acb98c00db95dd0a8551ff927e6089ef471c5d73577f4dd5fe083e176823fa6a830499b41925b968637d79f20cb67fb04cf5cafa1e6a1f9f2108a2d908f1bed86887a8493ce46546c9ca01aca9b749695a0ee02dbed90229910016a3f59d7a056cc8268ace95f669e3dfbc7a97e1cbaf97beaa938d6678719147c5247092839526bdc315a1c5c480a67cb69f17b5910e46611915fc5ef395b60deceb42391f905af1b5d25eb6362f7bb3a9323b9106950ea29e3256545cf414088257ac6793c211b96280f9564c284d2512c4981e5172e173a9685299ea21830e186e7313f4f628e47b09c8122fcb8da072b64193d30112791b7e957dc8241affd7e82f3c8321ac1e8f0c3b9691e1285fc991ffec9b963be627b1d19c17bdf9bc1533e26435c8e47650d7e350090f8efbe32aa2b9d15ba97d3527b7198397373c289681016a91fa20c3c81be0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f6e77a241edd8a9baf8d14da1df8a13cdefa8369b9fe52d50b6ac120535dee50", + "proof": "24dbd29b7f5732a2287f8d8227555348f4736422a73219ae848c9cd6c55c7c273424bb5bea4488ea413abc7fb4648b3c79c7ce3d53062c223593f9ec2d75652b6c746a61ff3879827d79f17c103165223db4c2f3332d65392e05100fdf9d58594e90e665849a023aa6c535f493d0af4684f91938aa3328ddc06cd37ca948c93816814919d96844ab8637c833be0000be587e1a78e5e6d72ba222e60307d7170bba04e555da7171f30c69fd50fcd07ef4bb1020a9faaeaa00328b0a5ffcf76101c96952f3d98df412efc515d02507c95ee1410c0c3ea8aba4af3287615fd9b007622b02739bc567f8d6f2cb4525e2436ecfc14735dc8e737877b33ab7012e544ede5338f0f14372720f4a3822f77547412a5966632c5a17e5e7378f78ba14db7304a2fc8b7bd663696ba935c21fbf599a5edc7b8b9dfa2d7996bef970ece4d56c5ee780421faa83f523e0758a1e9a1e6853c6b739943e56190fb5a7c65870f23b8839b96bf1bda2836d8664f112bef47c284c9aef2a0051ffe731250f53d9572acedf590b45046655e923ccea07700d26e556b68e42d3c5b1bf2eb0b9123a7f5a76281c248fc4dc78b0cf97b7c607966e0c28fcde612d89e2ddcc8da017feb0045a9035e2fae3011ccb4c4b69d4792d9d4f7732329e0b6281e349c814d6755b485a6e5647fd415e7eda1fda057f35a6a0e17eed3fb891f29bab8f38b72719100c4eb97d8a777648d66855be8804fc61d177a0c4cab46e0c3e7b51887ed097ad0c9e40cebe04ecabe72d1cf4bb7e2cc54a9721bf8044d31dd0ecf77ff72d073f0190e64ce3808141f03dacaf11fcdc2eeeb3f077a4eddb5154d9319912d104e40978c092b322e47cafdb334b33cd580079531d1c14cfb8cf7ae3735ed483320a0f36ffc33c44fc29f5d78793ac516c29f0903d94c62d134a4a17dfffc3fba2aa0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f87197d2ec15073651ce9b12e9f712c3349d496c8e226a4c277b6461a515860c", + "proof": "764025dc05c26615fbb1c6bd6a98c31dc6b0c201e8f3b0da79658316507061795a24cceded7b1aaa7cceddc6b814c0687140e7ff0833fbcd5e9311e26d5b52346a801ab188544332365354fcabfb57016c16588714b3166432091f2a053b00705a871219687690ea0c064a970b4261bbe07ed564afdbc7b061cf4748c8592666332ff04840936097da17a088595522ae6861c0efa6d3f9c6c6dd22ee75394d0c21291d789952e91c073488e4c0ac1ccc56fc48c5d0dc65165280399dc8ea8209f5c3de0797422b3e9305904833940fc81c96b679b34128efa3dae2a44f137f02385edc7f0377335dd784206eb51684cf0eb25ecfc5e83b7e1ff9b08d61f9571664972497d9cfd8a1a002223d2782a0712543b6a6009299c5a6d898c75eadd51fc6a6ae25cb65913e99cf2b7c25888bd74a2d8acfc11cf2c0f964ff2435eb506db8cda8bc07112b24ed8cad573f7511cb5f211227e138996f44330e1b5fee5658ba9844ad013b8ae73b52e2fb54b43c6668310d4242ddf37d74ca13d2b6414663de5871bbd0ebc6cf132316b44ba00b0c029403a87156b23918a098980d28e83e587bce889ffeaf8bec277d0747288770012e55b6222c7c09635022387297a66c586f5e03036eb3821082493c2eb54e6b7a98f4c6e09ef5eb96d1d89664610c3f10b2d15dcfb5617c17ec83d83994aacb477ef42ab4d659f7eb99be30ec01613d30d011ffce737e8cad44978f3e305eced0e5266f5448dc1ea2caa30738bc0a098c3d18a8d2e58c74b30420fcd35cd434c8d3b68062031217177e12c44411df5e92798c8bc8605f7754230e1f74119f1e19f1ae3a639eaf9893c3ce8746bb2a7055ca1e2e930e3b1b1c77653cb1f2d01630006d3fb7eadebd69f6767d9038860fe36ec4be889d41d2dae0c4cafe6a33352d7518dbc683f39bbcdace4bb4efdd0a" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 32 + }, + "commitment": "b2cb08198634a4744ce9cb0dc037ef875404ef9a24c233b659b444b2396ff823", + "proof": "4a8c83721283ae2d8d1edcc25818a0f192a0b3ccc84229810e39f1e455b19a5f1cede439f558ea86720e7c2d3ddb1b0521b0dbf75e44016adf540cd85fe4f825ee87ee590ffc923cc33ac515cb332d6bfa9d3c0e3ed97bcef188c3ffe648595dd6e25772dc34bf8ae6a3a9d0a0896ae486ec6b8c8b1e05c53d606a245b9b2978ce7539293d853eb464170bbfdca03794c66678f22df16b8c60b9bb52787a740364c49a2c20765831e189742461c4ed45010e126f5059e7f2485cd0dc23ea6f04c546e43cda58e6e0b126343127c058f224b04c9d00bcd56d55be9621bc8b3c0f94bdae9a69ff0dbb1eed1e8fe6c680c0eacc48321db7c6c29a60167d4c69280b42402604e9845fd5d70a57dd13f467e6b82ce5116ba0507f66ad85e0179cc96dc8bbd744649c1ac1828738f04c291dbfcbb9c7504e509e6d84213928ad46c2799212e6ddf93f7ba9e9b31e8bdb6c616d3d9ced20c99e29b6bf2e078ecc067943741e22cc6d054b18fd972127561b198ea3b1ad03af09bcf2dd8943738ccb8221248603885861487b0fc8fe80d78bb8c384d84c7fdd6adc043b30eed9dc1a5e73da08e9e3415b9698a0d93887a7780bc0f93d08f1c279b21c4e0bdef2fce4b71dda79cbc09a2e865c166b16a07e932edb2968546cbe25a87b7169c532471eb400fa44043f19524e38e8ff0602f9fe7db54bd24b63e077cf96c37b26f5395760295247e0731966f5ae405c790371d804c346b6ea2d875d72801bc3a15458b1c257121ed0ef44fdffa9a96369ee5cef2e0553e2551f188db67ccc07872b2e4f5368c804a0fe2e2ee727f1c1ead0925a3d5d85c8f4528035894257c314bbf6bee030c1e73f3dc06918558a0901215a349a0d4d31ddede244680a30f6fe59639abd0acb172b5093d5710e7e71f796922d1c96eeb3e6f2049ed39115ec2a07c0633a0d" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "44e35af8ba3b47d9c5b666a6f59cb27c88fb5779593ac8ef37e50703a8a0c26e", + "excess_sig": { + "public_nonce": "8a9686354273c89ee089e73430bbca43b5c30d8fdd8f495655066d8c018f2123", + "signature": "f1b5aac8115d764df0e735123f8aa4449c7d222fc73b8e8e51837f52b2e4a508" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "662802018690622daa9335005b47a5102cb3cbc2a146d0f920234c19f79f3c7d", + "excess_sig": { + "public_nonce": "f055131b559616027a35f28226fcbc031e0d6958c793590a82c16eacd172eb64", + "signature": "7dcd9525d9f61be39420dd649cb4e3124e2b9fd643671357c3adddae1f6c000a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "84036cf57f8a72ddebb1443024bee2771be5ef859f1bc2e3e32eb6bf9fd24278", + "excess_sig": { + "public_nonce": "c297a8219af754f9f2cb9deda7b387bd35c7a0dfe991b472ae8be7cd9156141c", + "signature": "e731abca7c24785f6927456e97fca2433168fb15d150451584293c2797879b03" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "86a588bbfe5c46f16810c4d1c22746565686e14f3495685ec9273a7abd65bc34", + "excess_sig": { + "public_nonce": "c8885b8ce882c9f1783a282d48db3d7ae8aaf706255e7f0f4c2b2dc79eadf703", + "signature": "feff0442b54105f7da86975960e39e33896fc984057bcd70c9fe8d2a286ead02" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "b0e191ebc4da95cd81e7a898b4f20589a6e70073d0149eebcc63a6511ab9f116", + "excess_sig": { + "public_nonce": "f89f3c97ef0a5b30f7ff66e603870479822365aa1be2b42cd73f0abbb8a54e73", + "signature": "1fab967335a556413c0ac3c1798f5210eb209ed1b5493bd4027b03bd9ae0680e" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "64fce4334c8b7946607ea93139e8fc183bb47a404922d907994f6aafacd04872", + "excess_sig": { + "public_nonce": "8654003ea3b90c341534fe2c0b23e3e9152295d2fbcc365f6cf8961387a53137", + "signature": "9111177c5860f7acc8a0cf584b9706c977e12412566ee2bdaa367d4a448d0a0e" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 32, + "prev_hash": "b983754e36d047b190dbab13e118f076855045706b92f88b445b62bf6cc880ab", + "timestamp": "2000-01-01T01:33:01Z", + "output_mr": "311bdb33db30c7be82b072dd47ad202d7aa557d6d20082a20dc652559c435bb8", + "range_proof_mr": "7bba8130bb9ce332c0839aec78f90995658c896fdec3664a83f1aef61de95722", + "kernel_mr": "135582b31aac2e8ecca5aff301f4c4b26033ab0a6427dfb18f571eaf258a5b91", + "total_kernel_offset": "a99237bc773d5670f917619a2e1a3547a20c08857f192a8d1e078b819c9e930c", + "pow": { + "work": 32 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "882010b75b9f2023560ad3638fd3e1180ffe13476e2e37f0cb9bdb7a6a4bc408" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ba20b0baf53f5bef2fb9c78c59a797efdd67535771a9f4fde723e5b1c4924513" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "eadcec559f12a212cf7dc70e5678e88112413327fbcbbd93c48fbd9f90443e13" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f225f8352ab7cce8c3224ae987560ff0806b493249dfad04998954fefd38ee59" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f6e77a241edd8a9baf8d14da1df8a13cdefa8369b9fe52d50b6ac120535dee50" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0a70e70adc7c5f6df3cbae97c341c6f45b2fbd3dca45c1b2947faf73d21f5b11", + "proof": "b2dc15442d285decd0aca497ba2231158a7fa181861cd87930e35de0e8990627fe0ee4e8cd8fa35c315129f1a5a7b01e3fc6dcf9127a0a16cbed9f579d162f01a265e0ff03c1a160c3d1832a51804ff9a31f5377e99025cd9e91cea64059fd19f47b0e51fff533aa9dff0baf1af9b74fb2a988322cd7bf750e11a53d08ea520bbb06e4ea2bf7105e96bab35ffd017295cba7958f309da1940100d4f5429f750a7530b2b7abb79a09d5a262d2e5cc19170c35e52c2cf0f4974e1551f34ba90a0c12ca5eff86938c22699914a2312319f939d9a41f490d8e89c8d99411899a0e0e5a3a15ce343f3a0c2207558fc0eec6330fc5f16698b81bbe7675d08147650674d2cd14786d3fc5cce34147831ad620298b84c3a4503f3f07003efd466a9aea5e7c1dbf02eb8cb3239bbc01c7c58705a52046afb9e9523997f9839167b9ac600380b6e7f9b74896d0212607830a4fc2c8a0126d4ffbc5fec7d9ddc471926762794afd909d52eb6f7bcddcc7b5673a9655e6fd2cac497bdab61ea8ae248c021e03d0423b68cbfbe0833e7c81c61327905d20d9729b008c6e5ddd90b4a7faae67569e078d1b6626834cd13ec29c1d8dc1d6ff0ae0bb8b3188c50573442b4e63166506f07f1a369cc42df6e5a40c2eb58d50fffbb944ea8c370e7cda533395caaa4876cca7644bef490b013d47b74e7d934c35a08b85ec8438f7452f73e319b53e75fcd0317d7d52ef30c957249e12fccbb074f4143c3c7e7c4764d8193f2972d730b489af7ea6f4bcb93f21ef1f51f5c9a6c7fb8fd66051af0b0dede19e980ba33748b8fafd2831de5c70197b8a4756712e7c75d3aa3371e72aa8a95bb5329b1233c32b0ff2257f0e6254d056e23cdc1fc2942c440350567c7ae47bd0dd4d70fb021518d9e0e0f71c22519e339e0f3f2607cadfa0f017db16669309d76207717909" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0e0632ee32bf50c2f915eeff20e98b085ab259066f83068cb9f0285a0582aa33", + "proof": "1cd80c784bc4b54a978131752f03b2f512b79ad0400b4fb498309ca679dc677d8ac99251018daa31ebca8936605f138b9f314c568a557c7141fc54aed6e8c850fc70916cba2c66850d3747e7c299c06901da3488f8895279701d828b8e281d6ff4119f8ec661640002cdfdcc29212f78dd4a34d10837ce3c7b8ce1a3af361d2e75845f254154ed374c953303a03414549df367ce303c23f6902877ee1d149c06a274ae2be7c4c489f2f47967a300ed732415de98ae047bf3139dad6c70b98c03719edbca11a04bba7705a3119ace451f8ab6f76a639e9845496c7cd6c9ce560c5227ba49e9463e5152501091391dfd741ec428f839dd5e3d448b8ba280264e6aba0ef8a30a7803ed51f63a3bf3c9649a0fa6148a115d9fe17a1a627f0127627d086f0d5bdef898b907dcc3bb70e6687faa1acc8175c414f83ab5cacf99e2096e0833cfbcda5f3ca2f46eb8c7075113ea1c5235ec42a2cbc73937c89d53c20825b8c07234f5d24b767b9b29dbc08e7a3a4416cb3420ba124709cade31c4119d0218178ba38a16728567ad69f9ce9e41feca217c8f35e59efeb111ad64d7a3e56f18fc8cac7be32c19e231344b4435e016d23a3a93a35a672720b35380d0769d7e42ee8c06cde707fb595968ed43050f4ab559378649145ab19a8c412a5fc6663ebad629fc3f9f94eaf764e80d25d6c1fcbe3226b8ea0091166fe5cc8e4d08d04010272cc4eac5960d69248d38e28c38cf141647804c8878508b15523662315a04d0bc5eface7a04486280508ecc07003cf691a0983476bdc6a718d26b32e2291c0a181ad7431c13a40aa7832ea92c0d8de2c3bdc1bb0d3a2403c8c73f87334275c950a4ab23e199feb1bc3195217aba316970534290b617548270c51eb874980b8a79b64d17348b302d94fd1c05e63d2386a17f33cc7607602d701a9731d4a707" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "128118f50dcd590c058a1f5d12bb504ebd0ffe206b3d244221b1c959aec42a66", + "proof": "8a6005ca26621f28ba7165932aaa469496a7042c79e650d178971e09f00932174a4b0d1ccd08e545bf1a39916e781bd97471d0415d3049b89a04ec4a3a35674d4e639ec018b2f878218360f2f561874a67e273797fa31505ec50bbfdc02ec16f6207704024ac62a39c6c1909c3eb0b514cb8078cc1d3224dbca8458ce6cdfe433c5ca240b836af409e48161d8a9d2864b11277488b3197f04f1933be03dbd70a6ccce47b65f18b47c4db4047fb28614640635d2d34a08d2dbecbb01282817d0d1a58bf83aa42f874d06ec36c1a7bcd5a223061a4f515c18de1c5e76023ccd30d3e4ab2492cda5155327b34fa30e90d3b2a7be5ed3bc6bd95d07b2a0ce5f12175d6fe7148c4341979c514fe374aa5a5f0bd36eb66fba76b480751c69329c43f27cc4cfadd9e6842cecd8ec6f2da616b18a67261f4c8d0e4bc2edb9c89a704924eda87d9ed47873b7ade6553f7becd2dedd9d02e648b01ed6d1fdcd78bb8a0ae0a18a513c3df501a732619b2f5dbeb569146668dd0c94a29679d49efdc23b7b95facad8042ecc7b6fd38f2002363a79aa69d578b4a87278e1022f2c7b82e85316ac4a639b1404e2c72f3b3496b464fbdf3f9717a604ece3c045c07e04c332ea11814bed77aa20d16282ee37e24336b650ac67b2ae8640c6836aca6e90a30d78e0cee971d90ff041e52be8d0b4904d124530f40b7594d736ad3b1184522b2140b394ccfed22628019605dd2f9ded3e80d6775230c51ef8848586b8060ac620cbc6720eb47e263340c1af1a91292e294cd2fe1c231813503977135a5b8e802e7f83cecce24371a45673ff714f3a52bee4244862526a4391d8a5c5737e27f6a17322dc87bcd222e13b34b122c466a3bf4c0e8b0bc0f08aa33b4dc9da1cba0dc09d40b8e9d005c5261c654edccc33602de2f1c7e8406057142aa6796e129f8f08a7506" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1abae0bcfc365d9548aaa1ec96e8f7daf2ff59450b3e8e369e64a4e32bcba741", + "proof": "c06944147ab08aeeefbfbb1372b0c692acd77796fb6aed82010c81a40177dd7faa00db76f7526d5428f0134fbaae8118c51357359e495e5a04af1d5b9314db384c356f97d0a4f15058ca87f1b4d101632205c69eebab09cd46a62f3a482bd96ed0639e86daf0d0b2f6ae640dc47d3d0324143d085c01e83b0adf48e9f07dc82f6783cc7c41d9ddc46abaff4395a2647da6e545ed0f9554d8e37207369e42ca090713acf5d02f05b5580717649d187f20154d51e7e7f70e2c7d32985617711b0f1ec5bb0f4c22f4e90f560e8b724d631870000782749e25b0ce3abdfd332d280b96f75ada7438850bf4840e5508ca9f41f88248dda13296859061bc70cbc7a7500414b169b70e12e9a65056307b750328ed3562f02352feadd54ace602791813334250baaece3057ffa06e771fdecb648dfbd15654fc37a4b5c0539a7798cec299aa147676bc9da4395f418a32e58dfc6bbfd053ee115a7b0c62141891f9df76818c8e452f3bdc601c141807c6f901ae28a546a86bf80d149352fe15874f9c25c12413bc0ec764deac36bcc5394133e4f5fe0fd644531b8a5dc9182d29a3b832cac8c25ebb427088e4ab06ce3bf7f4c4be06bdca38a6cb16ec60fd198ff3844242eaae7124f9f881d0944e9e62b1c17c817169368398c4a6bedcaaf9f2270935d6adcd992c5dbd401f1a5a0ca10cc2c3e98d7f37a1be83f63c63cbdbeda41bf1d8a2b9afc820383163cd741c7b94968ccdee669e0da19ad07c83f6dc046e2002f3eb598ec04692183e7c214578fe6b65b4430b542f0fd820884953f31e98e933b744f0705d6da9af361562be1354889d813f43d99554496acfbdaa764aac2ab64ec37fa27c095efd8bf8cbce326c9fdd481c14fbd853509bc0b405fa86bcac3061836fb2508c3043e78d828c587d7d18d804907dd399edfe25c9bca0259e33f02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1c391011ba99d136c9109d57860a924d2d6b6b027fc8ce27b1f4fab7054be137", + "proof": "4e5077af03df9782ba779b27b02545f545b31cd9e4cc20615905d776f4eb756e6a131c3bcd278d2f805c18303a43b22e19d129c2d6a3bf5859b72ec6137e3f39ee6880e1c97b0332c7d6073fb155ed1a82e9831dce3352b8cd502c18e3570a5ca44ecb4c149917b59abb459ad4fc3115910c5fdec40c681a97590994b1b6ce6ae730698bdfeec5eb7c7dbe33a3e711e9ea6e0dd8de17c2e8276c415ec9bae10b705d9e4f892f3a2625a801b81c269546299e03b746f7f2bf8a82afdb5e1038059bdf9f6072bfd57242980f9be3ff187bb60168538c83bcefec0f62139e57430e5ea7f3cf910ad5b38b70901d7da06727813a71b3f183ccccb6a773da247008387e9e39a98062f3dd9ab366dc8b3d2ad23fee55ad924420c053104c74ff386b3222c07f0827c7fef36b3e4004f1e3778f538ad35b87550df2bac60378dc6f9501401ec506ef5b30947ece8c91c3c45c99e7ec2b0629468862d303408fed0fb240b0a68a0754ff76639d2b616e78f8970cd9525ef6ed3d519e66371f69ab179b00e42a41108e18ce7bc6685dcf98261a8b65461fd8f264ffc17848b220bff22c3e0053c2e72ae7841d650b0e29a64d33ab8efe9fa1a6f7aa57c41309b2eea74a15e076ba0d1796d7f013427905f99699ef3425000255716c48cc8bbf5590391d1b72ac27dfa1d8d78c38604031f428d755b0dc6c0c27142b71c4a29986a9f0633e1e266fada644461ae0e4b0894501cf1862ab8246e0c7d1f0f56da9fb342c4b55f89a7603493e9285ecaafc3142201c3b5fd9c73f82a2314c0806630801c6e00cf0dd7075ea7ce719f6cd317cb32743e49589b9bf2ebdb59c3c0aeba3eb7658526ae833cd3bd779e393e2b3de9de2d2c5683b3590f091dc794180182be36c70062e5ab244c9b1e637c22a3c682aad4534c8af17d0b08f37cbf09b500f4d8ecc06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3e4c223c98dcd10b4785dd20519950a6d4f104c38f6d310ef13bec98183c5941", + "proof": "ee6b1a14907fd5ed9f8aef80a8f50e011d5615de066c9f32b03344092865fc22febd4581a67f1a4d164227c765f2cb68fd36e19d1ef1cf8ab783d403915bc00b64ad1d3b6323cd36113abccea96eb733205aa81a678fa71b862e9ed440bc615c646100ef46e59870d8f9950328bdaddb055e72aa9875cc3d6b92597912d51104536b4c7816032bf0115587f3d552464874e9b6dfb3890cf8bcdc916aba5636062f455f89014f414fcc85bed472d3f6885cb438ca09d6a141fabc861f570d540e93e44895a34a4d453fe7348a9f1f590147ec5f614770b6c15fa4a50aa0a2ec0b3e7c437a6edb9711a7da2342ed26a4b805165a0935921a43163359edf429637b624fc48dae55b59ccdf9f53784753a7d87e6cd497e8079fdd8e227d8ac09d42c8cede88c0ab8a2bf16cdea79f983b0d085b4efd02cd612fb6c5b56fb7953ac2c4206b79393b9440b70d8dcc28c12a4e7a23e41f47776724a0538be68c8cd4635f64a8b28d61dabc065a4a54803fe12752aa42c3be650f5874916adb04457745d94aec4e2758f0aba287ff39dd206730ae82fcae64044469df757e869f54a2f171c75f88861631f286fe72e774072cc1a9c7d81990e7758246b47ad67c24c276b40a334437fab3acdf73bd1ff1cd3e969fd386a26fc89a50d98ad4bdce6d4466d00a3715ab1213dfccd7c94830f6e70bd74eef3ea72d084597878c10e627cad63beffba7d72de0f6d6840e5c48df1cf22b271e275e8ad2fb0edd096dbe5439b74dc2250f1c81fd316903d8090ee6ae4af8cb9655589090aa5c668989b0789330f46f8e3409cc9c546eeab366dccb2acd4e4ca2c0fc4ad690d167e9c5c0c1ce90af504803b081a317c5b1af3fb9369926f33e00e1c08d1b73b6f1d513ad0c8570df34992ec3e0bb29ca35c4b68290fbbbd7b9faafca826d62e382114c6a96f2d0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "42f82f7ce2e251b6715407502602fa45277ef256340170c85f1103db23d35013", + "proof": "d46ae895adadf6f7b0fd5c40c6ceb76ea5ac7d59d73a433ec60b4a5cddecf0329eedd4ab57a08b515a3613d3a0ecec1cabee9744092377dcd815f3cafb14162380b1f385fe594cbd5e2cb90ee514f5ff00bac7b54aeca316cc6392868ce32614244b6f69be90e0738cd2c1d05a0b5086b81f6b46a98e249f2032c2afa0c2b958bb15eb605f7983ef67ef94e3b08aedb88c4180764f7f5bd4d359886bba1dd60541066e6e10f10015b9e4011d75c4a625a1ecfa8b21bcc6e3c1f72a4c83d379095fd3428786c3b75bff8af224e99bb72de46bf39ff77c935efaeeb426f0405707f4bd8a593b86ca640163934d307d2182aecea861826f2eda54d2fb0eeec50c18be6ea02e93d5976d21b9367a9a36dd7130f2dff800de774cc3fe800fecb81775a01a497222b44029dae63c9ccdf62e181278c95b05c78b3f113506144ddee87e6c04ac6cecf4444deb3933cc1be22f03426fcf806bc33d7f685cb21cb174fe12f6527c77f42c558ba741fac68a62ffaa58d62202973267360d850b836da2380584a981b119ffa695f91090b7103668e277ab622b3ef7dc2bc1d424dc5b91f46ffc902bd7109c233dca5da4113bc94ef5e44d5348714322afdb93decb15daef49f0e9a550e0af6d4190747bd2173bf94a0b5916cbf561ef3aafac4c6b5354ea04c05c1a87973f4a082653d3e1d9192720b6be24bc3eb59ef6cccac1084c37415e9ac71ab054799734c78d1dd361fdb601858f9001ce08bc6f0508ff372016824d982feb2efbc0adbf4607eb7486b0c3a2f8256163f68c9ad6973f31d54a9a425b72b9d0bb6bbd98cb9311d5efb910aeacdde66bd56336dc8e36d3a4c39071f557e0208d36609e028dbf4ce0d0bd5d6cc81e4c52487fa76f821d6bc42d7ec1cd0f02f3957a2e13bdf2360d7f2f20219f031b5ef8d4abb4db5b121da9977193820b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7232d05f5b32dea00d78c178b9f930aea5c0ffb9c71b3f3552f87ea4d0615a75", + "proof": "9005a08eead3327d7c5e03492b23b2b18e7c528b6fe7a762099b6fa053bb971416afc43aa96170c2469be1ef308863a1d66f229f59ba9b7ff29f1500a133766e5eaa183a9fb63c698d5a2851e37462b50ab49bb130c65622c16be462e6e39168da4a41c951ccfb8778fedd342a0c0130c264d3e7d7c7b7d7fbbeab1e0537c02e65b0a8447c367f884586d6e3f0779fe8b566eb6329833ac874957d5d4310cc0c91831d6a04d11b900dab88750edccfc4c72053a55b2ff6c76cb1144bdcf6720160ba4831485bc36932cc5472afa35bba2107fee5b180e1ac2b8b54a4468fa40a5a86f59f4d12ac8614e9490d55ab6f58b2733115fe84960ddc8dca13f4e9475e2a4bb613744a9ddebedea00dfe39792965d33b79496fe89d6f159d8ec54d591382836d14d4816204097e53f19accc1325d39086a8cc14ce62121ae924e8364795ae2e63534e6c50a666cb1f1405ffcbdd1b24d5000feac3aa5df3cabcd18aa45cab0e1a9a9092990e0a797307545a00eeda0073d4f990ec74933c6111defcb39f64677c3bc1b793201a3729ab457cc76f02da35fb9d893229f3ff4f634792443da08a91404786ccf33537ccfb713facd977a1eed293874e3da50250abbfed174e8d4f47cea786611fd47319190af397fe9f5d028a7e129e0d186708ab7bff51bd667f36d8628c9df1e0929e7f46da13973888a8cbe41e5c68e3dd526148d71541ecaabeca39c666c3660294579557bbdd2e1407d51ae5d286bcff610243f0e784a65afaa6cdbcaa313c08f5fb8946796e8878eb298ddfdcdfc3ec3965fcbd724963be78ac3db6cf19873fabeab044256837769d5189257e599e0cff239416932f109decc6d5b6f26e88f57928d462e242dcb5eabd537a405d92e4a22fbbaea0f011379082f79ee27d8ec5a8f893548fd3e7a1a517864cda3af69cd580ca02605" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9487e077acf794740373dc82cbd7ead91f9ece275c1dc8b4f014de712852af46", + "proof": "24d69f37c129edbf8cae38ad50a7d2219022e6b6660b062783d2896a3b8d077bf8344130f52d3f8f99dce9655de077cba726d653451990325c2a648e559920519eacba9884defc1afbc75a93bc0d474ab57f66be4242770e582ed5c9bb0f1f41088dafd14b59cff868ff52ed5560c5ec46b68bb2f218d94c8fa84c191c312c2915cf04dc122b14b69700f48468cd78b6039ff0161ebba949c4c5c0f328b8210107a321e4a2c31f57f9b3cd3df0eebb0b444421b8fa7d8eb21f15a0606b6ba60cd002e71f93d870c6416159e21a888247ccc90f8ac50e96268c962e1c65b5ad055e6f612f82e99595d2e23f5bdee726734b204d8de647fa3fada13aded8c50a04107bdd486c96d03d1782634db4bd2251e62e4282fa9db084fd49037306f8a1425e119f454c618c96ffb07847a87e4b3cdded27bdb3bd55a769d34a3a5a786c5228960e5d9a5cf0487677d59896ca2e81b3bd209c2300d65923ad37f2e34f0808da86f0b6bbf1951a4c13f9a355e06352067ab8b82af6d35330054b98e80ab320fc12e74a90f7ed9acdd569ad24086039054849d32deb57626e7656c703aabe29c8c7a9636058117431ae4fabdd4e352acefd5dee5bc7c4ffd9059381a200166f10fd2f4b341c81bace9bb016e2ebc1b0a44b421947d28de943b77a8c678a9c46221a0d117f13d4778424adf38779400d18020f90ef76f866b9e2b0349e47bc47fecc9a87e4207124d7ac19d9f5e1af566b3e443600d046a546f5ee40102fc444e8d59e917fb114b4e1a65f15ab3cccd1c9d09e91253f7bbf549376c5c2fab51046467faa6d9e8d61cc179a78646677437ac57295e10a935de18877802184490e308acfbab062b995ad6960a4714adc638344b1ec3f34ab5c3d6499601c384c09d927c4c9be93434546c3a9f57afafdfddc9eda5de2fc7a13b16f9caaf2dbff0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ca161d04ccb54cbedd7fe32f7371b7f625ef1617809a7597dc39371e999ff20d", + "proof": "b209b48a3b69367a9a2342fca2c5874a9bfc4fc3815a99ad861dae7605350c007850e7730f323764063013a6a71da0e32c244fa016f482994a0da3f003f9d9151e28a202c9422bd0799f02efae159e9e1879588ddf9fc16ded11a3675d53613796e9fb0199fc05c5bd04fa126690fbfabdd8484b62b6a0c6a24f4d923a9cb77f63304f483d052fef615e4619e585bb8578378c2055081132254f522f4261b0053f2ad9894944cc805b747d815b0bdb63839b3b49909da4ec1f9047ec90ebee07d54824080e4826e7741638517760530f74b79d7b8eb7c1d4f0687dc64d0d9d0b9860ac41728045928bd03fb61819e58a337825cc41e051e46f0e079a8daf6a790ae7241008475cda83d6c5daad3f6f2537bea912b535b099c1924262338bb23f0e9ade6ea5343a7fe80aafe3899bf187c818b5afd81c0761024dac4f9a01384f1e9016e0af1314552712c473ec606a74f07cab2b978de51d333e76e05b2781087cebcc871ceb1f2b56e74fdae5abf0016db7096a9d6006f59d6494f80b9e3f4abc651e9c70a708275e6cce88d5493e374757bc4c5dd977efde3bfb1a2cb6957f6c6a3255d2e50544adfe257ff72fa52573c3c788497fc601c4732191e3f29a2ef0aac95bcd374aebdf8610a234c3948d31802fa556fb7bea010104cef5faa35c6e10f8e94a516e8e827ff3fe9d8a9ce28a0cca4e5838b7ff8a40ffd619ecbd547416e32c104850efd0d87928ff51b72c7c47c1be755498119c324c3fa27b824b12ba419a960db35694ebcf6dbecced7132572a92372933f3fdfe38bef060ed7488a520fe5bd584b37765b671742d43028016c378389751626b34b3cfce57e15782b5310d5c474a7c20401c125456bb8d75b8c48f5bfb6418182fee49e5cff9042b44191ae2b5a4b8770c9ccf4eacea6159635331e2d1262b2cc88e1577717e0c" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 33 + }, + "commitment": "e015cfe0349bd99aa245cc523001ea943665aeffdb90743b3cf52d83866a8243", + "proof": "b4aa65320f1bb25d0333c0219538bd0b21f6abd8932ce75602977d115a53dd26ca4bea79f3a62cdd1614ae77832539897fdb8658056a1657e96908b6054a0e66e23a2f4583bbbc40bb7cd25f9468f38f8e9ec70bce0758d663a9b50d5ef8b4182cfe3935466209354f99063ea406140f49cf2692e17352667efbc4674ffbd237fffe3530e4e517b112097afa620e6be13da538fb173e69d921fdd55f506eef0c24383f3b94171dd8958a1bd5a55452f069ede471edf6cae9311af3ebdb12b305c6fca239341a6c0a5268196a1c5bbddea7cc2e3fd98d13f4f6ff10b6272c3807b4b76e7bc7e3ceadea12f000409e146a00a51f4a2133879fa92f6ce752544739947a41b05b197dc783bfa3cd2905c175b0a818bb31dabedfb6506a91df020b7826ff71788fdaf347254ebf2d98c51d9854560beb25072b84c4b28870e937cc3fe21039af925a482ca7d7b9e8f41a20b835a18dbfca9117838d6ed595edf37225ae8c6ec9ea10d4d4004787fe8782ad7fd8baa31c1da3a9becaea4e5b02061753125a52c395bac6736d04539278c50fc4980118c8385954420e28ec60f4fb6967dc2108b250ed48abff045d71b481a3f24a219d510d687bc88fbad22cb4bb181ba0d76dd5cd26bc5ac8aae8365be31ccc5a6a2d70c5eb673892eef13df43e0201ca3267daed9c1df277304d567b9ce521cdb57904616383d39567c1a74df8c34990a33fa830c97eab08b50eca7c59e6086e2a9b318bbf9d3b66c638e6dced6a6e364a091c24286d39f8081028d1b2778f02f0189d8cd96c23c83437ac98fcb43c06cabae73a3601ec9e718dd90485963ffba9436058951e9d548aacf69effdb11baa8210c80b24e1bed18827be7b5cc27e32897a10c9413d5682050554abbe90a2f6967b3f0747e5d4127ae17ef1cdb55ca14a75621392124f20d8030dd155204" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "14b8639e8ba33445c4c5e93aa5be0acd04cfcdc5b64b260a73a1a6a9d1f0ae3c", + "excess_sig": { + "public_nonce": "f4ab1d3173b2cea62bc69aceab9f7ba9920e8464ef5f2f05c6148fa0df66766e", + "signature": "0dc12046a94e6ccb7197beee021f168027e1a89de7c81b760dc830aab4c93e00" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "207dc51d9f547aa92b4af2667127a6215409476b879b5298a8d5db229a043153", + "excess_sig": { + "public_nonce": "28e447c1fe4ae64d97e2a8a7c97d6503a1fc3ac5e75afc14c01309d0c0c4416c", + "signature": "f134dde4b21b92c841ae2af060a590417051b6fd1d920eea34e2f4d7a546a00a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7a7f5793d9d69e95fefdfa7c34ba603adb6a5c1681fe1a49ebf97ac9909ba92d", + "excess_sig": { + "public_nonce": "0e6070c7a204a69e2438b0345faffa915da979c03f64ca5205d6c1c5867a0e38", + "signature": "cfed813609b317afd3ba00fa1c1b8bcbcee9ed5199f150899cad3d22b1ed310a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "d07892052dafd91ed779a0474153af8354a8c003a7ec0010e86f75b3bb61e749", + "excess_sig": { + "public_nonce": "e48eb09c661d70a7ab452f8bf4472249d1a72eccfb213a48cb333a54f906e955", + "signature": "000ca638f2832ce9a909db19f2c9058525e859d89b486576ee47f322a4ff0504" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "deea8764437caa680c5ecae3b81556e8a28b3180cd98b40049ca14f82410fc6e", + "excess_sig": { + "public_nonce": "b094b03bd488abbd28bdeace9faed0fe83e873b73de17de6b59c1d1e6dddba74", + "signature": "e3e69924614dab9d65b7cddf412a98e93b70aeb59a30e05e037132b1db701903" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "888c1b240a7cadc41e0b8e299694a9f7a0ea006bea49a8fbe89eea983b2cc117", + "excess_sig": { + "public_nonce": "14085d4d3eae72c4fbf42b772de2830ea6f5019a4d02c98be90cd7081f6a2f03", + "signature": "419a4e9c4d30a58d33d7ecb26d7f00d2d85e13547a77736bea53e2485b895905" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 33, + "prev_hash": "24c045c50aab1300f641f0e6ddc5a0d07c875984d1276c58e4c044c512c69571", + "timestamp": "2000-01-01T01:34:01Z", + "output_mr": "b6afd1752c2650ea30876857c8b8a55ec0fe6e47d01b40c009cafa095fbdcac6", + "range_proof_mr": "9f73c18955561baf2a32425d686663dbbc9dd15a04ef90c462a86ceb177efcdd", + "kernel_mr": "bc2afa8a85ace087e5de28d21012fe5f65081f783840de1c6b409d9cff4e2ef4", + "total_kernel_offset": "a851b715a8bdd7ffa707638eef854cbf57d55b27367e7fb9c076bff3681ec008", + "pow": { + "work": 33 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1c391011ba99d136c9109d57860a924d2d6b6b027fc8ce27b1f4fab7054be137" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3e4c223c98dcd10b4785dd20519950a6d4f104c38f6d310ef13bec98183c5941" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "42f82f7ce2e251b6715407502602fa45277ef256340170c85f1103db23d35013" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ca161d04ccb54cbedd7fe32f7371b7f625ef1617809a7597dc39371e999ff20d" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 17 + }, + "commitment": "62fd4d95c4bed3b7c245e4bd25d243fba909551253245886172fc2ada34d6f38" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "04519ead0863cd7d80639b5da9a94f0b18ab841a39aef176834f5a01e160fa4b", + "proof": "2ef103227bdd6c7675a8256beef2f434e428ec3ec69265eb55cd9758fd55313220454378fb305fb18edef8ed250fa4bffb84b06edeca2f85bdd34d24d136cf7110b55606c7a216369b85080d2fa0a5727d167ae8277ad9711dd47ac5bdcd195702b4fbab2c8234cad9d75e35d007f382a540359d2e4f18bbc5eb13045b183d56a22dd1c7d3038907581b96e71f9bba602a9531636cf494cce1227d884254590e9377756964b87b2426f86c398dca46cbc95d0b3a0e5a6e7a58b2765afae21d0aebae49785743b98640ccb706d33cfae85ec3d90a1912424e3ae4f76bc954cf08924e510e35ddc5433de2f5ef0d6727766e5b14c990b75c62ad6e2cafad0b0744a6b8ce59dfd820ff525ffe7b2f059e2e204b20f6f8f72ca875fc5f47b4b5fd0db68f4a85fe3b542c8d18c8decdfabb08f12887e7b7cef46ad666699525dc8b07f86931b0a4b430ece52ae00a5e69b8b810a4e6283eda67defb2102bd7afc7631a4b6fb2afdfb9d5ac6cb2e76a831da548225067b4c70d62f3a864bb239e33e590c3e531000854afb8ad2fb86c5058cfc90b4cf36aae77a34d342f6a2da64cb7724421f23d5f199c068e8edf78657e3c2d0c9dbc3d6f4d624356163f85a9f2776be762642d948f89c93510005b3b217b875e07c98f5ee554b39f112fd732b106b5ab7b02217100cf67d11e41b3f813b3792f20381c1684fae9032c6dba85ca2452829305f9a5d7c2ffb95f95afe4faa91a1dd941dde30c217035f5518cc65dc4a4eeeb07023033ead9efd31dddd9a92298992e8629a3f7159dc041c9d0051a65408cceefd69312b45ac238ce5fdc60a5a9985c8fae7db2e186ce44d1e0b3ff52e2549705ca6d56134d592e8232b19c6a61948a72821dbc758a8b893a2c188c70e1431d10f8008e66be9eaa960136d54ab63d07bfafc584b31e0dfce37b4f4e10b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "32e0deca835dac1faf0b9543f82706dcd2beee1d322208a0d36d3cf434aadd2d", + "proof": "4e95a08268d8ffaedf76d483f7beaeb91162b5d36b15bddcf8363e4bf2fcfa0de2507d1c751531654e02db57439ee228691f59ac98a325a0dedadbabb34ab706bcefa454edffd8948836c99c594c33d4214db7958a4be8743abaf342c503ab3b4ed2157ba7b0ffc255516f2c8f5eec8932288e46428879ec0b51faa65b7b9b125f0b6f2acaa7477d32b3275c9bd594a611ab2054d93e15627c29d8f307c36b0fa4d56cfd92435c3a2f5df137245417ab8f1d67cec242a310950050f893dacf06452f7f6a18c7412c06c6a560d6a4c5eab06ffd6a14089cfeac4927491862190dba44d4fb830c46d7b3d3613a85875e17490b47b22ff26f52f60f40ce835cb75d4ab382841df3839461eb657e360061f366217b1301d0026438a46229772e8b7f6044fc2ced080fca039d02214e725419f6cfe8c7a87694e3fb02abacdfaf7f5ed268f4c386ba8279d206f3a4e142e4898d07dbc382b5895881616a80cc904264d84bc3f88e33549c8558bcc45c5673c97297f4236f994c2305ac022ff825246de6a5eed00d22031aea9f9a96308fd3284cd70fc90d0459e7fc529f67acd1bc085812f99e75ae1fde47bb2a4facf062de6e400cc31feb66c484529915bde2a606be11717c333b2d86bd874837d79acdc797561f32acab5d003e6508701d5d21150460e53ab57ab577d563f3359f86259b9b4abf6c26e52499dc9ef115b2fba22a3616b0df01f63577915c7ca3acbda83f32e3c11df8e1b9cfb7d658635d4cec508ae9e29e6ba3847353fd71b7b1204584edd6fde75a2b0a629a48d5ccf9dedd28de045eed6b3d1dae7b533cf294a74c1073bf00a3b4fda8c93c6961e00ca747654cab6cdb31009436cff4db91b04122cff1c3355fb95df014fa2d5c270d3c430bf61d4fc881cefb791c8481401b68dc4fa25c232fbed01fb11d3a123089f54809" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3c9de4b403e4c6414217fb0469cb19bca14d81e76828e0e85448827f8f3d5952", + "proof": "2077eb9168dd09950aec2a2b70f2ff85d4b3b4099c58359c716a32341814fc0fba755a53542106150a09f5e0ceaf3aa1c77776c0504c875a8936e671d0a14176d05fea36a131ab0f1c1c976a347ebbc5d40c3b568838f90cc18f7d95f7ca3140da3eb82120dd5b3694183788cd854ace947da472e92a42d104a3930420871e41fc535737130c67b9d5dc983417931e71b39475b31d20f1172c68d4e029e709088ff783dda0600260867e7551dd1bba487e1c4c9dffd28b5e8c79f27ac3b50500fcde9849114b1b0b906ff3d147ab71ba850190dbc6aaf1bd909ac6c3b6d7980050e8ec06c91f0f31f260650443ec7ccb9b7fb5a4b096140fa76d55fd21f804786099aa734411e71a017ae1c925cd047bc4c5df945c40b9f876d6d042010c8c47261ddcf1a79da69feb9a0d83f000c25383b33e1e38d3a4a9c4c2e5dd8230207b5e581dfb6502995d0c077c018ff7119ed15a299508bd6d89bc66d5358996f852987b1490a94fc725a13a9a0bd1899657b02f59e876ec9d74cbd6db1309e96b3446ec2920a02eb27b26849c20d08cec42d786c77ce47eb747d6e4b01d187cbd46b8ee98bbb83b19c85f8a293e4e3f51bfb28b58dde9db7c7ed23c9e36ac35721f569e64ac5bbf60a12b59ced6f1fbe5ddee84ff689b3349e8ff3026b118fc3049d24899120a2e329d297042cbfbd99b0c45a55d99a490b6c726d5ac4eb539082bd8e4a253069112613e219a70036653bec5fbcc477b3652d840f1ed95dd26256ba4a68f7c88992b615bd64eb033f8dec264038ec18f0ea0afbb97179cd61ac04eaa8c9482ef7012b95c291464646799ba82ce5c6877361a1263cca052073d6829e2a4004b1d3b0b4cb818fcb300b41cb656e35826649a6fd46aee2d68d8c862025fde2c9401360db095b2616e5682469cd845305103c9125ab065c2e8c2f39e03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9230e94b4f58ebcc691a46f043bc5f2d292b2647a927f2fdccdd550ac744a16e", + "proof": "269aad69e43aace9ab98f84cacad86b3924237c070e5b214169255d42373492428adea1651653e0f1024962fd313a9ad618fc2bb2bdfdd2c5b080fabba053d3dae842787d8d52411e7f4fb837703fe2c99bf92d3a363f140aee9694cedee1122e2fa263838e68284c4c1d1275b7780cdc56916b4071d0ca312ba1c3ffc90c11b5c3a92cc9721cc5f5eecb2e021f987f56472952670baecf2f2bd6e9681d5420219ed92c59c6b7f2b88e305fcbb653efdf7a2a6ba061d3741be795cef1ed9e70e8c68fa97c6e8aca663eedfc0bae995852f73dfeb22182188ac7193955a9d8a0896bb67533c328166b6e355874fd10ceae3dc1055a18be8e28f063bb358e97a5726aeb89faf88690734c9ee69bfbfeacbdd3e1e38d38a420a71e2ea07a977731f7ecf7a6f03fd63113ab55b6a59a0316cbed662384f5d9236cec07304379e9e602ae1787f3b095cd1ebd80e04845f1797f1365496716ab98b77db663855a3d06b7c6089c3d65622f517c45073833ace94ed547560e594f8d6ecf9bcc0de8faf5b34fff4f57ecf6c1481abe4a422563618f1da744f9a32e11f8644631b8f85ec3c98bdaf87fe0557f1220cac8f42cc14bd0e21b446cf9c1a087e3cb6c24b167f002ef0746c168eaf9793b90a270da1ead58491f19b1ed36a0847b8a1f56b820e225c49d541172fc94e63f03a097ddd6b0cf8eb43683cb213d073bd1e3f4c3f0a581645dbd85d3afdb32c7d91c0789b05437ce866699d15267d6461326068e00c15b6c2aa17303cbacd2d35d69c859b2d5295ac1d712ea2a720fb771ae25a4e7329969760d45dbd3e632bd0f8a11549dc5188fd21ce42046a60adc50bf5f8007627703e9bcfda31a47b96e5f95d1e008fb63e102115661d13326cdc24b846fee904531db89fe1b6178b7a0255bf2982c7f32a066182aed4e9bb2d9ca6fb51e51807" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a0ba43e4d84dddca0844f77363f7ce80f13959c4c77251133e221b5b91d49f28", + "proof": "56991c6b6e0e4bdefaf299a595e136ba4f825b8f5ffa608896b99c939ac7887fd467a522df8c8d2ae4b24c9140b678e5304a52ee7ef9eb008a6c75ad4671413e0eb1f79a4baf3b4005028d06c473b80e97abf660efa078cc21c7dc6a7745ab00980be6033f15e8214b1235f179c77b4f694f1d7acee8c24eb34408c31faf2343d7a7401128e08dfdddd65bf537c90424ae09ca096121137851673bb842a2850c6ea91d74764c4419041f37a0b633312ebee780c519ba13706a57774de2b7dd06653ea6da4cae2c35670a3a5848a9e90f3f45dd01d9746990f166c6881977a803706b46b2e602b3c26ab50ab38893f8c526ab5aabab27f5505c5bf6e56ce080068294b2c0ffa567101385142273aee2fdeb45edddabd72fc137cefd1a1df39f10b2c334b6bfbcf39921d38d29afce92cf5c0d548f7f629acf7c1b3ec1dad47652d6678df51a72fe2d28d85b5ba2fa3b5792c1326b8a36946488bae957d8bc5430e60ad83a9af059dc0a0a11e6b5f56efe32fa2a8cee8c2848524de59eee672110daef50a3a721424f6e8f315b1ff81ca85bb56e1aba51aaddf77d608b3e3a3e6a78648d7e6119824a207254546c2093c8e9c716bd713eb09162bd4fbda56188632cf534f25b7715ed6e2f749df234afbc9640ce0e772d811af4c1d908dc133b28d86c004c088784c34e72df84208de38261c3cc63b5169f91ef62accb46be9c555c146466c714273c7921e307c2c0ff89c6bc2a6fbf39a4b57adefba0fcdcc3325a093f306c38df965b41893c8b9d04b6b46f06d440bccf1a6f239b440e405e663a7929e749cd031d542b04074b24940afad5ffa411045b781ecfaa5766e55f4b9b8ff5db644d782bc3a0afb538cf2c8480379690cd1bd0ccc1fe6f27e40cd10b78e5a88dce2f807fe7fc8492994b4728b410a9d4526714950d6d86a59657250b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a63c748ea34c75817e5ec603f2a3e200557256b4bf91b0c3cc977956c2fefc48", + "proof": "b431699e8ba4d93f03e119fd996a138a6c9e9b16e7ea370b9c4f95976094244d0090b30f16e121dd3a24cfc8213ecc55d85618b6d29f7870c5c5a461325b5b58ba3de58b6b6a3056d773f32f8802c6e5efab33a91998f993cee3b5bde28000229aa1fd3db245bdcc756f8eca715bc35253c431496544a4dedba821cfd48ab10d3ec8d49ec6519e9a4416f7453a3e1bcb6be847ceb901d82b97751365b6105f00b1e964731632606f67d646e2bf6458ff9082f23712ed09012b042413fc920b0ce9c93759c8933e4423e6b956fbcb6848a021ad9cfe9176c32652bd5372cb65007cc0fcd94fde55eaa5fcd8a10a807bd073ef6a06cc4a0143bfff834b8b8adc32587b75f509a09fde9aa36964ef04eb6367125d6c5c82e88f909847fccf501849e4252a9685224b81dbdf3abe0acc64a3bc78925b7356ba5b71dc381b9191673cfcdc985dcc0a10da6141fbad29f09a478c99a65f4796942a6e0b7786c4787b3292ddc83f1cb79c96d8608974a792bb0c072168e322ea94685fd32491416bc877e895d345f70f0e1bebd83667db3b55707b0b47ee2a11dcb72babc1ec7c158374ce39f969c402a8356af81744a39bc60628a32584d16fec22fcf029869ac49e5e56e791e1dc5e69dec5e115b22afdbf94c6275a15bbd8a8932306e9680f934f16ca735454c4de539f83c81a15a88e15e81d52186f2d235f0c4802b4f90ff0047cbe811057d30ff85a2c710de9d2fbee6a632436b4713b519d6e7ed5d2253e432e60bf2abb707137dde6e6852ee4f0a7940f3cdaead2eafaee4498d8c73e060f562294f036e00865ad856c6a692298040095222b9aa713fd1c95328f60720a33373cdf5c5c146311c66f2a847e3e2250320198e3d4765dfb6d7cdbde0d1d402c09b3571e0c4936ab1c064d44e7d0a9627eda3f0d3829678777c97643ed7cf3200c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "aa45ab2aef06070431a184b586b9f74e56924ecc926694c8296c0b74fc30ec4e", + "proof": "70a4c735e156beff316a68a28742e5ccf03b3d9769998bc6519c91ec226e4a50a2031707e67a260a8d3c155691ad97e383b48bb954b074d16378a1829cf483608876e8e20ba8f5054542aba940a8ae7bb411a84ed647a7881b90fe1fb451da54e8451ef1642bb4e38672dbca544990befbed9b6863eade58d07cf1081af6526e6d542181602334d54e975b867d0decd9c8b27104cbc822231ed52c1995b79b0785dfcae78b85793036410d3dfe110cc0c468c3299746ef090582bd1329a1c306aa3be165794e6b90cbc427c085b2ed10e30c08ceb8b5938399337e29418a4509400395c59ea1c09648d0b49a852b64c47801e09bb334eaade5707be897759c30d0414f1fb87f21bee98ab3c793e3d283073b69f8bdad1607e52bf060a0a5d257b0536969d7771995890e22cd08d72eef446417ef0292264db01e454f2ddd20280c329cf0148a9e2ccf4dd50747229b6ef06ab736b61aec7f2933f9ff6c79f52aca99a2c8ee9ccccfb950aec732eca76389fe4d300a4364ddda951cd18e55e8057ee1953fabf47746140db8076fd05c681d403792ee3a668e7d964fcd2a185f42e08cb778f954c1ee4531a68a0758d37fe73c09a0410898a64cf0abcb2fa41a206a8988201836ad63bb0b243c0de82e35b0f86477b5a9ef5d49afa44505cdf43d20b34dac8fad69efa23bd2953d9d1299a4371e08e31bb2f2258864cf78d09f6ff2349ee861d2a340ae5a13b3efbd7ee984960b7b79cf480631a71f6d9bd6eb33328313423c49413f4d1cb4aab670416e94d088b22486314ab90f2ab39f390170e0bbcf5aa8664b8a41f7e570174102c8434ae6539bf678a366f0fd420167f8072a1d62005d07761f1530a908d807d51fe6c84ef8c5cf61d9c2befb715514d60b3577f61bd76b7e07df7958fc8aadd5220abf8816109dc59cc83cc660ce7b5601" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b2bcda3bcc48ac180c7afc06ac0aa1da03602d4188a4a3abf2c95252a0e5b061", + "proof": "065cba967f33c44b43c5386026bf1a37531bc4af1872975f32fb1d34639f8c37c803bf5787ef2605ec6e972a441827772bdb45f78c18e302e5402c6225c0b836c4dc4faea848e5d00096cd9e908de2ae6a8c84d4fad0c2d4ef4be4a390f6090b6ca79ff6c7cf8029c4ef10765c11b84d8a5f7d039057dbc618f53c2da9d7321d884442f1f7fc88adf4c7736aaff18c83b49018ddde682fb5b8cb2df745f70e0d550dbf9f311d5add90443aa3d70cc72443fa2dfec37560eef033ff62796e1b0cc030dfdc39495ab6a70982bd6ac4baf197a2607fbfe38b6c643d4243c718080ce6d65cd22e531c0bf097650baca93f7c3ddc723680ef80a17604b1953b75f65ad4ea9bfc725e6b9b93d9cfb03c164afea7b34129b94fcc92f104a557babcb37f9c4c8bca37dec517b21f9ea789dd4528f43362b285634d9ec38a03c3fe1372288805a64b8f3f06c185f9382e6591be4d602656b1b1ad720820791d5f0d43fc483268096b323931790ec5b2d382cbdc61f51bd444912a8f1f1abbdf3d3670aa67c8ed9e337ad9ec1ed1fcf4faefffd8b77b6770c4dc002a285d915bac55e75724d6943ece9718f79049adfa2dc6da2dc7dc1c0250c9680aa5c8bb007f769eda26064d9402dc4b739e5b26158f8f69e8b2c4a59d2442dbe1abb3f698cf006c4c78daa193bb57129e5a3a6ce21e55a337bc71b2b93d372220cc3faf07262c85fd79e455cdfad2d9f1bc4f290c43b1d7d9a531df7be9ef2022679d58823eb0e83d23b2e8e3eaec0d706cc06df5c3bcf0440317962a4c5e7f6f5185e86b830b83e41bf6dc3d9851b52c643668ca4aaf753a94350549deba39af3fa3cee94d41b1d91a9726e7482910a12d4921b9352ba289535e9a287b9824373c3f1d0b953a62ae06d10383d04af0358875dbc420c026e5642d717997166c8d4bb22cd232fc5c360a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c22b22f48ddd207c07774ae9d90971c5288183420a22eaa06687e7abb3346650", + "proof": "8656094d3a04450c5d5bb3df596f54a7a089105dea0624338c35f1ca0dc3f6653c3654bff6dd051fe9d2f7bc26f2390561589d0d7eecc46861495a2f39805a39acce78aca984d07396550d53d3a189f89d2767bb4708c7974a7a2a25190c6b6918f09a22cf0c5954ac4e1848fed281ea5689dc66dca6153a8483e2caa944fd238230540cc4d99a2ce3791c4176b4024191fbd78eb470e88201fa17418a4219088d0a50c48352f597754b910903ea76765fee491f1d4d4303da16207cec15230431214a4773f64490e013ca277813141acccb57ba0cc6411e70eafadaa096870fa0b34b1c868642c3ad70b04eab964cf8ec7a51aab1d88380d655e3f15b6af7304045f1375970f3bdf3319479a5d3977bcbe17315020f4ec6ed045f144f37117d86fe179f7562efbc878053c5f375b906202f379b6d049fd3ec8b53eae2db4832e820a3f6779440e5a49c067c724caa3cb00b0a3f441f7db573014a5fe680413c98546a0e8a4bee7b0db2503617e281c5bad0ebacc2c58343618c845349f74768a09dcb026044c4cc80d4dabcdc9dd4e29a6bcc42fe3f8a41abad00bc69d0983994fd953f0f4ab769ba10a70edb2f00a4f11e867aa5fee1755eeb921cd9111c08523462ab4655d0f2ff951518d3f525efc45a661495a763d288d81d5e04df8d43b2f8a907820540f8419c836beebf679acf27e457977b42966a750ff030ee601118f4fb3671fddf0b5816b7d0ab487b3b2fb1bc04f8ddad89f77f8490423add759c1e7c6a7b2535a0914e4d80303e4a094f7f18fd8511a8faff1403fc2eb1d578427b516bfeef96341c79464b2f206c1be4da884045ae464b6cc224c99824c7341f8459ff8ac0ae6448de0a82c8db2b6698d67310828d3823f370c4a7efbeb3016d7060806ccd72624196f4e13b3dd005aabd6e4791cf2feb17b8832882daef03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ea1985ba1f5d64ea8c85390ca5e4a19a956a5e3a419b6d8e7c5e7ddc769d892f", + "proof": "0e00de1b86e00113f57fbf7f03b55317ccea68ff1b522a29b0eed0c83de3710fba24cf70cf687b7e66c9c55f3ceb55771f328e8d18a754c6de19f01bad5f0e20240fb1e4edea076e6f1af61a3c3bc0b1c136deeff7b8bd70988ec5288bbbdb378a1020fdd4db343b4a971a7222e916a0d7a6b2aeda8e87a52d9750216119e214362f6e4b6f38e817c362cbe21d8d1b0aeec23f75cc5aad0b1702021fbcd4f30142aa684c1fbbc57ba360c32c1c8752926d37f81d0a712326d9d4b29fc95b570081de1771489298d2516055efb65ac47a6679f20057dfe88b3db56bf460088908e8113114483bda02fb95ac15f88f7426f028a64f88fd0275cf03b3145319dc29f8bce124bec2216e4df840b9d69d2cb68d3834bf160d115c1a5950941d38382d3841757326069387dbac6e07dbfd3b9a7020ca0fbb594f64c43af43c2e3f5529f002171650ab011f7786e06593b992873f4fd715804c98c027fb4005b0d69b70caa58f3475c32d23548f54f7052681e3a77e598d1b5755b038c93fec5c3e967d680c67ad4cbac19954cdae957de2a69bdbeee570aeae2f720f7c2d91d3b2b92e30bbbd64032531786862befcd9bea79122362afdeba62c4ebcad5328ffbd2f3b80e0d9dadc3eec919094326d1f05cc1e50ef31904cc8ddb78dcdee0738d53266889e97066840e27d4ac641511db99420fa249a4ba22143afb5b5745677d4fa609c84e40540a9ebecff0cc63842c0c4187b2f598e816216d368cc2ea0b11cd737ee16f7b2886537b2e6f898de7ad9d1ac781f996a9eafedbbcf5336baf6e3424b3249222f2a199992435196ce7df57b0d3b33e4afdc01487e460bc1ce36b1427cb69be0672bef0b3e9b6964233ea9f29170bb66228f0a521d74d477fc8dc5850b5ade603ed1062003e7f179b1e3cee985c4d9b261c2f224a09133392b973b1d0b" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 34 + }, + "commitment": "18ae2f0e1f043e520ff4f8ae0751f0d51581da850026041d08b1c6d26aee295c", + "proof": "32f1856da39b7746f240aa5642dea715a31b45ad510c3638948c9269a521d332f69a4148d82d815daaeca8cb6b9acd0ac6b1e19de41ffc3c6db69e25c3025533cc070e1e223b0181acdf7eedbf98db31649141ad4c9b7e4b0746d6ee7601db38a6eface90fb24d5cb510aa2e0c1530e577bdeef619b429ee86ca277cece9df3f4af1b4313b93833d13d2bd67985c470ce9d3463ff1731b928344b8009988010f8f834f390329badf844f45110f8137ca867deb607c8e02017f88ab32008cea0495100cc363907c34131f33b519878577e8cdeee93f86472094a0c6746ed361069a0e54653bb9a79c17878ee02fe00bf2c254bab8bd5503db4a8d2be3eab336629493156deced7b2a80872d38249d7e654c06b45853273c14695d79191cfe7b158c2454bd75ea8a846815858c0bb8394cd94c314835506ad76b439c512183091552beab6edec3d5420e36cb113dad595576c025c36eb8f0ed46526029108c6f351e751e45b4c8d72ae6798d3a7b31ca624cfa20310721080427708fa01335b73d8c9749e01a03a96f0177de8b9077bb4dc92b32f877406bc5a441f526ac6ef93d0cb537c13e7fea2456bae87cc0eac43a23c1ccecba483417b973d3027d00a71d3e93406859670045de1af463449eafa49f1b9cf8979a5caadb9e94fd77f2275bfe0ed0ffe40a2e6486ebbb5880976086f2d73a1ead2da9a61b5684810b60137f423fe1640be9277422e73267a298c515f13df0b20633276aa6b3f3171cbef457581ae07e41d240c6bfe257b5be0b137a13c0a7aa6ec4b79c50ada5cf486c2e5db02a3974ee5e177aa8f743d7d06e5a6b3231d72dac070d59d14a7dd6c7401074993d96008d15a29881279d4a15aa916d23edb9a00da941f82898df665a92590ca6fc28e2b6ab28d2ce6f90c8d01b64a25f1008bb53bb000b73237b4c31147007" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1a43bdb23db1149ac8048164b21e6bf7866332cdead1aa5f83870a0824ea3224", + "excess_sig": { + "public_nonce": "5ef892f1056abc164a42d980cf784586dfad9c414a627f096af3f142530c3e4b", + "signature": "3e7e3d9188f5dd91227d75a32eb14c28ee23c8559ec53c93ec71dd70b39dd500" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "2e1a0761c886ca336c025873cbb306eca31fe47d951654446bf2e60003f3a966", + "excess_sig": { + "public_nonce": "b2c33ca38766dddc88062ad72c595b0b8224492910c7d4371861337fd269b22d", + "signature": "67281685cc23ca33efe95e94714fdd06e03139a3bb14dd1f3ac1a2200645cb0f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "785bf0771858fcaf5a88f7210e67262dcdfb4a29478b0294dbad3ef30ecd0b44", + "excess_sig": { + "public_nonce": "7cb3bb3e1e3f0367a37b60907e1b60e5dbba0bd6e6843a13a714548a21d2f704", + "signature": "700c092357a4e2205eeecfd15a98a235fc8f79bbe8eee94f55f6dc3138feb10f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "8c98ae77c8fd76d1a921ff22e2ba4a9f32f5ef01a97ac248da1aa20fbf731b77", + "excess_sig": { + "public_nonce": "f0c26f27df0fbe623177cf01872025e6df026c677f438c2bb1b3167614a0740c", + "signature": "9d55ab1c55b03ecc95722596d1d425d0afe4ffb0137f3780173b89fcd1384104" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "924ac6d6302b3dc910335537c79cae5e0ab63e4a952b29311620f9e0822e205c", + "excess_sig": { + "public_nonce": "a64c21125c48cee118df4997c4b532b901b3232b6d30b12f0a6bca002650215e", + "signature": "e04bcad9d0abf1a60268547a6fcad3a2e68c0c0a3a1114e9b0f70eaefaaad305" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "1404c952fc0fd14218834de6a790cbe90203983e6878d19e70ba0d522eda9f32", + "excess_sig": { + "public_nonce": "6ea4f5db3235252c1abf9d245da8d7f35dbf41b0927b18cc9317e3ac64c7fe0a", + "signature": "1782330a829e7c1eea2617782f42aa291fe70d5b5543ce8eacae85d359679a03" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 34, + "prev_hash": "a04d058d22d1da6c1e8b043d11aba56624e0634d4d15274c35e0d51715cb95ad", + "timestamp": "2000-01-01T01:35:01Z", + "output_mr": "0bae13d04860734a71bc6ddb7f3cbe59b13df2f188f6ad6d37a8075cf73a0d5d", + "range_proof_mr": "7752d80d4408d7899dbf6d387150c41f2d71584774fc4dbaba6ccdbd447984d6", + "kernel_mr": "68bd066688709f889c14cb921f0c2ac5c5c60b8ee6a62d64d6e68804fe910300", + "total_kernel_offset": "d7f5ddf7441c6737f89a90e76450da841fb8cd964eee160fb12d9174cc0b540d", + "pow": { + "work": 34 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "32e0deca835dac1faf0b9543f82706dcd2beee1d322208a0d36d3cf434aadd2d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3c9de4b403e4c6414217fb0469cb19bca14d81e76828e0e85448827f8f3d5952" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9230e94b4f58ebcc691a46f043bc5f2d292b2647a927f2fdccdd550ac744a16e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a63c748ea34c75817e5ec603f2a3e200557256b4bf91b0c3cc977956c2fefc48" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "aa45ab2aef06070431a184b586b9f74e56924ecc926694c8296c0b74fc30ec4e" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "006f0bbeac62afe9285ce4a84db94be22fb45a2d0aa4365568692bcf68d3ef38", + "proof": "561f1b0fda7d090a9f5f89d215202e438e031cd7e0d0070d9e6d67d718392c2c1a6706f38672f2adc9db61e8274b128334345f7fb72f4cbaa0fa7abaafd6eb4470756f363f5f9a4fb7ce4b4387640ad82cee407620f9ae62720bcf4f5b24565732ff4fc4832e567d32e805ae40581fa26f0f7f7c1e564dd32343ace486ab8062b9d6f21b7ffdfa816fd1a3e69e24ae182d3f727714d0b8fd4bcbf415f7c995066d14254ca7c1cafc753a00e057cb6e2b345abe3371325b366f466da300d0cb0de5da9cee9965bb460e049167ed4d5bcf443f6b174163f50175fe25afe05863079e307d6d0f97bef8a78527cedb63bd2288a339a8be43e98bc3c1c71c3114fa5dc8677f9b82abab87ce87359473c9dd9cd2508fd7162ba71cf1c067551aa0805530dc4fd29eebe98a9b626eeaa8d018ed19de9b581ff075843fc327d89ed7e43a24f327480e674753dd9111c3a700c2cc6ad6a886a774afa8d8911abc6410b94bfc33aa8da8197e1bfe1468f405db754cca5eb83a69be6d990b89df0c99e83f744a327143bbb3b5ba5cff5dba4110b07caedf6a935ab1b33989aa45d62bc57744de4692a78d428c2f59875c7507adc040ab95176ff5e6080735aa864c5145c474e66da8b1a4c9fa9917b0c59376039713c6cdd4418beafb7451943f3032421752529bdba76d219295793222cac0b83bb993a6de891ae4647cb6942aef96b2bd2d00e510449ce547377c7cb5ac2204b0922251d784826917c410a0d27403d8373e9efafd1861124f73789e42b3026d59d217a5c7a0ce6e3688f3c6fa86218afd532c29662f27295fd924746c69270ba2267fd9f148d7b9db380469b77639694e44f237d3cbd436cbcbd35b3c11cae6acab8ef3388def08f23225e1fc6781f47807ccf8c47b1ed1b66b6d1ee5cb075be60a4b3fc4ab11da5a0a4ca9ed850377cb09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1cc31c71623b5cf69aaa4a811fcb596b32459010f183df11df146989ccf70308", + "proof": "5a64941049fd91e33ab22f64eecc67404bd12d1e90adeebc915146cc197c885566a593aa85c393a6873aae9a68b00651a45de9172e500f51839229f716d6fc4304b2bc025dbbf5841340e8bee3c0126cf53a39d40ccbc7dac8e91a481eeb3b2988390d7135bce72512171bc2ed44eab7472972d50c0cd13503992af115661b124e4e9ae2f07c296bc0ff6f2ab914d4c9ada837e4d5cf159c5c86cc95a6dfc2042efa2d8f5a54038f0350b1a86958197533ec00d9d231d370aa3c84f98b316e0eea47330736c5019516400c25e594cf9a6f489fd84cc1392e24c1c7cce55222064e361f3c63a92600b59028fdc16af6f8908d7e4cafbe73de7893e113b0f61e777c65f13bee3202c443c04c3f4083dd819eeb3d7baffe2e730830982660bc2309b8aa1788772515c4444b5f7d58be5c139caf3564439fd1a042cc0df2f089930af2857586fb0a113acea58c7965fdb0ad16a4d8069c46db8e2cecc5147edb6300887c84bd3985c094a6c28774bc09b76fd6746a2f3f96cb25f7ed2e635756cf3eb83fda5f756ec1208328ed938442df8fc6bc448eda301541f3297eec1bd07a08249620fcccc556c9a0c270e3af178f855b03ada9ebdbb4af8eab74dbc0422451720f04b915754917c3a4175510c4331d1204f5c8871179040a755b0e28d3a3166a8533665563f1ce0042d931f8e01a17df2aa7e51abd48662335b159e8407a09a8eadcd114c6577c3613f4e8d07162bade9850f846be2271e200eaf1914df27be636debe6b640de8f41381887170ad97bb29844b31a718ba6348de9f105aee19e87a5af467a6d5fc30db8d56dde56d75c0e19d9c20bff670e7c63d99a99c721467bd67cf1556ca6ddc4726d59e05093ae006a92c88d79feaabe066b7dbefeb0bce75f74b86a7c8a1d61f182b7a91853cb94e5f523ede43818160f9350db63208" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "362e60c4d9b80f9717a268e7aba08386efb4752167314b871b307d41e4730001", + "proof": "20cd28c30461edb1d31b8f5b1d473c463e3ee9443a1c008bc4827f7dd176ca3f2ea501d048f917432de1c35f91bf1f9e1ccd40824771c19cd35b3aebedf5736e54673d89a8f128b642188bd567c6dacabfcbe860d02c3ebac9a6f3cc73e4774540647e64e098d04f01f4250fec17bc8ff1ea8cb4fe0e28c8862f4b78e1c148661c3d66f9f855e03bd46226c13807fc2604def8658995a441e8f01fc08e4a1a07360bddc367c1a137838b3a6f01932fcb1ec09c821e4d5c51d7a5344036e3010b77ed88e1bdabeeab1571604bf88a3d54c6f04d707401548df54a88b3689b9f01aab44641eb066032ccc3ad1d12a88b4aeb9f8245b2fdf3d2363ce184d7b04f40bace0b560ca7754ce013532ec42ab1c93470a405983c0cfd7ab226e0f42872130c59f73654ac6c8eb763e489f1d2873eb4ff154893207b31f274eadfeae7bf60c4f2b90c8e71c01883e61b06b2021bc3eea5a218b6f830c5953b91c9fa248b0b62ace681e8cec55934008973418aab04f863d676e08f547e7a81ac4f3e62fb33f09e38467e68f4ef208d1e85b58c02d6fc7225b3ec4922c2a6c113deaa84a9193e86838e8368808cc51737da24a738a512c513f7c5f554377dbaa97da343b84b5ccc76546653d2e4cc8023cf8b2a61317884753b440e6b8291fe097180080156705f649879adf960e9e454900af0353de9891ecf5ad7a809d68a2ba2cf5fe149e015747d00427af79b6e95e41456f3e4e0bb9449e0916c191b3302b9a434485fc2db22367f2dbb3b17e7f5adaf9e93b609a9a0896a5809ee13479e30854af33a14338328b5ab1c946a07b8daba1d72cd6d5051f7907c981854b3f8fb535e7a46a369b115fe3539f8ea8ecdf901403f2de0dbf346d3da48543b277faa2debe40a232ec30fa4de4392853183eca70d4ffecc169c25c3ec724bedd3a9313e2bff0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3a9b126682ed9b9924dfbc178eba66db5c0c6c51a0d6f7176a358a0ddf8b5d67", + "proof": "6cf64f384c4b0f77ac8c1c04e3a61ca1583451e6a427ba1e2fb07eff0f023253f84e6f8bbb396404979e18a8bfb9eebf238e494b8f878217fe19a5acf18d42261a33589d7715af6955d8b999ef0991a8acb2e112e37897ffe8d8cc2ca74f4c3f1c16d4fe5d1fcf63c652388293de00d7fb152fd97fd6a15f2e5163a545cbe42537204e46c8f8c5f1a48aa416ac675640d0942359cc93816b3425ddc67ed0f50334a610243680776f44d6939f50a4955d1453ea7c01874e1197156941d5a1060c450317ae3070adb17e03df2dfdadcdda344caac5bf3c1db829f66cb717602b0a1c2ac608248c64102d435357f92b3e73f0fd0e0fe079e620bca459eed6afc2542644c0b12d9089750f6817513bc0d3c64df7c565741fae99ef10cabcb59d592300fd89da8dd9168640bd366bfe32f287d90eb1728e77f5d579ae0f57e9b3c91c4c34154d44ece99d6a0c835cac5cbe85bc5e5dbbc40646860047894459df4411e6dcbfa343f5717d68ddd5d6b04bfed8de7cc01ce4e7ac7084426b3e677ae97ed88b86a9a7c2e696f0c38a04cd63be6ff69aa700861ebf78272e9478daa8721792ac5185b8141a55b6b4e81ddaaeb3b430b47522eb4c365e5e933949b3e8a5277c0e74cd47fd6b7e052b155ffe9dffbb9dcf6eec2f14fc25be9dc595883cca202aa2a4ea2c96f8fca8032f093ddd58cbe86c440885f346a8cd69851193cfd81a86a579d1fd0874047f424c2321074be9c81be59718ed311bbb0d440c24f32f4cf0157c597730c594d49b628952d3e7f56dacb2608615804c968358e4f283c2223848ec7a932aa6c574e603d61a27064146ebc8f2215ba1b8ede068d1c4822c3b3de1c4620154a366197f75ccef8ac56ac79b5048e9c093be06259677ec3a5d0b615ea2874af60ed05369ee3529b48bc406db03900384c308f92b8700b3513e0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "46b84daa6a3223b1ab01073e448865212fb297fbf670815e1c93c10e0f41071c", + "proof": "7e5462018588fc6ca58691e771451d7acf7e5547c355546f09e7872ac41fd927b0104ccb6ca80ea6b6b7233d9bd43e5a5206ecd86617751908b500bd8ca9ae06d2767950ecfb15ff5d73ef406b683517163559de9c8ca3a61e8d137c640ce02b60ea86e2c854f148704c57040eea151de2b0c92f56f1d5f9540a9b8992b86b6320401f7ea9ff7787f0c245bd1ad98c7dbd89ee6364d6ae5c260d3d1bbc40e70ca8058abc869b441eb43dc86dd21e862e788ca542769c87e4441f2eefcb30b30ad493e4b14496b98a0229c5e56285c7cca9ff2853b6bd43f39d45fdc617ef490e421873a01e94a620b873901e525488fd862531c13f4f39bcbdc0728400d94711e62fff52ac40a20850cf4337ed4c9bba15ebc971f34e3781eb6a3606ef0b5c2e48f1837f4b761ac10013d3289aef526af1a379efaa37ba3078a24bccfc3d14616a43872d533158f4bb09239785211e33bccbdea6d73d54ffdf56d6a652be921cda2780fe8192eab0aa73101e23f7f4e42b37ad5f7a619d02c27090f23c32366f52a402f4898d81b693c5e574f800312af99d2557cb3e0bf238287d91f4821d3034007b2c69892610f8c6cbe07ade0a9688fdf7dafcb85457d1db516b016333136e8f90f584bd95f256dd0b92bd36eadedbe1fe1a7f942a801a1f504ed0423865365c5cc060a6867a623a644fc758f0d1cbd30d5472e946cc8927d8e1045460018e8e9fc16c0e2b669f7280b9c07fd3e0fe6914bae221b3a37b609d2e118e0c42bc92052178b6514ec8455344733ca0884d9425164d69107676da6209f3a66f45f4664c259f37deec93d1aad2fd4face86a4372cff2877f64a9f8d998a46fa36061d69676144f2776a224d4bc75cc0bd06a33e98be0415737b62f63b0dee9340472e15224aad08054b5e80e0395b72c0dc1cf57d6125c7247c1cd563293018a01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "928fba870bfe76a3cbd3e28a2a8de3f9ddce82a8c90b341aef17ff3cc032c422", + "proof": "ca2ed0ff006040ff9f4b1ca064ea9b76bdd0f1efd9093e7cd9af6e0efa38ca7a5a18f5c050860bb81eaa9d13bb6f751634f792f9f00b5bae05464cf1a662fc13b64c1e72f04f752b1357a47ed2c46080d19855a2771a252b3254dc75a85f777ddc8cc0e8ea1d3b0925918bde4d09c746a83836c729e0e54b5274aff2968c4a6a2c28eb002bb234d239b38304e72f22f9ea7781386c49e9603d113ff2d2bce8078ee01581c489c44cc5c36aec5cf1e54ca094ad50c18ca3d74fe960f02cf92500436bdfc60afe42259fac6df402306684bef9c5c43586d03fd717b331e0393a08e84d1242a99582b451a01abe6e93bd8437aa5f1357a9deb6b46780861b7bfa3e6e4c94bbc068726c8dff583650e49774cac027043fdcac6ecb5007237fc14b21aa8d8e335c02b6831228995cc79429ae0242c532553e37a316f449ed9c54e260c2ee54395605a9e1c4889cc79893a8d14e55571b2cfbaff664d4a659df84fa71504245b122958491fdc1a2ab98d4aa657071f7cfdb0df72289b90f36574b84752a5e02c5dc61cf47c1701a95a30d4259a1b1e06675ae0115d864b36573ba14512e10dc7af10757aced433ab598cff25dc82934ecc732ef2ed9c40137919c35319ea96405c10956a4ff274053102c511a0e9becd0c8b61f817566d345cd7e2436ac0ee37e5623c8818c71ce42c3054c60ef0de3dd208f390d867dc2b21ec3a73fc6262cd82c75a7ee0a9d03e3b40e23e2a6d93070d56532c042b0b8b9ac05952484be83a2a0c715391cc1d38bdc6190c1f06a02ebbe96bb8e863c75c1f047ba10aede5757fd077373ece50ed9e912521fae6282bc8eae7863f003b738d9f0ef430d46927928af3bf4dd3c726ddba7ab769823202f6341b2b7b53ffe7be098730a3dc4d39f584e480887679907d283ea8de2a5dfb894c1afa31eef5ec9d270f908" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a02aa44698855ebef7c8f19307630aead59b78bf9344eeafbf43501b56547b7f", + "proof": "3238f0fef24a8897ab1357e983d1d082c4b86fd9d2bcf0e3e36b3acac1bd6a719e4b65b5d44c180b8a3c4a2f381c4e0a0be838c2d3f1a649932ea60f7751245db275e9a427e3b16d11f5d19438bff6df290274ebc3bc0f9763ace82c557ede63becf82c2dcec6ba9b87f1eebbe6ff48fe19655b22ea5e9aca86d468d91da772c36d4138f2219e02e04a0beebd5c8601d2a5c5a5861fde235898eb2746b148d0607f4abffc0f5ec3edc23e44606d73afe910e8573866b1e06a33dd15037c849097bf29dea651f3491a86bdd51cde0b90d094ae440d5382faa0d91daa7b355fb082e781a192e07deb05371ec0b21d6e6268d1002904d8ae692caed5deda35b697dda946b227d3d0caddff2786683a6f7303c57cc5105da4e35a1754cd9e41c0c1c12e94775ac2a6430f22fdd081e97eebab138eca6ac7c0e2ee3bd61ea3dc4fd3d7c594a13011949d324831156c16c124688785d8ef0e6ab51166e0fb39062253c9493e14bd2d12d764a3c4a7932c12ef3dab47933d14fd15ac69c4a2c9d81aa7512ec0e6a759ba69715a46d3dcf3ab4d9fe456df013c6ef62eeed3207f621420b1cbaccc872cd384495bac66eadf2587749030adcb0a3ffa96baaadc6f2289c426a2ed6d0c286b8d964c43e871b51a81aa5efb097248c030c9cd6cb45d104e915e6f5c45e6aa3022e2cc6083dd03d9b063b95f99ea3fb4747c0b34e82564740288453173141fc322af0c27a3a9b6eca58a69b4532654417c986bd7925e4dffe67421a6ed01cc59312d7b2017109e2ac4687c8f1e0642e7de33adb99155219596f9c9460d36825c29ef56bd37d35a74aa595fcdd55a58a364627acfde0ca6e666fee0f223ecc3e9d95c5442fae61fbbf9df1a6c0d3178a2d2b65adc7178208e9030c4fe77aa967eb3a1a2c5cd3225e9346a963c87eec2549f3afed11f18c04150f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "be97bde87868cad426711a321925932fcf18cd11f1c37299c8a54613969b2d40", + "proof": "7a5af881ad59dfe26587120f4021ad41f53f87d8d2488dc869610a96c597a97828cfaf35fbb37323d88764774eb6d7600910ec9f9867ff7c34c44f14fe27b774d02ad1096869a0f4ff94c65137345dfacc18e0686044f5c5570b9af43ef486375c3506f09c55be520bae467bb02e72fdbb5081f567d64b21c30672290fddb8214f33323093b7734e8ea9ce834e1ae092520ce522656adab9de5518c821eb6d0171290ef09dfe385f5d78acaab133ae0fc82c2fc4c06548c318518f634f6a0008d069c9a79a3ada20681931af40ac8c9b14ee84c45141157afe56db22620fdb075e1bef0f48ff6b0fa2c8fb9db2933ee65eaac25608e03afce9764568bdb95c7cd41509c8b46719b308ebb3ec67e4da2350cf17fe490814feca44bbb46c6dc825f847c5d1f9a74f13b4c9c578780a0e901c7b2a4ecbdc921c1c0d5f9eb1d68b23445fcba33db4edcb36dcf27a126370cd2c63c062d2ab256901d27f4ca7301101f4aa45dd1e027de4bce766254553361d402e70eb6c363d13fe8d9a5959fabf709cbc9c57796aff6f9c0a2ccaa8d4280a929937229cf0cf094ddd25e1a45ab060e8a661c32f2be389a91c18858feefc29e47bb291d634f015aaee86b8128cf5281c89f03f5af4f0785ba4e0124f993536b3e26dc76dd7230324a983b64cacee0d88e126f35073e22b53af9a86891764682fe1ebee5d0747697e07bd08b3f7104402c2c6fe8b1cd492b990080496a980ba6f1fd04169352dbf7c01c858caa4683ae451585014d7c7076b4ebece35592a5f2d1968438f9959727ffe619dd65acf47446d566307443b0bdcfdd212492933b2097ce5be0ef18008ceaa8108ec08fc5158d609b4bb0d500a1cc4bead8e95d4cff8ae0c9caf1bee005563ac6a2417870f09b7fc843a71095ad9ea44deb022802f7a10d093344a0da62a7138821aa16906" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cc826908f9108f02f83955a5c08ff75567a1b3cfed6e8cc0e62c04925807a87b", + "proof": "46d7f1d8c8da8149a788844acccf3af510c9d2277458d95834c309b9ca5304069434b94f721e009b7a56fff470488db1c88dd8f0e411a20ef5536b44e8e0ec3b36ab8dcd79b9c8c209d53891587ffabcbda4f91ec1a1b0f429bbbce036235151dc5927c920e9a4fdd961819607863d0083825a4a69b6714b84bb71805f005a1b5210e482b0d948d4f2c7374d821ab6f008319dcd690779cfdfc3061c600cc807cac4151ffad013baffdf204861a75100f34cd6cea3b061fce4fa0de1fc85fd00e031bf318699bc6f2f440f9bf4067c04fb6dd68d2db4d231bb80daceb6da6b06c0667011c779588df95ea865151524864b9537b278cfb4fe871aea4bc67df706b432c61e31a0974dd0a8f539558e3c213041d81c42a1c072225744f523b24368f42eda95398c8b67ac94326932c7b772c9e78ccaa6fcdf77083f16e07b4a4a1bb6f86f5051bdd55c84253d49cab9b42869880050c88d513bb2fd9a08f7c57a209c6c0b64d12c9f0b34de02cda9b592cec4a3bd3366e6f750eb6655806c0f363bd0a1ef62fa0484093ef17eed136302bbb538f53167f0ef8fc034e0dc16974f6f80182e4b047060233dd7788734cbd73198ba5db7e45a8e7b28d9bceb2bf4603ca6d323901b1faa79d72872ebe54acadcb55f3691d9f64b94f530d2370b282179e0430de05f9ef291f8e78322696599c9b8dcd4470272439970da69a2c4cee129d426c2274106379c681d4afe997b3dfa9eba3f37fb9b29b7625a85fd4c65790fcc2860bc45dcdd9b44206c5230ef58e61cd5c5a22b2d7cc375154faf4611dc052caa142e3667356496375baf060c545a646961d766a8dbaa0f9dad9d179afc41f286711efa836ae2371177a241cb55e4723a53ed7fa988b2dc29a3955f7eeb0b735aa2f362ae53e660f2ae8decc22f938d3b6c4141720df5e32549f537831703" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fadf1889108c9c69d4687169ea9cc9a61f8f7bc9931ff37ec1456b97e82d7a09", + "proof": "045abdd4b72b10a689366c7acecd969d1ce068613065605c097a1acd07afe13a92c08f0da0cea570f3411fa0106bd2225a79ee5a34d4f4a2e71cb43b81cbcb18faed8009e930325b9d184c0c186d68a0585d937ca403a36bcc53d76dbfe46c215a6c98c7102753866cc1b20f1f809e6ce98aeb93c2aa2b30558f70833d5391108244551d32306854c169ada0ed49094db8c457d6f131b66298d6b7b23e702203b7a99f3b6c75464f0c04a3450c4c52b548d9a173d6154ef47ea2ced168dd14007229beb5581c50b3fc006b017ec36771722686972a07f5ee4433d4823b36ef016aa90e56375a2afaf9a4c8dc73ce275066abbda1d8b2088bdcc3169bf828543042b774e29eec3d6f005f7b7f7d816913c155c01d7d80bbd45a0ccbad2ed7832fb4fc5750cebf2eb2fefb339a11f373fff9dd62a6214601deac5ad1622d6d0c444068f4c23b219063ac0f7b5812656a81685d6518a4f9bc7e6d6deb4b839ecf22d42e72eeded37a27dd27156da725db0eefc963f9852bbbc531b05c04c8688d05d40f12d3c092f808f4cb99e79cc338b1a6340f13c4beeb10ad25db98bac241376e17b848542389fd38d91833b80d32df058d1cbd9c45a21bc7963231a4a389103211a6e79a64878d4c4b5e45f0a988ffd8c551a98d44753bbbf36dd84d968f741ae9e77d2e3f558e7081ba4422caa8aba55a2f6132c53c68d95108e8a8e0ca30be85ffb5753f4758bd32277d9ac99814cdf63f231df38ebfddb59910420e9e035cb5dddfee31e4350e27858f2b29b34b248c6f05d1d40174f568efa21c63b23566bbe895569c6c4cc1465cee52275904a4dfe0aed4893f243e02d7346f96cd2b71c9d18f7172c128208b0d8eb54784cf945497856113c365f1daabef2075be0ef4a52dc219a9032b7bfa8365434301cb28f2bde51152b2b216341e7ab0be6409" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 35 + }, + "commitment": "587b7b847f6edddb3fbeaa3cdd7b1d3227af62da6b3f3f0194692dca3a62db09", + "proof": "08636744e6a57b016b0180873133d388d8c23186dad1ab32f0d21e9c94861b46da9d3456fdfa6f455592dda953d260df822251e223042c1147e9b4ac4dca947c06c5eb9bfcc7f6db5926fe939d9ccb25b93ec8cbcd79909cf465f6fa12d12145aebd869369a7c25628fedb0bdfce621ba84f27edf35a55797cdf1cde84a3305cb11527cbdaddb390a9c6fe0fb88afa2ca746dca860b5eaf66701a5c4d7f8e10459881507dcaf670e473feb993a0f41a2fa5f26f1467472dd1fbece15325e7302a48c00040b5896386017d7aae8ed06d992bd66a19008576d9c168e8cced41204eccea61742b4b204c365042e0a0ad9e688b7e3c1d9ea60da01d67e10cdc489412857fb46b3ca7991b2c8916f304b2ec911f401c87608822ec2a6e05e6a24043b5a61914d79b82b99888aa904fcb74add669c27cefedfd540df4ab8a97512bf0f4a17a2059499c4b9bde9c02849f65612b69eafe89e30c90eec26f9bdb01d972f946d88633e92e2ea3a4e1e92db437723ac03234cf020fe5198baa31f0a70b600b8982e3830b02435ca860c9e3e995a07f850e9ad5346b6e4b509c2c7778b7378bcbca41958778b3cbb8d51c6341641d43a8e834bb3c7eacbdd2cb3af450eb056f29e7dd5ab2d8c366bd1e12eb7f9055ad356ba5354e6484dcb6940e8b265d2381cc988235d071a046c67b5ea18eb447f39485fd0fa9c9e30f8d6e210eb2d2403a4fb1bac0c4762c27e7157254c0ac7bb50750cfa41909d8105def305869d8a61605576c70564278c374508505f008eda0fe7d4dda072ff76e19c145e89b45660048427762ef4740c7e8aae31e4e85ed7f070553b92fa9bf0cf545a12302fb64e7360352d9121257480b4c0e0d292a4c7b19b2f67b1885d163586907614550a0b4744003d9d5fbcec9d126c969ecd18baefe750fac3d8aa2806f5870f970c830d" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "04820d1b9d0f5494bf79f0c52f422dae6ba164eaea328a79d61a6ee6040c7433", + "excess_sig": { + "public_nonce": "58d790b8bd61320b1ad2604aa1ffa609462e5617f843b627bd9569aa7d51bb07", + "signature": "60cd9dcfdea4a6e61be7174d84153aa7cd269f8f14ab3b33619b7781bf822c08" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "68bb2b9c1e4ecf7826cbc239486149138afed618c0270741bfbda7a682f94258", + "excess_sig": { + "public_nonce": "aa1d2fcee5a11b790e88af056f91effa5e0ddf14e5b2ebda0852fce048e8d258", + "signature": "db07b6f072649fa06e50c62bb3409b07f7dce2033efa51a2bf0d9ccdb0297701" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "a20de0e9900ec6e35e700986f2bddec82f9406357073275d6d46426dd5abdb78", + "excess_sig": { + "public_nonce": "98995a96ac0c1d81915eb28212b198a50f686bbd6bae8112d4277f0a2cfe265e", + "signature": "10fa3d6440da355b7a7a13f7a0c7753bf3a5814d67d672e3e75b0db29d311901" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "f4d8a66eb783346332a0c6aac938a4ba81421da2cfa200de36f202c29274cd3e", + "excess_sig": { + "public_nonce": "ba40720f4265dcd5951a12aa974516906dc90bc850f37a092f4df71b0200a356", + "signature": "7acfa4bfac2b7905ff6ad4d364eae6da726c6c35d2604e6d3fc007ce0500d30b" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "fa2ab7544a8cb68dcd6fbb7360153a28532ec650de0816559a1e712aadf3865d", + "excess_sig": { + "public_nonce": "58f70f289a02cfd0781c97c677110f69583e9b4adb5ad60a2281df8c411b1e69", + "signature": "9e817a732365bf1e41c557a9e6e1eea803be8177f58fc5c5d39dc66aa31c9e03" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "c4f88bc7ca7eb8fcd7b766847081532be06e933eadd572866fe5a98955faeb42", + "excess_sig": { + "public_nonce": "c0fe4cdca6288677f05eef2be04964b280a99a2b8870ded36a1789090186494c", + "signature": "44c1f298ecabac5c4b41c805e4f741f8d57c8784df0fc8355a460549b225aa02" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 35, + "prev_hash": "eb03fc6dc2c32622ea92dd43519e7ab001740bbf307baff1db54684b8ef3fa10", + "timestamp": "2000-01-01T01:36:01Z", + "output_mr": "3e2e755d4c683b06262d3aa456a4bdcfe1864391da56bf69cb851902cec875cc", + "range_proof_mr": "29ed938166e36a189aa074fcf6c58173109b0d0371b0d1a8381db8896407002d", + "kernel_mr": "f75b15d3ad371552958309f70cbbcfd2fb5d294ffde1ac5ce9d73ea14ce93725", + "total_kernel_offset": "ba6d4d14d0e3698db36c13d017a70b6ff6c43316f28534962df12ec6f4336102", + "pow": { + "work": 35 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1cc31c71623b5cf69aaa4a811fcb596b32459010f183df11df146989ccf70308" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "362e60c4d9b80f9717a268e7aba08386efb4752167314b871b307d41e4730001" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "46b84daa6a3223b1ab01073e448865212fb297fbf670815e1c93c10e0f41071c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cc826908f9108f02f83955a5c08ff75567a1b3cfed6e8cc0e62c04925807a87b" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 18 + }, + "commitment": "3480e58d181643d932004bf4722f4f8ecca015b2ab1239b521655a49b225344a" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "183f65f245fd66fcbcabeb47fef8f4395700a1d482ab816ebdf2f1265adc3803", + "proof": "128226467e8c8b94656153c0ce368a50580330c131e13b106d5140a3a6fba0505459ec68ad4887084006cf35e5c86ef6693f0aa007deb2af734f6b841600cd628c091d43c4f1259791e02cd7fbd573844c35b453ddb5f0b7e0000c967556dc49fe2017d17af88481a4a3a2d4bf9c88c308e5f2376b512d19439e38fc7ec81019f195aaf12477b52e4bb524522bd81ad7c3440497d58e0aaa075de3026c9876087b2fdd17b34058ffec01b2d0d8f069deffbe701943a3e769fb89dcc9885bde0eb84b140981603509656eeab50866468edfe4e340ab03f99456dcb556d1c95e060ec651abca51554f88358efd378d8aee262d076b988394f8cf10b794342b9545845d6c788fce6e37ca3d9e58cdb9c55231d2abb11d3b9a7813c38eeb7d2f103bca27ef0fd68efab90c51f065a06dc541767b55be3dc8e2d3dac50c090c4ca25150ec7f6ac2487ad696101336b5b0be5cda183a4945f62feab6e81af3c615d567e44e3537609277058cbf506a6afe3788632e54ea6b7e532012825c420465a31b064465401d803756c59c9cc08c92f39a329fed6c0096524c5c1ff01988b8c801ec98c31ec00dcd8918097e022b779ee612a312ed843066e04aa25df7298ccb3f48e6f9530c477f00c1e5f10b9e47544b1d90366ede65cdd7c448cd4c6e1d703a5a9b1bff64241a054607da43316aa1b98c5ff95f6a5555e770bcaca2803374317a65b5abc4e417cc3a411dc914d4918b8d36e4de72125ada43b302579190f437c0eb807b7855800d8d755e10e28a723068cea0344392d9eb598ecf34c246064eecdfaa5142d0564531bf9d89cca29ac9c4a6da4640730546163f7a0bb1a8312022928c9a6dcbe879e46867530d1703919f0174ce0016975ccb5126649913a7086f9f8c9b54db121a98444ed93b9572d2f4b5f44df3788e4400ed16fcd26c0e0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1cce846a6fccc2a5fa42ec3aca40f8591571857d8cb7f683c0f7f14b7df1b733", + "proof": "4add85952e66cad0f31274e352e4e477d446c2be58ad7063d5af1081210e9a0a507dfe39f84a6ff9c4a6f30ba5f56778a88d63b9fcf0974ed43aabfdc29fc61e46c3da7984bb406c88654ad1639f611f6d12b75556ca45978a07fc27812b9916d4f83f19a7fd027c201fe46b191d07357dcf654652cbe3bd370fd4cae7d54e3b862895d36aa90fc551af3bff20a0c1eb131e50f60ce9e0cc18f42a37451f720aabba3676518b4817f21c514db563a7eb5028c8d0e55ae5825457997a27f8110bfbcd433fb3700f058391060f424aed06782ebbdc73e124bd727649cd18200c0584f8bb6b84a173970028e4d7606dcc7d09f753e081188dcfd84e9b8317dfcc63c8d6c4c1148904132c30842d29123289fa29149a87e43d7438dcfa8cab58915a8ac165f46df8c1f3beccadaac71b736fb3e96d5b4c4a58da977f9cd5199ed7232e4888e6eb949523555668fa613be327148a2e124dcde224dd508178bdbfc033b6548bb265baaa72866e275b9a36ea64ddcaa68bf5f19f1d154ef13c932963216628e5ef5eb0d352c927f228a4100ea953ebe590debc27dd8767426a151c7611145284f7f516059692292ab009affb902e9dae3d06837533b50db9f7c2a59813080859cd216f5baf6261ca6c08b64732c7f49fa72b1cfad3aa88b11a8bbe99021a25f3953317e09fd0dfdba8ac236a3e525465d785ddb3388df4eac98a6c84746e801d2ef380904c86ff0a0f3f595a8468d6bce46f40063754f0366725c3794b661d4f7a4f66084eaceef3b67f382c2542fd3f35dd2c0f06bb54b0277c5c9d66128c10a0e5cee9c0868328d70351072b5a4053caec778035d0b0ead2c4eb903a19ddd21942d5c0c36e38a26bf3a8435d709bb5bdc83b6841b6f745528f9be10a7754992311b32f8781fd245d14c2831c15d5d19afe546926ad1d3d1896792606" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "766570c960f413042a287cefd3bb24615c26aff743832198d7e800ad7a48b64d", + "proof": "a0d6d569198b66803444939f99e567447d4cf8a6d8c87e278a17226c1356805126bf0b29589534b18655b5c2c2d0dae18a625d0af27fdbf716b5e450f05fb21266fb6ced71e23ad33b98dc592879edad9b29a4ccf10653e804d11b946cd2ab303c751fb0d388a1ac92555009b859c4b4afc70e7a9ae97febfe41594f7dcfc901c6dd11e051a4182dc81f2478319e3e9f57d61971fd931e84e95ea2be11a23d0f38d0ffca0f4516dec83eece819b493f3c97ea1437e75e32cda31028031cc39098703b7452eac7bf608ef9c8bb2c232f75abe4a76e5466a2f999d9bdf5b194d0b7ca471d8ac0d670138a7d0d2a20846ea3e9dda9ca8606505416e493b0fdde309986fc2aba171e4635f06d74423ee4224c7ac8993f4c7c5aaccbbfe25a0056630301ff5b9e3f714c137bff35b1d0a17ae4af7a56b67088f2a0decd3ab6a0f1712be12fb980c06ca5d7f93a7ccd5b44c6e494fb5d059ceb6d427ed99a58645be1856967f9caafe5fd573c3d5cedb1e88a677ce5bae079ff46ff0dbf2149dfa6f3102ac0ef89ad76a811fb170fa6f2d72f57456d595a17e5734df5709d43df3a24d04ea8aac6dfbd654bc749bc995e5da4aa09f85164b55714bed6f20122ec4f6611a67c6085f2a9aa18852e95602cdfee71815cdb2b90f17a5c3bb25a8acca6412ae274581f83b5870bbcddf0f8babc2f8a9acd6cc6fb5a5e078f8e87ca64b20610a05c3ca31dc10f4446f845ae5c4fa5153975c1e1defb0811c0f748a3a8acd6abae861591340845e291b24f41286e9d8fd26dfe7ee6c0b6e00028adf728de3586c99c3ba9920e88bf4520137f56bdb3561da15c7a08a8f82965d6672fe773119d5918189bbdfd28bcb1bfb972ec7bdf2ce72c8ca6e3a9990aa0dae6e9f77590e3a8eed25209cf14bc91b0d2c6b66d200fa4ad0171131400c07f56915305db60c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "82318deb45e5f9191764b6ebfcdecab186dc7a9846bf6917e3817db22b317e02", + "proof": "2aa1d4e2c442ac8d60ac73d948b66d15f60b091bd995c81d2f8424409d58c745fa964ac989c89ed095c1d3a5b6ffd58cd111221fe2d9b780ebd746ee777dd718249775d0565a79e1142b970570daba11baf9c682d529753da30bff5118ca1e79462fc4655fdc12a6796af9f62f8a24dac98051c91ed0629da944706ff3213b41b621c2a21532e10548fddab262f8c72305fef896b552fdb29ad71196b135900c2f6714742f26867983ac288d231a7c7795d3733af940e2c3e8c8b99eab02c00a89f39301aa708151adbfbd974a8119a0cc318183c455d1d9c6d875a10f5da40ea81456570f6732f76b7b62fc7b984b710726166667f432782d95efcb32ee1668bc39f92456658c3e196e77ab187760349aff2de645e2c02ffe0adb1baa8ecd36de4f64977adca4bf5406135c2ccf74de89d09a4a32ca695c67c260f3e92f866e4c1f1701be88a2f0cc6c18af2f245e225f427c8951089279cfd230850dd583009ed9950b68df5df3b2e3d7b82bac6dc3533acae90ff59b698b3db088b72d8f1feaa204aa5f99c8a0e24c8976188aeac6008cc462d4fa214cdc37d57a8c34b0201ad58cda4cfd3adc1559a4dee2e5a2ffde5b0f6d2545184804d52819e6d610008e49aa218678b27599ae34fe65d751d5f332b68da382753b9e8449c6e0e0d02046301dbec38e94adeb3eeb0a8d37887a60e6561cd7f63bf8f27f5f1cd428ed50b640d58568cb47c8a8c445557c6d6857f102afde63c44926684e1ae53233f74b52b136e7ae0dc31d94d2f1452bb73bf62756bf0728630d12c3d7d553997cc1345a77d18c740133569e859b08f362db555cd64a0c50784f1f037400fe04d66d082c2c928632332bc642790dab77560d2e8c7b5ffddccfe0e8c16a7bb371aab803354f0e2f2f6454e9e44767f32caa7cb439aec5fb5e2adac8a24467147502d004" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8cba281eb36b0679fc43837b6506b8a767a24ca15b8d182b0a43173f3e0e3a37", + "proof": "8231285d6b2d5290cfaa2e09df5136289067a0b72db916068e24221900b0fe6faedcfb1fb1c2a95e04729f4af7f026627a2c01349f93e143253d259198d4a82ca8836ce6b7f66a4417ec97e971649fc487b219fc14d21266a1846f7ab0749d723842c0c16b1099f269f8569db69e3f4556a36536da07c6a2a6a49cf2c581e0100f92e349210fa43067b63a246a20f6da1f462ae2f00548d7e18de8d90fd5df08d2f028cbb1259f628fe3bfc1ae1965be73d51b65e0a8e854ce0bf6704d57e80fd8cee7144010787f7cea73e8d96b5c8ec47ee5f43c95df4a8860a0b586173101125623b8a7debaf89725b8db570aaa57087a75b3a5c0452328282fe5a5b53d49c012ed46384920bfd54506ed0dd2e60ff0c7933cd73d2db921b5d0698a3a107faa507898d2e0a392a4dd828b47a6747b9ae4fbc6a01c4c1f33f48304c5b4101386926c1b1d13223531c922b23008d9a26c65c9ec8a22f20980b4faecc573d81d1427f43bcd139e0783c62015b01097260d54b62e6ab9f07d4ce78c2b6c8c942ffaea43a9c6e03d106d96c730f753a06a9b36499a9f4b5c67b927f7b80fbb9c43c63383f23c80c2fd31bffddca4f667f7da800c866af6639f9fb29d49357f9258725fe27fbdd08281056c4f8607f42c2071632a7a895d0f4bd78ee6bddc48534a4a3da2c55e30e04494adb3ea44d3498b6671945f1d31a57eb2f399305093bf397eaf7e83655489e8665c0dc8033c9318eb1fcb1f625ed847b33f9e66f5129f335069c5c6d168054813c44dc29054596ccebd2a1258b12c4564e3947ce5426521943509cbf472fe763857a76f9403ff0dee0d5e7e7fa2f4c86051de06e9d14316ee38fb23be9610369b0715f6e1574b0050975b2c9f504ab27057ad77e9235f042d52b26e95d4836f06e23b7a2681604cd491710bbaa3a953cbf7d0e2ef15560c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a00e3b5c42840d6988c82cf8b2a24fb29b3584a86256619eb1b904fe31feed48", + "proof": "58404d9620e1fdb63bd85c1f291963c6aaaccd4cdb763756a21126c632bf1173f0fe5306909de479b776aab91e909d36f408a0e5877f4a637c0708f408c3c94c6272f2989ca026ed1af67f7ddc1b9bd84930a58aa339b9530c2954bff0ab823c48ad73a669c94dff4ab13d3e8887beb0dd668e7ee7b9fb6944deb71764690633f1e7a21d3332824a771ef7bee0f4990c06c6e621dd791af2f0020e4c5837510eec813691616b079e2d531ceccab052be4cd7193bfb612259c3c9704f42840e05c5a57204f8cba87dbd6bcf6d05adaa7e18ce8309c49e8246a44f92918cfd1808c0158dff6c4791d37bf8dfb0ac31c0bd2912af5ae299d6ce46f02dd5bf1ade151a4908f96f3ce468e0d39215476062b08bbc6fe2d1664ce6ac243b4f9d10aa47fc3634a5e14d94e55627bbc0af8e0f14dbf8c309c53cfa355ffaff4cba9ce96bd676ce210f2b2a777e31cee00eae87f9b98f8dc7cee5bbf5f54d667b28804319e6406983915fd96023d0910e777cdec4f883a34313df50af0ef4e7d85d63803258dc96654fe25132505d33273bef4a571232c32e30eca00035f1bdb1b154c2406e9a54a12ea6dc20d693b47d42d9b23465f53ea1e672228a87587b0d789e465212eb75a2ee0a346311ad0794b3c70a8a19885c7df8bda75918ac49a3cfcd4244aabe0921df11ed89a974b08400845a49b37981bfca417457b120c0128b2b0e475ad3c77c26d86c4ec851804b7a5f8b2869f62854db1580af796583007843891764ba1986c5f410b45a825eaf67cbb8a39189e2572eec05e29de45ea2add4dd42a043d7d071fa493f86f29f7f109ecf7ce34fc38d449d650fbf787c126541a50732d20e6755e8a40c609a663a245ee865165fee0659019a463e8e0ff699b1840ef3ed1f194087e2a61a0ffeb972f488e7dfa45da399382e6c527a23c5279eaf0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d20e0d4253183d11d0dbf014456070c4d09d5079bfbd9260edd83237a7a9e04d", + "proof": "fc59d99977538f0f04341326af23eb34c64a09e0e31df518860acb2bcfa94b578239a8eee6203a4db41f645ee25991441335fab51216147cfabb23e0fbe5e3409e141d3ca24799e0c19d54dc496d389895212520b7b3cebba92869f953bf217afc1d98a85835aaa8ac644acccfa19e1a680a80d429202aeef4fcb26438e4375ba6cbc5243a3735d3123fb5765491b96d7ccde4488006bcb56defed1c7bc06b034ec5d69d00eaa8547d6a0b2f29931161d5a4c1fcdc598819cdf63213a7d09f0c25cf731f282edb3246be2620706a730c85a583a1a66fae872365ecb4b9ac6a057e002baedd19cd6b1b744a44ad38ef41a93f32d111d7cde10d9b706ae73a9038f200b2d6893d5da6bb2f229599df2205ec30cc03c399a75378af5b426802211628961ed3a9bf77d6230b225b5b9645276c938d52f85303fe70ecaa7d97571b58dc7090bb3cbd3c596171b10cf14e6078b19575f8c300e36f0f5137d22f5ed019b088a69bf24464515233b24c091f3021f28de44a4c97e1b56a808781cc064a28402e17b7cc249a07b8e6a73058caf3f04f025c1100b1eeca6ce02bd9de68265850e387545f597fa1f4b2496eb4e84435fd15cc09a1955bc337d2bf8dabb0e826a01fca836338a8eeeb2e672d3c2eed9f8a88e9c8b313c00fcd2d51f3bf1c60787ae953a66def53d0cd33e8851c0e1905774f5bd633ddd0f71c1a2b5aceeb1a548e6034824986b19c75924cc52987463ee7f94ffa14574a411c43d5aaea5b403bc6476bb69ea1a3c537405d16fc5825745a7153f7c8562cb1b145b0f8a5fb2025ead83f6ef1cc526bbdc7d63a30368abccb12f7951bbb602bd67ecd59b468af2ddb0cff3748e8b2f3fc45a22eae0f6fba821d0cb180ed20672953227c07f0c40fe935740ae4e18da1054585b6e1a55676f265c1355e2b2a8d2b5b5deebbd96004" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e69f74ecb9b2dd2b22599b1b5af49b2b1bb88ce5e0b931933a94d3502632043f", + "proof": "2a3f574b0c3b8cdd89baff4cc93976b2e42f7d889f25941bd720fe25acd27c403801483e5b9ee2df54f15e75ef613aa2ce16076d9cd8830d339b77b520ad54319e4301b57cf06be249fc76778ebc6a16c423e672a57f27150878a8fcd56d98353e26e29f71d01186a9ab8d2b1cf258aa311739429cd0ba21c9e9dc684327c34be673efb8c2c082bdaf2d86a387b4200a91fbdc1b2719a0512df6f0c30b7e460f3fb19dd6fda95d5270d6ee6155631173b99c0ca65019838643b75bbdc53d5009cbd4d5119da755799d97d916c19cafd65146f73eb28f55f9b5eea222f0d55507e6ebaa7f7e911d41a929556d5202b68f5554f57f29885ef36505cf82fd4fbc23de6b05274db2fb999c02cc4ee124aebc099514b3aaaa69b83191f1a942a0927af02a53e4164567a832656c440964c33b4c0a64d01ddd51b92409c9f6c5b63f73a80992d730e8a26aa8ffc407eb7920a779c88526cfc949ae66a883afcf72b37ff6cfc85b5c58f7deddfea07108ed7391f75654cc59c583b470268dae7cc5796df214207739bb2961f1e50fb78578338b1589f3b7cbde807552a38225d56c5a43ceb3a5401452b7d64c6a29b26b08392d5dca73bf05749e8271d7967e267e57475a5d5f7e78137a00420141031386629f433ba2f1b69cbb4911aef66d67f2eb565edcbf7172fb6445db985bf9b86299cf739638d6a23d2338e4f7552694591d54a29258a0678a79e0ff2877d310be8e52902cd38b4e50299ea9bd6207bb3033653e5bce0e959caaeb53eac189d21c1a934b755999602686d08bfec0edcde4fd1168e051c907bad77dc6e6f68189bae260f52927ece558e85dfa735cb998539844f717c42398c31c9221a38e7082bc3f65c276e5388ee1376677eefe73a184a20ded468f9a22646006ca33e5f5efdb7ef13581413b09343095b755567c8c7ab503" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ecc722aa1897f5ec4f64cd4f691cda8008f483147b25c650fc266fa4e0dbde4d", + "proof": "7a6a4806b8f7d0175cb7ab34f660fd108cbc38fb612efd19645a2865be6bba56d248a37135fa7364025fed0ee70795d1362103db2c65b56f2be39f6b911b7148b2ad403af720256c8b00a5f49e7ce1458d439972d751742ba17de771cc6c991216ebb60a58115fdded3831507a5bdaddbcbb9ce38ef2168641d815d5cc64007ad1f2f396eec2e49afd4f4f50041941b236892127ae42c01f4fa8531838717f059b6e2f479313737e6a0cf185f4bcf51b6626499b66702aade3bc8085645399030ca34fbaf314ccad26820bd996018b65c0719d814a900bbb7957fc4c3222af0786d0a852a5f72a70ce9cea06c96a06835af92b11c467ee37cbc5912b5b93487e7c1fd582777ec85b27f53c557cfb41bf1a5a1e7800807a33f3ce0e019ad43a709c6908cc02c0162e38644d8a9cd55af441c97dcd993f9a6d43bfe1e4c581735906955d92477d47137cfa13860c005070646174adcd981367263cccd703a8427ffc5dc508b273ba148d04d6a50a8fab7e2ea43511abf010e5452176336463f6063a3ea2e8b117c1d87f9d56d0753ac0428c3ec248a98edd16c1906ae9a0830d435e79461bbbabd8d866e84cc9adf03a51c12f80d4a5d23165ca9a6c6b0e52310d289309e642ee90eddb2adfda3594820ce476695705ae6f40a691eddb0394764f9878cc891c5af139f0ebc03de071a3036e5c40df1655c15c5c100f2a48085976828eb9e4580350e41838c96914a541cc843ba74bb5e1b058cce298766ce2a301deac3c976a050e8b0f3d882fe7984fa4ef329d433d93299fcb2897d2d9be080bb0976312c4b6936620a9f5163ed8c4490c2ed7430bbb58cb2efd9384e9b7d673f5ed1552bdd836583ae2526a6169c97ef6c450581647402431cfc3197fd6570e7177d6b6240115f58b846037dd9349fe55cd29900bccdc614964187054425903" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f2fbcef22353e5c7505d1da1bdce4725e5b7eefbfbdaa16579d92d56a679d408", + "proof": "3e5475daa7afefd308491b69eaef85c6b00e73c616944b381a254f61aadade0010542697760e6c25adfc2338676856dd3192204639a2b9aa42c9a83dc1aae83866a49ac3d279540413a7ff8ac2ce498daec8e20b0898fb0ec8e5bac1a21d90713207cba239d41a41edd034dec187e3826fd6d551b57aca44962b2edc3324795e5de91b6d52e67278a7f963f062dacd1aee31893e98b169c3081cbfc42088b804e2ce91a20e726bb912775510f25b2911f69e10b90a862c77460aa6d0c9ac0b019e8b21356a0d7939f8560b550957643c9353ad70631febcb1f95931d82dd1707a08cb0564fe21faa1fdbd9f5da4659a369e06032fbae4e95598cbbdd9a73bc20748ac5ffee86afb9533a698c5e550f6713acb5f0763fe5a7da36649c40ae141f3a4b54feca8436712639541623e3f52aa7d31d5ecbf5109fca0c1e74b0fd84383cb875c59906f4ba31e25e7e898874315b2c85015df1dc13b1915bf99a795501b87081ecd188989dd46841b35721bec5abdf6a09c711a58c052079047576406246f6354c67aa81d726e607cfded6491064a7a9dac2d66b7344a2afbc2c391b6f5a20d3f703f41947940754d98f4d8ed6e2bb5c71865bd63aebf5c7aef503944ef6881abbf79ece57e269672d5077e81c0de6044b9328c6546f78b353fb104531eec2d0fd7859ae7c649cc975c46597ce2a7c68e1f404784c0ec519f3c5555c68f2df32901e82e1ec0286d2da9adfb11ec8520ef640893ffa32287dad4f8598086ca3f1646173a6dde34b2a65c059824af588ecfa542b7ba3d39ac278e8ac36046a61e069dd4a21cb13675eb4c959e6b737971f1f6d591d00af695bc0cf2e5322149e05e89ff6e17bdb9d059f15658f8a83f986a7a9fadd2370e664b18aa55900ab6a4907720c2a646e8bcfc1561268591a2fa4880d6417cc07c4346176a4ee01" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 36 + }, + "commitment": "0abbeb511b0ec05b4b9d10600b12e73cfa9bd9af08e5a79ede4e25f6e8e88a00", + "proof": "9cf1cdcca266709f23e1a1e552b4da45d19f143d7a397e310f635c584aa1187a5478e525239de5d298dd54f09846f086d722ba6fe8c1a97c98602957da31f3217ce460760c0387d24cd64fb7a96bd2df47be5edf7f06beed260112c3eda46c74526ef033409c911d12831ba55a5a0588f8692ad5711b17794aab7e7745588c06529493af2f0f6785f0fabe50f38b42a41def0066d3068cddf4a4c2d624d5d10246515d9a1c8b0c7b74341c7811e7ce1105298dd8efbec6dbc528a9f808a64b0521ea29598be95d89beb9e105c5b47a548d7cbba94e0899ed029b67f14e462704cac56f83f64a71f0db094febfe4aef51e0620b4003660a261f420e2358cfbc5d109ca3e6efe08259de50335b70c5360a84b1efe25ce98b90a6ee02e3cc0f5866f6b168e6df5758fef558d24cf7014ce5f6a7d0d086f377bb010e237e5412fb2a42d61825de892b1472c6dd01f12e931a0249ad1aeac31a7f1557e6fc9fee8005c22e05f5bebac01e645aa52d0f786b47e90c16ead629941425a149e6cfa354452e85d9e1133e7ddc80d3521822004ed9b2dd0f07fd6b2b5d91dbd866ed5b677158f07527c251b71d11e02cf6551256710ed3fcda543e0db8a6071ceaea43ef0e905979632c1f82c63a12372836fc54a3b534e8feca619beb2ac409023b644d4a6250b01217a434e2f01ee71f589c7576e25dbdb00b50cae6d20fe49d94e9fb4d483588b15a4e540ad76e2b5ca9c7929a7a17874713175d0f9fa7e93bea05b3126400c18272619d2ef74243330309199e408140decaf447e870cecfafc171b7499aed78ee1934b78428aa8b405fa5496b2237acd29e667550691b665ce94d6c3cefbe87052f6b25ac3409a3b482c5c482cfb9960daf6eabcd9c01c398e320ca018a86e5d8d6f45c52d0fd24299af5e095c7d88e0d4b9dfc2806d9d16b1eaf160e" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "32abe842909ba9920f1e892e29f5d03c6ac87feb5f958131c26a9419b7a5c214", + "excess_sig": { + "public_nonce": "ecc363f8101f615700e2c41ae5551e6788d58354df1f407e38d5c9fff4982262", + "signature": "8953da24c0cca70150ea31c20ae99a9c327076a8b6b4c67e2bbb67965583b506" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "86a9403f0eb67afa45f387dc0a5ed0d8967b792896b0cf71ff6ba4aba9be585f", + "excess_sig": { + "public_nonce": "a6217671d14749f3711cde27de6ba9a71ea7433ac69ebab22587bd1c72764021", + "signature": "1df971a86773b9156ef409bbe878b7a85e62f538bf0b3daa7763437ed8acd40c" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "98009a3e30ad03cda2e09e90e4e808bb7f157d7af3a0d6dbca08ec57e0ec453a", + "excess_sig": { + "public_nonce": "aeabc011a4d451db9cd22f6a753e1bcd4b9d0a0f858452d28dae087ef6296f0b", + "signature": "1217dfb8f802a8de2012a4eeb3892326bea0e8dbb5c998b212ca214890ba7f00" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "a465068f799e49b45a26bd57aa8506349bb554a0158dc532ce0d7910ff65867f", + "excess_sig": { + "public_nonce": "867864cf0440ab3a06e3bfaae1514ede27967a9496966ad7e49d4515f0fbe654", + "signature": "033ed689f57615a70f02e434f31c661c91c9a30db2ba5ff88a0ea21ef95bc50d" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "b23f6ddabd58e1d7c0db8dcb1357ee8a16caf2a7f6900c0802c5245a2f24c268", + "excess_sig": { + "public_nonce": "30b0f8583221aedf8030ad77bff7995cef103eb7308c18bb08e797908c609a65", + "signature": "aca066eed1f294e82742b083c88a0c84589cd050d1ee7e8d8f96c28e2010d908" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "2852e2907c9a619b34cf34422fd26e7ae8a5da6a331c0fe7b87d3f7d42a84351", + "excess_sig": { + "public_nonce": "7485cccfaadb37e82e201b127c0a81cce46d5369c6d0283501ea0d671140544e", + "signature": "a57cf24d4795d805826b00744c7d5f797eb0da307fa7c669048906e61cc22b0b" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 36, + "prev_hash": "be2aaf650771cf52eff4d818a7b129baad837e589f2a4235cad55b955d765f29", + "timestamp": "2000-01-01T01:37:01Z", + "output_mr": "99b1897a8bd8aed1616e6d7f636a146fe3a190c809764d6f1a236b622c7d802a", + "range_proof_mr": "de267cdd313cbc43b2208d91626767ae55a76db9681c0d8b4596b9c5c61578e4", + "kernel_mr": "5ec3afd1f7d047e900b04de62083555b8121ee8fa4f68cab6eadd836bc69b763", + "total_kernel_offset": "d92efc204513c38ccdff1226b6d7bb3829d9253b09ba0641efaa98b9362fd905", + "pow": { + "work": 36 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "183f65f245fd66fcbcabeb47fef8f4395700a1d482ab816ebdf2f1265adc3803" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1cce846a6fccc2a5fa42ec3aca40f8591571857d8cb7f683c0f7f14b7df1b733" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "82318deb45e5f9191764b6ebfcdecab186dc7a9846bf6917e3817db22b317e02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ecc722aa1897f5ec4f64cd4f691cda8008f483147b25c650fc266fa4e0dbde4d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f2fbcef22353e5c7505d1da1bdce4725e5b7eefbfbdaa16579d92d56a679d408" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0ae457336d33c0ed8a3549c81b3de67f21213379c3cbb86dcb754ea6686b505d", + "proof": "44a1e99da7d501e496a31aeb91e76259d378d19b331743640b1380fbce09fc6d968a5fcbe4383a6d6982f770bd2a55a913bae258f9749727e07b2ee387899229f8cc0f6d73313163a832ad8f9e2690e7d5456c0b6238bf95ef2f14359fe0df6fe6d401826084e390c893ac83dacc734151db39988f868855165362f86272ed703e07b5eb236426758b77ad5e328edcf3bb0c9b369ac0b3429191a0b5dd299707e4e4a999b6b21523da549188e95f62fe24c62930893b4cf7d6cfc4c12cd0470f777a7d6e6ee5d9d54b32f788cc0d542496cce65ffec9ab9cd371b7561135f20f6e295a644b66737912e6d0660e5f14d045b3c998ef98f8e27992ec0130b24611008ff1e1054e2c772d50576f7d66b10be42b8b3715e8e76a0e39463334e90576a4f70a955c56af799fb98d3f060f52aa936235f81acbefd41d4e03b0743ec4384ef4382949425eba401c9598c49343c541f113eac9780df484402aac56151f4e6e234562e2fc636cebca7b4986326347e276fb11b1eec264e85751be01d0cc3b92f0e75fff353357c18f23177dcae928d3381d25dd75f5dc18d1bb940d055a03b8ad03574184fe29108214fb9050d251cde4b024ee563a8f2f8082573b4073530015f220b9766c0b380e058510414e3d1254086795832468316733ac3016b652969ecaa8ad616b6c55827076b61ccd08f38338ca845e390873525cee98c82a1d06d43f6c3e469d2fd36d690e961fc638cea3f1f060b5651aff56a309d7d8ad2c02800b173884d1f7f211041ab8350a0588e758fa1285e50857505961bb54701b509301348bd85ea835ed32edf9081853d024d052844a9d5e193130324d3981484a155f283576cfb6438a2157133376471ff0612a7c6afccda13f1831e9d9240977374c69fb35fde4e3dc2edf1bff2b5893890f68a03244209dcdc4b044c1f00f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "185b1feaaaa66cd3c65417dfb88c0f7ebf2d5a1a3ca095bd9f5f2066c59c145c", + "proof": "ba64c946aa336508170489f490ef17030e55af8ac2737e1947c7524d97279d34f83eeb1889ff2a6ec7535e4489d41720c325a0e73a28d559417f403ff8a5a3029a2e8295d2aa87a383929dd03a3ef40b102a9d3638b132424b9f1a33b5cb630eb0e4447236cd91104da1ba712af36c4bdad0760caad2d2a7b282cbbbc7a1b00788d96d0e1deb9cd8cb42f45fad4895b84e12d04949522127eb4ca3474bbf090e2ed86fa774841fd2b1a43efbfeb1b4a004fba98f30fa1a17b406eadcb186f303c61b677ace96ce8b107a43a5c62f6f29dc5bf2c0c307fffff0110c051a9ed00ae4f731fe0ec2fd574439bcaa8f235d2069f7290c4f78f696f8e42014acb60d18fe32c2f6084b50fc04fc1991445bd453dbb46474bfa7c14c1bc2d3020af32c15fe0b12a9632d7c3e8a7c3ed075863dab1176a7183ff9ff4ba83a053d05957b0cb8205ba69d4ba7f0417839112a6828f7d2f55040283fe087287406e042bfd07718938daaa8766ed79a9e8d840a3098098a1b8b25ea45d93db5f796b1f38fd47054cc0c6aaf247710ec01eef55409eb7f830c13d31256481ae2ca779727a9f6799215f37b0283bdf7965050818f367ef3938fad4efc99da9cd8a1a3caa8247620ce80700c47d2cc674a51634e2b08fd17382e7b8dcfd3db40e2c8a85811e7161410f88899294972fee149d3fe8081da83b60a2ce9ed87c576c62f8a4a9251be4696a9430a4e333c57cf3e6f2dc4a931e716fdaad20a1cd0aff54c8d2ed9efda38fcea8877b746523b4ba63b6a2613bd0537ae4904b4bdf35835dce1893365222cc00712e9836e742dad6c4a413b7ec793bb038a7badf66784970edb9bd8651154b1ed4fc83c4f529accb3952e9ea18eede00a2800f236c7567431e2f9070e600f12a2c80a2d7372e85b77fcaac28ae2ad5d7f49b6ac5bfe980d8ffee587090a0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1a9f95849765becf792cd1ee44498add853ce125315e7c3cffd9a244b4669b03", + "proof": "04acb9801f6bd53686dc2117df34f4d39a8abba8a9b8a74219e39b83049e8a419cea358c7fc9926beeecdc581a1871ebb4e847255c66c50f31c23382a6442528c0f4954f1f0502f6fcaf434e8200e204e95be40f87f77f609d0b28b9a55de01c185f63a7f58312c20acf2eeb44e25e61c006530853411207e3b791bdb72f392c1f01bbc461dbcc46e974b65f40a3e0e17fad4d69061bd75379455888ec6c560bbaae26315a93a400fc9cada6231ecf99f317d2c61560a7ae52258b1c0318ca0f8c81940f78098ce92c1316a20071be2d3e30caca4ac6a438c79f57ffe0365f08e4d1468e8ff8ef9ac3057056cb6071f9223adb5ac5b44af76eb792cf8899ef1bbab9b1de942776d67cd94b8d9751d8d65e89d867e93d9dbc37f9f5438bdce96586263968831086f06d91cef3401ec3918c27dfb6d7621f8da06fe3e076d52968c48e05d9069a0265b5146042c4cedd1cdddec25af5c62fb85d84be08fe950534362e1eb56ffbbbbac4b9874518bdce6b82e193a48559cd63fce108cdc4638e3feadfc4bfeaae3463ba8d934fcf730677f6c1b03d888663540f090d92f4d1027b8274b64ae0c923a0f1806118a1163925a60518d916460e8bad9f300885990c417cd378d03996caa6cfeaaf90de5a0b78592c7d8cfec79dff92ba6cebe48a3e3d302b8b6003da02c4b9a2063fa19afd20cc3724832a6447ae92f1a6b35db1ee79dcbfd71edf2ed1c215faa46e3538ee399142ea27a74f87b9780e5a08f918d836bef8788171715979bac6596b2647e0765e62ee905c8726a04d44e6d491f6f073b40941590d1f70043e275d4752edb984d538228f48f0a652d2e510478276602dd253c1a39f442ddc2c686454c10d871d0fd4a9e92d7cd5e63655b01f2c27ab096f662193f9bae537233ef887554cbb00ea876e52b02b37494176d3f0e39e1c05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "28697eaae0490f7c58c6ae24ad8cf20f3fe3fc77458c34bc97a5d50df0190f5c", + "proof": "b0941e902c4ddca6daa0bc6ae4e4d8375f911c2d4de81434040da2a1cc1d9b5b3c69a95862f8c249d808d9aea3c6497de45a2873bda1ca40fae469e12604ba1ec89307d2c4fbeb5bdeb3bd01419dfb63cfe977a0cae7d341da7c44d8a9b9057236f598c7294b00c1267bfabcfcadd508542a47e768559adcf53cdf3ca373903358db8e91a8c8ee767364cb6d05cd90eabfb86efc3a0d215a49aa2145f7d4180d6ea5f8d3fbd32cc155c9679b55e17d57031fe7fbaaf3fd3438bfe32bf6238108aa03d48533334009dfdae496946eeb7f76ffb9d217e851642a920ee8d576ee04ca94845da942aa446874cdab9c44469e745c7a88b119622a60a7e5d46833495f3eb8eb9fe904371c421aa1b1cdf6774fe62abcedea2c56afe29d42d190e8a271fe153c092259b49ecb7f3a3b1f131ac0429ce1146bb0e99a8a91f0f2702b6d3a0c8af91cc60e0aace8877fac2cd2d5a66ae2886df2fc47d6e53e72ddfd74da38d629d98ceaf2edc8939e4d8834b5489ee900754a94b8d60529c821dafe24437cb8637456886fd7e03e3dc2eb836adc83a1bea92ac713f10b9f9728707a7eaf7ca21008de8d99d377986b7bf8b93934fb63ff31f66ad837d8411e28475f790d3bea700eb3316d63be7c5ef5037fbeee3332a368be902419acd72a13cc200cba6a94391def16aa3cb7e6ec8c151806ee6adea62deefbd008e0c1b34119f04b6967547c61ac76b10c86593d963e17bf358e07e6898cab35519e1c5155e9b66eb5709a5a7652122f271f61c2e590622e0bd5cc787e27d561a6f38e9b581ecf3bdf19066d9492fca80a9a009b5f750ef469ad6ebc4cfcea6dbc70902e8d3039966e12982c67b4dbaab630950e0411c34b3f682cfd896c706a3006894c130c7ce06e0a8259443d5e155ae509fbe42cdc2ab899c9784e4b0ceee7d0571c589dea959808" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4636403e07bb0239324a104ee11e95bfbace92654c14b1d947da9ea736ac7710", + "proof": "b2101939cd268cde81b61c1c8e7fffff96b9fd9fb94a82c7858879438cec7564ea30774d40b07d1f07d6841cc426d21ce39b156526596a1a80dfb7ce35f4f94dc4e39977234290253577b12a5dc36c5ccc4fa70d42a60a8230e289aa1371f63b4880c936e910f88b602b4dfef750e451c38a34f19bef1bb3319d47e82ffc8e207b3be3e42bf4d3a45a8f3b23d4113b39c0c7a25d686b71ed1d869410b2537b04ad5876dc5350da6867de9e007cf2e57098d3f970a2c02a4f93d574d6f1f7e50c39a952c809435b7708918b54b8cfc932ffd87395510eb40ee4b3b99e59e9bb01c8185a217b962a337e9bd3f1e10c63cb2bc5b4f960d2862c503ab2b5dd04340588dc33c69bfc0cd4b90aea5b08e4cad5f9c150580ee458b88e1930968b820f7ada59272be38ce30be1450708da0b0e99ffc19302dd0b84ba14be5efe4cd403416cc477396214d86e0f623c93d08acc3e8a1e9c99c3fabbde71165bece39b191fc8095b8346fe657211d80b66073f0a9aa560f92dd99b70a1e6e3e34f0c85a3152e82de0224f2d0ffe6fe86174cfda1a71817662453c9cc017bf3081963b69e2820759eae733223532b422c818931143223b6643c3db5f53257772cb0f6551e47d4068354ae55f34dd4726686c247df52c78ee8706fb59b165ff7651f0caea31792f07656f175da996a8296803005b33d330fa7fb8be80472ebce12848cb0062da022181d96ea0fd5c264ec57eef969a8bbcb06bbf4a37637803691558db9803734db242caea8b130cbb04adf9632c82a0b10158ec2b11c1c522c3f4cbccd452d981f04dbc4362982890b22b43c9d274b5982bcf078825281e4057b9e96fad86f626e4ce11e70876c90a2d982b80dd6719b692d3b4a98679a45f355dadd1cfc0bdc52c06ddc3a9d5ea088a05d5869f9e69686e63fd44c1b4d5853a907ff73d702" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4a70a1e7b566213908ad238364250970ea3222cb3f59ed3ca1f15fd160595378", + "proof": "ea0682fc51e803e569d6f82378996b908940812e2abffcdf5832d34d367a276d7086e2917540240d492dd37987df6d115cb426264461e471a56a844b49c59518fc3309ab0c29d632731ca987baeda4129fb7567684e8a9dcb03355c81b73544984e5b8501f1784aa621e07e53c570eb1b671f412715e266c84d76ff5edd7fc538a15499386c8bc981091ef0fe82bc79174d76de00f8f6b7fc2c622a00fe28009c5a9d7c33523dccb17e2e6d463f0c9edadca61c28898865429a0ac5201384808040445842ea859a78fb414b26fb023bce75aeee039128cb61b5f4a013e163f04f80047d5e4302c7f41fe28ef107705ab2532ae57397f4418e64adb35cbb05d5bec0fc411563c47d1688a10ae798dcb3d3356ec1e5da9cd12c86b468db37b98602ac17edf554f5706253a6b84bf05571a725220e21bbd2a7be8849389df71773e2236f22c943c6c1b2398a04b993016c329dba4945807f68a06c915926a43d60296efaffd792af721de38f0b6f1149d72cd0a541c3a569a115fb244da6b09c8381233f1b7e8c6f99e2b91d4f60bfcaab7d127870b7185f2d63d9daf84eda3eb6efeaace20d97bade0404f5de024f0a0d09b88a62d2ac3f81002bc8de8cfb54e163cc0704a7df12a01e6ebb61584a7a56df7c8b0618bb74618d70e8171b3d61a20be1f3fc789251c64d45a8144a91e1a2ac7fefade3e357f9bdc7bbd3852d7b95c5cc4717763574278cdb32c92cb5da80948343756c3efc6d74bac662c04e1fe1bf62f76c9828daa4f34b0c36502e9e6d66eef0d2635f911cb2e19ea14d23d6e3da2657908096e12feeb2cd11c379b0356b6db00d834358b392831fee41eaa5809f4b2dad60b99507e22ec0c60fa6001c2edd5044d77bf9bcc165e4087563c7e0ccbf04a08b2173a313996c9f7944500660054b56d357379e2c6f7e3d61d41d10d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5015a7389aa4a84f240c4652f2388851cab92ae292a2e9fdda5a1eed97dbc02c", + "proof": "b8d71faf2f99ea2f64880d5419b38070a615652ec58400b73eadce3255a1d56958696e6429aae73b5b82ee62dfdcd48184be8f4fb2c7d21ce3a6a8ba43185e14c4fa1aa3e78626f7c55f9dc047b3ab6b82339bedd2f02e1ea1a6246cace5d624f0e43d7e845b8b4a03fba20a9c5fd221095970ae6b59c5d1cbb860c8c7cc65781a74bd7ec45c9e2160b73a9e21eec68b1adb3adf29b18ed7f639e5bca38d95092031a0247524dd7f2d76e8424d2453519e61d52efea7be25a6c0a2ce94f0900c7cde170d471f44616ebcfe71bc420f7aa7d6a1aa189a0efa045d9e5620dc9d0402b36343f745b5b6af8e05d1a15af6dcb000a6321cf474f498f494d5538a77258a96eba2a5e32bdb803a323270232568786d56c0f4758605c7dfe13bca14d91c80353051ea0082efa7b6b47ee54cb31ae47fd4a1dcb69775a9675537ad16083bd61086496d71246c6fe701b7cd2070dc7ad793d5a457abff93174c08d618873722976962eecee2e14d3164b14cd44974f56fc1d20a767541f6254203c3609508a6188499745c006f356f04b813e40ab231c5a39fed5b8242c179f4324d5ac14090c0a911bd676bedf4231282a16681aa610fdbcdfe845ed0036a8b8abb08d71a1a34f1157d1a226a01ec53162ed777f7b9aa8451505ef02df317470f7e51b9534c06513925ccfb1f1a96c27ef2b600442e94ce311f2643439b827ed978654064e0589c8a21f912ab5bf1556f2b314a02ffa737abcd60c50f4e3c147646e643355ae735cb8ebab24e03ce342096e3491de980e1d455e4b4f5142300ac6f874302129d345881badf169a5bc8260527fc85ece9fd18ac9aa71cc61b1ad338c2af21a6f1a421077d12da27764717e7df02f13403449f80d0ef2b924cca2bd107700f52326f6527f129c40d8a1e6346afa1584ea01425be7601e0204287cff12ad50d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5cc69f2c639d4e75570f1577bcf5bd058a7dbacdef1405efd44a3196bc34045e", + "proof": "eaf1c9a4405b0bb22b649e71489ee20fa3efe4e9348e6076144a48232da0064bc2639a14d0d18f15fb9750679856d1e6447ba4df69dc4b64bbce797cfd17796adc2e1465e9017d58779ae58c6bec58d520e20eae424f87f8bc6363c0436ad96494b7ea7842136d8352810a8e3784d3b1695067d8d5c323a663af3456bdb1747d5e9b7cb9d7759f91ae7ae0bb49d0ea7bb4b2f885a5240ddc8a6a355c14de370a5b985039e7b7e57f8df6bad4d0ad3c42a4f490644b40c9e6f008a099ef1d9e0018c78b3a562904ba7028f78f55c1b40eb21386392d202a004560faf4ba275206fa7a95a994c211d10ec953e9cdbd8e9c28747536a82110381c2ac78322b2d6788c1653fda9463cb45f8b1e11054db5278ed00c84558c3714f553aa491209e56208eeff91fa4bb528a083a85c3862e5ab8692fb6c505b3e1af6911781ee188a41a8679ec52eeac02c71ef66e8ed8db3f307c3445bbad4e3d508fddeeb56448649c26a6b88b51c90e6370b2e9f88fe8859dabf8d6e1b49f881d66f1f1590edab26a0e1c7727dee06603c5faff617b8b1636676dad7fe36c4baf9a96de3c6b5dd7f46dfbba969f1eb3089f9487534ecee0ebf019a76e95cedaaf789babb41296d62b2ff65d8debb3f890f216707773454c5b7bb6df877646fd84ffb5fcaba43e62bc4bb2c01432e9a19fdb141ad7336fff08b69be729d17818a93ca852ae4ddb63df8d44971646fd683dfb46953adb36ba6a1277926d1bd92f1fa223c44187fa512b6edfbf58f4ccf6ce0a25b2de3a0f22f9822dfd531251f0938f6298dca9b547916a8c4ee3f1206bb891fa80f5a2561339709834a6e9c6673358566adcd98606498fd64fbde504867a78f7823c3a0f594c7e8bebebd07d86c3995c20db6be150cecf44f145e22fbf92361d99ddfb20db47188b98be7cea89c2d88f74e9a4ab10b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9ef1ba9a7f67dc32dc6a757cc084a8d8cc2d8e96b70162272656f1d17b877769", + "proof": "0c75c53e852385bfe5453254a7ee5f802a717d5ca5876dce5b0a2598cb5d4d1eba5083f6597bae2fd8f1347f73e41fc36abc1afe38d59d127c9b3427e1a9be0b16fc32407bd6cd61dbe34dfd81fa4714ed4b7aa90e422559f4821b8f7f727b6f0ac60853d9e5bc8666250890b1509f7ad30234860b61d8cba64a468656888040fa5f53382ea280f11099eb2f731256cc67313b856f6e629ad94a3ee39bbe63063b3d3961212e66d3c2ede27a52bf1fd091b63e1edcbc08d22d7ce86f56959005e5c042583102f9c17d94791498db0abb463b5ab1ca5a26e72089e9302339be05e0145ae01dc059fa511df54b04b6b4927875663898e3070702e1072e81638a71dcb62cceb64ac87ba0b954e89e6ff267397b48c456b97859c9b7b58af89d0476d8eacc70c6e35d2b81a1083353c7a6c8519b1d438e7828a11b8ac1c4ddb3cd273e3e76274980ce51cc321c151c262d0f6ea6ac5959ba7dd142843cafdd87c238fa56fcf46ba9f08954a9cb20691c4e62e358b03ba349a57be2b5770fbb30922542a110976b6e703499decc8b7bf399b5f643c15423888ca04c97267b5ea5b4351e218038a6e4b94f8f445331690c35e9a2a77c36f29fcdb09c8a000ed7c45d57fea2022afc0f91946773583c94b895bbe9b6b7b3847fc9c92d0ab384947fff12aec91966bf50f097c93d48120166f27913f80087b75f7af10e97620809e1337a220648972ad5b2310d50a6000640ca87f7237ce8cf17e4021714fb306ea3700354b1e428fa1a8c32a5ee77a952b6a65311c5c815bd7da9df29e88eb7d30b3e5dc0f33dc310b18f60383b0a4b52ccb0b23b2f2089edfbf49265f4b819c902ab0177b81b231ffcb22e74635044fe962b382678e1bce5107bf89c663bf779b89609f8d8e80c6b7f62eefc8d3f405d016f521d8eb6e71db7e1d4a0d1432b4579a90e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "be760657ec40f2073702f676f04f1715a3ba61e38db599b8955ab9e843626242", + "proof": "ee81dbe5288d6acf405aeb42d4e0cda426bf3aa508485bdbfdf6d575c2a2c1166e0b2187f4bb8759aeec3be79d2107dcd117c1c0c435ee7fd74f64814c029602c6a87f3cfc52ce83eec070636095e83552cdfb75d565e837a4dd5b854ab84249088a53abb41830f8cc4621bcc942cc2241e45aa01a138c28093f26d0608e752c733d187dee05fd2e695083cef9b1e0f0b6ab5ec3a91d96b308dd947cc1492101948344f008350a614ec351ff1b1533358b1d31ed9979d2bd27dfadb60c401c0f7f333090cadd7047b9831372fd3e1b02a6dfad64e2574f8edb5852503b020b0d28dd9e8e87be4581700227cb74c7035fca7e85945f03ee8e4b7e1642acbf20207c824618af88b3fb13884c049b65ea79a7be65c71473e7a42331951b3d2e4a603a2b5cc3a0b58a0b29da4184fffd695f3dc504a1dd6d16f6f7fd89f9b6e835419a8aed35101b58965cfce521921aef73daeb3165f3a611ee4cace81fd131bf78ca40060524229b6c7cccffe585d0e0ef060295930e7323e921ed7dcee94d0d6b4a68d2faf4b61bd855250528791c0f1ad44ef851842608ef0cbda530dd10dd49e8811c3cdb579921d5db89886a99fb292c27e0b3928085ef954c89ce7d5e221c069773c52ca036c9560923d3dc7f037a1020e3c420cc95435618fcc2af9d4c163ce656fe262e622eb35e8be30664a38980c13a976899faef8e0ccea9ed7dc231568aecfdcd1809dfec205ea3eca9f9705c26a93e48629a2689033620f4651a1bba0920665d3846698442e2e2326e719189fc810d208ec7c29d864ca9af939c3dc2d31ba87b1dbf4c0bce0a30703c9450f4c3bbb132402fafaf9f057e467a5d68b6115585b0dcfb6cb9a586292c73d57a5442587916c23a067616402f7480920b1d0f3dd6a7498c6331c5801657528acdccb79a839f127eb6980777ab76fe140c" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 37 + }, + "commitment": "06d1a9876546ec85626ca7fe974602e6500358ddd45b42b1cf100825bb4b5a3f", + "proof": "7485be3d3784c6ff4a3a449afeed2696cc54d92b4d4ed5ca5de0353d0e6e6e6278082cbdad2c536987877635d9569c2319fc6d4b092c3d9f665e03e65c444507b2d41e584bf05530a3bd01c3f5bd12dde68c36d7201dd329818d4307ac8294065cb167e570a39fd88d0938d30b91581c3471d209431131b0ab767003be752e29639637e216d6ca55ca643531fb9b03f1ef61c08ec96af627739a2cb02589c6007b2effcfa661ad9ee6b91d9864716e51ec51ce7d96684e707f0c0f7f9208190e90d0ec112ac6d9e3385d6fd35c3badf25b0b3c9bd77800225ae8dc50acacaf0a0c883897ffcb6cd8671fc5be18474019a5b766732b3aa927ba19daaae874bd2b68c732adf7f58692a9ed57435b85cbb78459a3bcba12a659f6a14de979949d374ccd1c8ea56ce19f636004377c4d80eee882ab0192addbe359e66d9cadf9e14c8aaa85b8dc5af1e119618beeda5ebd3deb543831a43122fb2ded5ee53544c27ba21d42a917c4588716d93a086abef9ddc20abb0dc07fd7f7a708c79e7f95e91b6098091ed90b54dfb91d2ccc1e88ff135e01c36513e8eb8d74fea43b01bdc213f47362cd95d901aee5bc091d8b6762ed7a7ac0fb35cb8b7604ff923f03f6f85b984cc11aad9ede0ee2e48adc56833f1b6badde0ae2dc1534169208770611df3d08f91968ead2ae4e20e976591ce763d86e49d8ddd951542c1a2d98f5edf96f750064575a216e3e09cbc6dcdd098a2c019fd5bab2e892c52c073bbd6665760e375e7ea57a5cccdd08593b1c6982b1e77e1977942e3ed3cb8fdee2e6e0d491aa1b205bc799604fdb34e176bd5d405c562bf83d184daf9222708165611cd25e6f0a761c68aced57a1590026e31e0a9d26ab52d7b00e0c563d39b5d845ec966c55080c9d9b3d7a7c531dee0465893d62661b9a128c8f4bd5ebd75496d144ce2de906" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "0e01f254b6b1e2ad1497309e4ec546c8f35d22b61564725bcc6c6b93a2c67205", + "excess_sig": { + "public_nonce": "d8946681204012b237fbf6509f47ce0a5ed8a184e1f55c237dbd151e4226e701", + "signature": "9b483439912446bce936dbcd9cd3b747577d962dd9cb3e89aa6c087501ddcc0a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "386a325f9afc2b2644ea18ed4569ecaad9e5b9904c74ad4b96e1ee8315d1577f", + "excess_sig": { + "public_nonce": "5a062ee79f2a25cc0532af033609818783c040548b3966a8b1ca767fab74fe48", + "signature": "1d6d919e8c191cc9c629b5ba7407b754a911593eb1c4713e75fa64cc7be00109" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "60123616b7f705a62d05c248a144e2d620798e63d55a40b826adc2985b70fe05", + "excess_sig": { + "public_nonce": "e626f7f7d746319aea3e9950dd565cbfef7d9daba8c8344d2261dea8985dba65", + "signature": "6a76aafcac9b74dda7c46206251ace76912a6ea3af724e0f2176fdec219ace0f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "aa7a26295864cab1c7124b69a255cf3fea30673efcd8c655739c10482690fd1e", + "excess_sig": { + "public_nonce": "64acd0a76cb22008c34679faea34617be67ed887e1fc5d5ce5b185cc4409775c", + "signature": "da90c4c690df664aff8159cd34903bc93bffa5d40ea6700cdc7ac9aed30c9407" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "fe4bf8df481d7385998a9b557ae1c5821a3cb57d39d25dec164a2793e5999246", + "excess_sig": { + "public_nonce": "78760d4a14beffce9b8b646103f19511311151ff7f37304520e89d88c6114367", + "signature": "e67a65963a61795a4257eb44f6da2acf878f32255626b18c68752efdcdd8ce06" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "8472f71e935b3beed2049761ff424a62e8f6e4704b14ac535cf57cc5fa2ab342", + "excess_sig": { + "public_nonce": "e45e68c46402226c31da05d373ff566f5da1f83115cacc5bc3442eec73d64c55", + "signature": "ebd7a7b4c0108efdd2cc378ed156bc6d2043cec3f68261831adaa8f727bac40c" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 37, + "prev_hash": "c1880dda03841fd04f0b893cdf7f450bda5bb58524a9e692134870a01f3f5167", + "timestamp": "2000-01-01T01:38:01Z", + "output_mr": "7d65c1c615d4e873a6eb01c7328e7baab216f4de3bd9b6a5c056f6fb0abc435f", + "range_proof_mr": "9fe25fb7e8abfe30563e6f66ec90ecf7555ee61e2df1401f7c38e874493c4a3a", + "kernel_mr": "d37c46a24606f10509f5f9c230416aefd690c020c10ba927c7f613a107f76b73", + "total_kernel_offset": "754d0a9431e78d0dea1cb382474db0e52a6bce5861014808a263401736940305", + "pow": { + "work": 37 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0ae457336d33c0ed8a3549c81b3de67f21213379c3cbb86dcb754ea6686b505d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1a9f95849765becf792cd1ee44498add853ce125315e7c3cffd9a244b4669b03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4636403e07bb0239324a104ee11e95bfbace92654c14b1d947da9ea736ac7710" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5cc69f2c639d4e75570f1577bcf5bd058a7dbacdef1405efd44a3196bc34045e" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 19 + }, + "commitment": "68b067c55997156efef9d3f223669d5a4e922511f261b41abf4e3f99d9a0322c" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1632f3355c53afb55c8f624479e7b8bb1aa890fe61764bed35eda330e5831e6f", + "proof": "48b7e99964a0687d1418fabbb43fd5e62e2e3a3c8fe04cdebb720eee0c50c957622b37113e9c3142514137ee4329b70ecfca746233c67864e21d97d1aafaca6138ec05ffccd024dd617aa2ed3c574addbed4a80b1b88dacd3594b30d8a73a618eee54cf715e10cfe1c803f0ee6369873dfd64d4622c4903eb1553dffaf011036f9ad011ce2382e1e654a62508254a8526967793917d185ca0202d86d03dc9c0eea29e080a51737774b5157b5c45c077d4f1dc2137a8914dc808c1c12f5434c0a469329957cc4e74a764bbf17305dc85cccacdace22b83c9ff61709db6437570d1a472dbe4b180dec969f24bceebf7c8be2f70c7746eae7c4354517bb42593936da7fd3a135db658a268f1754b8bc71755435f47a140f8b6b61dd8c790623da0464c3a19a9f30476e1845e8ecaeccb299731adafaefc2487223381b14531b282b863eec3ef4dc0a5ff067a41ce9ba66a4ffa9bd718cec8c0f7dcb55ec004adc3ae82d967a36ddc8ba122b2ca86d5b99f208165848976f3c2ea53fe71d0d11dc75c412a9f15af91a9a2b7a4845e91656e5fcbe56f082b2cc8ebf84852d14fc9b611cb0c56d70970132fff2d4ac10a506ad02f72e97d4541dbf92b23a8f913429368861c411f68759aac5299a4be85b654ff41e25cda91cf3f3ea300b3c558ff31688a622952feb41a49e5f32961c3afbe875a53a44c64fb0a848bbe48d8f6f9d0812a96ce738ded6dd6469eba7ce38180c86701969a4ab7c09786ee57cdbc8ca4e4cc24a245fca02975a23ef754c7a3ca5a8e166bfaa1d043598fd62140af5b64a929e43867627eccb95b5180a4bf923958c295e7882e77b5c3ba0625950a9a41065c429cb1f83582f39c2b7412f26d7fd3fc64746b296443cccf51c650da5a10ee85d05303964a0d7db035023e621df605fe47dfd9eec5e24fee2e5a8ff471b03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "22ea1a31438c3c53767f80f798386f8708f65680cb72fdbd5e2239e5d3a33f7d", + "proof": "2a76512c2556b926b277bf86a3fb80f475ea29b47ad1ac31281fc4d707c12522e2b4aad23b2b79a7458d64c3947c2a64d785fe4f298d5b7b54dc22c3cb29627c6646f0afcac757a90b4d86908054203b9c134bf8eb19ca50f6da746ddfd0995df0a995f41cfd07aacdf40430ce996b0c3051690b910de14ea6ed7c0a4bf96152e4a82e93bf63dd0df3490ec0355f5099954afaa11f35e241895b8248b418f20f0e0055da03d1542e172fb0582d71c6953eeba2379f0112ac8b45c4e0a374b001e6aac316e7c3938b64b9f498a3626fc0bb7eb1698c4204f6fd02718619d46f00cee9ee19f92fdf4b2d81f4aec88d47ec4d0834b98dab4057e65e53eea76cb737602aedbdfd46dbd2ffad46a9eadc581a3380856a163ecdc16c8fa3c85d6f902baa7a5fd08b7f20fce894de447caf8d759f8571270478ec9d6f8fff68b39dcc391e3a994c8ecfd8f5d473f84ac9007dea05c6fcfb9f1d1dc7db1482c1b2eefb74bed263eeef090f06b1118611f32f009991c55b53e558248ae645140ae17f4f1d907bef117e741bb8c76fb3884863d62c3b8f12a4342ca991055b42b94c5844368e5a85c42f0b467281e4d6b580c2f1ee582b46739b6a69c06bf390b00b9dab258ad23e4eda811d806f464145c326342688f86d920d86ab15acb6d1bc79661a752a35e4b7a720d8a4653df0e8a1ab51dc85b5908d836f89dedb6aaf33ad63061c4a8da1c8e41320ca583525941be946554e6ab2b305674f390102d253214e7b724c4c87df682e027fde750dbe72ec268f8d68f4be4bf6e794323fea7017dc5c117c8b00d42bfc3350b8c8338ed821c974914e409c65a1f72771677fefff28f4295dec0d0dbe358bd8718b651932c8a1e35f1f39219c5a05f071d739969996a80895aa3607d5145ccadec7e82e985a1a0b2d6fa1f3a1c69f27e04d82337a130700" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2cf15b00d3071299d70a23842414576ff5a0d3dae253e5022ad2107be942c77c", + "proof": "d87d74d9e1a95085b6fe688e7273e5fa77da9c1504aded739560cca5376ab736a49935fac2098a45009f3a4de3a2351a5fa0c99af7d80532a65b29ef5495647d1c545c2c91e3eb5e09da99f8d66973e6ecfe544250e9a951f9d21d412774ed0c1a44cba472c7fa91c6b2c246051f2bc102a682c355d394b428bc0d7edb083b08e450d8794d84f8c6436dc365f3ec3a0a098d9418d8f21bf03a38be00b5eaee04df060269595ed8b470d2a4bdacc85d7f3704eed09b2cfbd4eebdec8ec58ae50ebbeb65143f81c68b8d68c54fc1f3e8958519c3f043f951102193254df7d57e04b874973c703ac80c24d3c7e7426249088af5927faa53d904fa40f4f0d85db41826a5219cb4a77e269c0a921d4979dbb77fa9f3d346da536cd5ea9f594ab6e64e6630efe550500ca0849dc75d6c5e38b2187baa49cace0152ed241040516bc7475cd59128ebd2cae046df2775a0d1465dd25ea3d2b57d4ae10db807be801dc8358ecbfd0b2cff44b911df68f5a1b4cabe512ff90a5856de4f445e71f447007658a4aa60e04b8876fce6638dc5b297142aa5940571fffb5e4516e0c7fbf0d8ac58540aa2f486246d744ab3c15ad951e3963cd61d1b1256716e3bc7d773b761324ba8de5a63ee131000ca3622d2e472d9badbb0dd5e30b72c1b3029e8a867e61370288f2e3d2a0fbe16eaaca4ef88d2a61684363cd7d40931240428f7fc848db62b96832dd23c68111f085105dc07b2fa279507b7d9a80f0c4e26367daea40fd74468c337c598ef1bc589bdb3a92381983d62237030c3a05f0004d3818cd80b36766a14743b828b927b180b611aaed3452f96b3062478cd5e14ace25244301e4154983315de427d5ad1dba881bc43a4c90798488a55b24bcb5ee9edf0a845d1a8059ef4ee6ffe13b393178d00e38497242fbd8ba6a2638e4bb6714932c7dc942e05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "382f72ee4bea061553405ced579816164d117d289ce0b532826fc35a24ea6b0c", + "proof": "ce2bfbc288bab717819c2a1338450aa6b63624ed62fbc607505d9538a5826c37c24348b90f13fcf82d27d91d72c11adf6e7b81c641cf313a28fa8e7f267efb31c8e972bb1840458aab09735b17e75d47e473937ad04b7e439f8da98fe946b666de5ea54cfb41ddd909e385873e6f7b2ad054a0c66582d10a3439db218bc3254e6658c4e23104642886f81e6db68063c154b70782c88479172d1e98926c0dad042f6192d27f36af6b06df76ef3db0755d009c430f0597432193e21be39b549e02c1c5b383430cdb7bcd63fc8b00b851799445a0964afca411b02b2db66fef330dda07fe513f2dc12b36ea694dd90f031d22f08a813b5c9a72b76740c4661e751ba8bc1d504d5f41f99282dd0f50fa4bd6dd7c0c9fb66037fb08efa10baa686762b05bc16cb6642d75bd3d24af9143299fd5919ea95db2459f8a2cf1078db4cd655e2b24de8b2c73ed066d978d565a1e5f8ce9b64991aa83d8762d605f333f9d4530848427a3aee24633b3b8d567752117aade90ac7381ea8b8621119b2d0c91393ea389415a3ca578cc5e703218ec64816a63e9035c865170f40bc067c9d5d27ad6300bb5fcee42baefcff3356368917185ed752e25a49fd71a83b79a2a1c4e0808314f5003f96b6bc7f978e3e80a3a46e674da979f98a54d84e2758e9fabb22404551c20c0a8362c29116dee40c0ac540cc29fc94d733014bd5bec47009861265cce192b4265e7b2b0b11f2d014673adde1bc8d31ca28c42c2487cf51cd5146a6efce3572c2a6bcc2df63854953d949e9c47c796b421d5cf4beaea79c9d1ca34922991be1faa1fac8be72f11341715a5a1182d97115be50b176e674c26561a2d924a04b33fd09a4c98a84b982b0868fe5b4346213b74cbd84376bf6ebe690f03c3e86963d265e6c15516b4d19c2307b7e49dcb2fe92eb335d31198dfeacc420b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3ad0a9fd3a0ac3421060d861872f8f17edb645d55d46b31a8a2a0363a6462b21", + "proof": "de9b2e3aba86d80b9a51649a1cfae99536e6da3c679e32eaee244a32d192ba7998732d5cae18db381320ac6fc138a2c7d7601bb426aaad22802a8b0a5bbfea173e49d2f7d07774adfd9986f4d5e3343c1b0e08e925971ccaf12366077f2cd2750627819df6a80638a7b972887b4935ad37b2496374eff74aa1a1a98649a0ec39afbe37b19eb3b97baaa26af8b983efed90ed18df6e7b3cd6ea5e20527adca60f9bbb89af53f346c41ceb98e566a1a54ff2986eeb53b56476add871b9a6fc2c06c3bd770a25b786eb2a6ac2de33042ae1a3140185e4fa58355972adeda217c90a1850c6e3b58e036cf92225f76eb558d08e0932db8a544d14244c9248cb56f558c2810c5b34d2efdbd54904efcf2f6c74642e8e73fe1e78c769ee5fee202bb97bccacfb31388df2bb5bab5a7090f951b3c48b3dcf15a0177187da5c81ceaff8537cd3864374f84ab5af8d467f791d328ccc01e21f7cc047dd485d90cb5c362e48dc7b1d00a50b9c28c075ed7630d8feaf8ee641983d734b4c0130218a2d0808752a22af71b3ed6349b122afc0456c3f58714f0875a48d96c8f083b22a5ae8435c260be4398bb3b303f91fcc3ddd29ce07664285c895beee81ccf77ef313112769ea673189982119c5ad87aea7e402e312ca9910fc92519b4a8d189d2ee7f4cb5a18e070a773f6d54101528b8fb4610f90241768daeab57f6caf69bee0eca46d2632c1618f00409e93c8098739a246ca352a366557e7664da9d9eaa46a3850e160c6da17ca090b505738321f7d727e77a8732bc653bccd5393bdcfd253041f9138c00e24a95a3aa898492b7d648d6e1b502d9e876ea2a97550a4b27ec7743e3e27551cc10c5f1caabf845e9e3b1879a8376dc0ee420a0b5de436a5ac8c5c12870ea8204aceafb0c940c1b1301d3b96cf02cef96a3240cfe0e93077918dad1a2308" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3e7ada2c2c931526105e41f42daf045ca827af75e0957bc55fb5710a9c505a53", + "proof": "428d3b1f90dd80be5f39caaeccdc774f37656a933485525557fba31d65d3373678f1f7ada9839b92576ef212ddbab9507a854c9dd00ea84ac95c84e633280346de6cb8b801bcd46f3ba90860fb32917d0d7f32daebc55024f221b4f35bcff91a1ec490350de1017186b3b3275b6e26c2a763cd723b2702d1d3707af8fc8af67b1aa2f29fd8592db0c1e69ce09be99e265fa35f44ee62575290899f9d41e3660c3c071df36449ce3904e910254555e13c04aff06fdd554c60c44f407cc40f020e8071324583ccafe47b2811a7804043a34fa702c7743d4656980ab1c45752d406e268e7a79e9e43f9ed0688357b487870ecd2cc1e34ca66dd55e18ea5ef7928537af1d49e26a0365791910137b1fb92433f5eab4065a2f1bd6b50d9e6ae2ea219e49dab3a8116375ce615adb4269d12b36396f3ccf8a039d4a4e7836dcb9fdf13eaf6ebc18e9997fa59945d9fa8e4efa338fc531787b5a4f50b2da7aa0e3254653cdc7983add941ca6201eb53eff9a7b06588bc55be4b9b73bfde80ef6f24ae45861a6c6309600003f4c6541307befa27ac7cb8208444e6a02e6ce9d9e5e98137b4bd6bcd0bb8dc0d6fef97c1149f08995115c1299dcd0679264e7e98a54b2f58bc77fab0a41b8303edfbd4dcdb6d66c5f816a9c298a06b11a3c693f47070f8250cd47dbde363b57ecba749ce2dbc196a9293616daee89aa2c948b5d7984bf94a4016238b02cf66433a20d95e24ef802fee09d0247ec825dff81f927f6c0334766a338e8f99dd28768ca18cc728c6bde2ed45665feb6839c358214e30e964323eea7e5cf11eaec7037e7eea1362fa34677d3c323591e26b95906420991ecb3a3d38c5fb69bd705393d6e19f7c3e70b16daf7e601c94a1bd613a3d238078f9390e64ef61c06915b38327eace8d11a65ea769b77386764cb3ab097a9f2058333a0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "64789b34e0aa9609b4fa2840608eecbc2180c2afc66f7479a269eb67e0996827", + "proof": "9aebecb85a9ca2b6be1b0662bdb363b1b3f25ca636e30fc1a3cb9aeb6cbe006e24f03fc24ed7253ff18d67d7e68f8e37cb7a5f5278b94e7768c34a12d406dc6b34a2e58d87462d1052bbfba121c04477ae6159ac4dca2c6f8972c194bb36cb48a0d415bd4ceeb56f17b9ba4680e51e393d7381a9a432e0eaf0f7a910b9c6e77d5ea761b738f896beb43a5f01b9c533141f3ca8eabaa724091e77ad19d9a7090c961f826dd10f6ef71f3d1aba4f9d5796c92e2c0a5cc0c15fbc25af6f3356ce0c563a044c6e038de79fe54787d45a97c1f1929e380fa8bdaffd27368935816007905eb6e42ef1d9d421a9bcd6fbf1af047b2cee433875341e195d088037987b4f526e3ca9eb3f1b1ec8728a44602c7312c20b76567554b625f4fd948e999de34ace782e43513d02c334bf230bd4802673838f3960e82ff86f953c5007d9ca387298cdbb6fcde7b2d59f6353bc9e11e5a7edc62d6383c029230a86b5168498c655dcc62c66124b0f873b29f90d7d6826b2576fd35ae945ae0361041460cd4bd025d4b7b290b5ed1fc25380dacee90fc34b986dd3e881ce758699c357e7af9608288ec0d27432c3eafec47bb8fa91afec4c63c4e42c71f4598cf651753b3c92342e4e6e5024659931d9c6668bb634be4a01b57ec95b9b5c9c0124bbaca074e681329cd05309635152978e45ad872c6c7da4f638f283f626396b2077afea48f5260210e20ff57781befc881c76bb780661c13375de1e94bf36ab7215c4c27524dd009cacee7c41573f58713d4b61ce0b6348e1cef385cd592101dd6487df311cb317607497e7e2076df08e275ecf09c6a63fa4f188e0f50aa86c17a178dbc3ef3e6e6caf134e8004703b02b39a03beacb725d221684f86e68b254684c42498905202ac8689dd0545154a9bf5e237fa05da1c84ee8814fa187316e06f03e9bb5a7c09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "66fce19b230c7d86eca078e1e161b2a9d32c44a944f85c74bbe8458acc1ac335", + "proof": "bef9fb53ac6a0d59db2b15002a8044eeb4ad128df321669a39c8cb2ab847e477803d7a686a901e541ba3b4da87e9b5a3809a8b18f5503d22c8e2405555469209700ae2001a766ae765cf3e7b57d7e7bf12dcc4db0f6c1857e04a5b944b0fd96a7c83c3d3b06dc7419a1d1e1de7c01fb6b75825501f772fbb61354f7028a8014fea11d32f94398594a8da948ef48ae425638c4d0830c989d9d82c5c5736337c06a336f5f087e28fe21ba8bd3ee71aac31d5846076e837e49f8585bc5ce8abf0076889f9a0d20d003804768c48063ac2ba8cab309a11d3adc2bb7d8f17439d270998c025b1a44c091cdd31b93d0cadd61d6e86454f4e5b1e156a22c6e7b28ada1b3caa58dd4f6b2953f286e90f8b99ee90f803e81ce4af8c10d5cea754f2aba238d07dc29444d2bb3b1ed31c71bfbcbdbdfb184aa6913ac80258d4a6cca5c14a6d56835bedd6a9cfe9d4bd5d0952bf58dd99d43b962173af449301f3e52b15a5680ae065c6083b6a428874bd4a333b7339b9a5ad67f51998d379f1a5e91ecc585574ced65528adaaca7e8077c153ee6ba4d0e73975118a9102f5ab474ec00aba3288c42430387372be13ed6f728f63f58b9e33ccc9de115c779304f506c56c2908b4b0a81956965607ae6f2372fb70bbaeaca57ec010dc7f3e937754a89dc0dd4e1c1708ff1006eb332173a177b2c2a3b74f9e1922cfd525f6780f0d704c4e3d0f18589174160c4c0d358aeb7748096b5e1fda627028d7cf3e0c52e99d29ff770ae6c482a23c912ded04f908f75513edd3582f68c3fa6c59e67271c4c78f4491095602ba5f0fc0b83a3edbd34816bf48a496eae4e37477dafb4eaa6aadc1f27e367af36c4dcc1360befd5c1c3e92f92e3b0fc529997d38894909c6fe5e04416207eef3718dc6272de933d30fb160e9a18d774872de986f29800b12939f5634c808" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a02449f5b4a7a570e806cd889b5c2d1073b9bc65d771dd66b006b577258c1775", + "proof": "60996d1b76c494446b3492ddeafbdfd8863750024acdbfdae7362939c3fc7739ac4e8f59ca2e6b9b0e18a6eda34204cf1908e022ad6e8fe780826ca3c8e923252429a74fd591b59ef220e447030668e5235340bd7e38f21b631af8c55b3a2021964967cd3a7345d256966df80d711e5ed83c6256ddec0b46f7a4e40fc28bd9228f3300a904fd30c5d452ed1f6a37a0418f4e6c493f20324c4e229b8356d7f100abcd95eccdc482f405913cfd3a4a2ac8cfcc1a496384612dd8b55e9f807afb0ce04d5ab77a6b14d33fb44cc3ee3b1a66a388efa7033653643459374d54290c0800b0145a6a2facc8b6009cd146d0311138fecbdfcf101e43265281bdaf430d6406dc1b57eca30f2bef6fc12600e7ca7ccee1300abfb43b6e334d12c050f947640e15fb844d9f0acde9b57030e613011091baf8a870615f64e4b57a2916c4c97ca2c0312d7361728f27e1176eaf4b6e93b038f7784459618bc405140bf36d2a5cb6f8dfd4e62ef1f59726c1eb757732ae6ff56b0734235fe35c2ac85ac1697e6d6ead100e7a3ea2a07e1cfb2e1699018cfc44ed66d56cee9e545c69f1eb1e993de2adf8a8a94196d26c2ff35d8bc96b3059989ec11cda023798dc73aaaed639132aeeda8f87e3cedfeccfaca54be518fe8c4635414a6124be9025604f73ab566e5acb75acea545d4f48c562b394445c8bc8d94f415f9d7426dbfb0e6aabd7397f2a7310837794a4e715cc2c463fb9497caab66406db718c31a6788acbf0084578a89f5610cd6749829b3e9647876725269081a8d3507679b08c1ba3527b6b4f107064bc198c34ebd6c7e91206d352f2dfbcb6c803e5797f4d0a1c43c2b059fc2da1ef95c9df18f5a7941966acaabd87c2a7287e415a59ecec16e1f30b40ef850171a3f9c81e7eec1176bf3aee4350a8c2baaae6bfb6757968e9ace727cd584700" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e67da33d5b6c14c809d9421977991909b0985def18c358db891e8b3afc712854", + "proof": "2e82152e7614e706c3b04d78ff2b1c3efe7c6aacffc75b3d4e9de40aaa254c058423578b3996bb7c004d9568b09bf8c6e598e1bc6502f18f2a14f6d932d2b075acb6e5579d19f49cf92da03b9fddc331a0d44137444842884e0bf522dd45e944127bd3414d5e5e03246ba7cd030aae2efe3f16320a1b513a5a1ae98860735f7e72556a810ffdc15c0722a4e2342e17d64c6982c1615c8a0af03c75b9d7647f03f6310da1b9db2262954ec2e1f157ce9901e634f43c45a0fbbbc2fe1bdef5610f0402dfd4dc90e1a9977e5c02f9559f845752e8c42c9543538be5801e9df2020a2a8fa17465887f4c1fc4de30e1fa49ca541fa0a9e18f4f02e57c7920a42fb8116a4f639ef2dc159f38465fbad2e9425ad1d654683a39e0b54e439a419605f021b2c76d28f610b4a87963b6d963e9e2dc8c10f0d99ba409720694d82ef5be11717e370e3f516f5a6efc36390c5dde285dbd577aa7a510283ec6497ff9b4cca13d2609a36f44df72769af95f09bf7bbd18075b6fc131c3667924e24b7e6b87c34b0a62bad34dd5daab045a7ba2afd8a8602208879a516f9a34c62b68666ba17627a407f91a5c3625ddf4576b782e54aedb66cc6d35c546a44536b73511de4f242f9a3afe97daecb40f2aac1a19d69b52a77d24ec9041e09fec8301f526900273277af5bd946c599aaa5dec71eb4ffd7d07071445568391dea4dc9cbd1f6ded3652d0d05cb9bda5d832c7349a7818e2b71477af96ba410ca396aa3731a1103cff0898e7ad2259479e18004a86f534b27623423c028110a32907e62a1daaa630ee270a0e87f4aeead4c6497d4381e4bddc504cdf900ff1c4e2171151a4cee046e0095d8e8e8c3f6f4b8bc76df226441a46cf73adda0afae8e104c0b0c608d24fb1002efc5b1b230a3721e3d58200ce63c3b41592c36cb8a334d032b8920614759307" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 38 + }, + "commitment": "4a207ab981ef638ebbf69ef6f7fc686d8069e44a814c28d1eb86ceea9dec810a", + "proof": "c0563851de47dc4972070b4dd7535472074e06827629409fb011d02f25953e341484ce18ce7ba15f9c6824dd859a90c46d9b3a150042b2f93a473833e2eb022f3011ea387177f79b4e6f7ebe95389efce9aa1396d4355c413706b9105bdeb90d98e6dc2fdb69f80aa909ff46e558fbe1f88cec9644a9aabe200832d2c2e3a46e295aa2f83edc84d3f5bbbdc6918cf90f2332c9f02bb0f0ea31afb3b3c81667053aa274d930d729d9b31df20ae4eef329f4ae2a75e97a394be513bbb31b8f120de23cd92a8545454b39de96eba620802a7d04f1b2cad97782d26126c192d2c10cd272480ae14f59dc26e93a5d680aa942b54d6d82c4391c175c09dfe06fc5120820c47a3e9f3d864dd9d32e8e3f6cc7ed68cc3415b0eabb98472d1a65fc315902ca1ab282a417c967247048dd94ed87fe4ca34ab13b5b1f346323780817b25f356c271d7ab90c0008712dc3c0c04e2c44bca9913815a72e17db3bce309b03ed0b08b140f830cdd9600cef5525e37522ba6760b6b40512f999d928e3d214ef0c2fe6fb92db06c1d2e8c46d17d25c82bf10493385dc7b67f898cb120d8239f1200dac1c80ec9d1cc99d6964c18e2f3b049040d8289c4ac95475df4c7f1051253e7f807b7f2d7e73b37008fe9c3c5512b8b3004ec260bcd57517e8ed3331384a4c0e4c362405627b41b363b6d82c357da5a3396ba745492efec7769ba734d19b706144bc79ceee81b69ba4ae923f291c1599ac54eac47141abe3a72814fa0702c42ba2ec52914e6d27e1ac8b73d64fd658a5466b918003b3a6c7184279055709a013000fee6aae88c23dbd3d0e5ab37088c345b21e05f53c3f14a80780534386de7ca0c60e604e6546fcf2af4e64d2c769ca51c293d0db72b61209d038ce45a97005148c45c3603dd4661aeb903faa72968c6fd78384c2f865c2f88ccfd8c9a09b0c" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "4619b97711d5e07a9b02a14fb584427d626608a888cc4ac499f943b3cbd0ec32", + "excess_sig": { + "public_nonce": "5681efb9b8901bcdd5e7ca7ed7b4ca0c3d52e352430bf869230458015aede142", + "signature": "9e8748ee89c10ed805a3baaaab6486e7f840b6cfadfa0364bc0110d3cd00b70b" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "56c46ce6af4e80bf250a11eb62f56f828d681eee3e9b0e2da4943ff53f385565", + "excess_sig": { + "public_nonce": "28c962e677ee7be15194f7009aa40034b6eff0580780140ca255935ffa236f19", + "signature": "61aef566a8f85014e9f88baa42e795e5d832f8c2f7eadde5ff9e150380a08f0b" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "88ea445f0113b070f02ed6caea403f4be12a6e902d2e3b48dcc02a03d5656403", + "excess_sig": { + "public_nonce": "eec741ee317c348d898f0fff3bc393915929173f51920955c035472c7d070759", + "signature": "aeb50c341b621c25c0813ce33b9e19545afe397f84916adb8d95f95d31d10203" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "c0cc4685928a4ebfaf7b49977bded4987c67cd74d5329856597f5002bbddb76b", + "excess_sig": { + "public_nonce": "6010d4a5523a93be05e129896f4aeee6e4f74f408039ff6aeb61a82235c0f118", + "signature": "2cc41fb1f1d43467e1e7b16bd978f0404443c4266a016559fa317eeb3fd0c10a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "e65882ef2dc939b17ca24c3b6025ac75a1d0130814b973bea709cf8120139950", + "excess_sig": { + "public_nonce": "dae74800f9a8c82e91e251dd9810ef6254dab6a4d2ce57d9eae1463d8abdc834", + "signature": "392e7b9181d0c4968705f89e10110d7448d8b9803463ebef57ff21ba9830f40a" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "7eaf0635951aa552888f4029daaa9b00bd250a6598c724f7cd07712fe21bf36a", + "excess_sig": { + "public_nonce": "146c6cc58bb2fd2646ea4f697e43ce39f8995e320b0cf6e16ebd2b89313d2d24", + "signature": "5c9d011bfaf215b21a45f9e34611c0f8098b723b6fe4522cd6247872ed18090f" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 38, + "prev_hash": "002b7ebf00533120ddbf7b61eb76f3cd6a4a74624005703ed0a23575cf2f1861", + "timestamp": "2000-01-01T01:39:01Z", + "output_mr": "47bcc5c8f7a553d137ac08cd3bc7c14bf5856f12ef126a9461dfb6dc9f19a60d", + "range_proof_mr": "c617b8349a979d29664ae562ddff3c9648276af6c4546a787095003ed80e38f4", + "kernel_mr": "81220f1ea57406d1009c688d8d944c8214efdafab607dbb427192f3ef75d15fe", + "total_kernel_offset": "39d9c79ce0bd4c6267a089755cd49c90706e19be982c9138ff9b9c77e8cc8809", + "pow": { + "work": 38 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "382f72ee4bea061553405ced579816164d117d289ce0b532826fc35a24ea6b0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3ad0a9fd3a0ac3421060d861872f8f17edb645d55d46b31a8a2a0363a6462b21" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3e7ada2c2c931526105e41f42daf045ca827af75e0957bc55fb5710a9c505a53" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "64789b34e0aa9609b4fa2840608eecbc2180c2afc66f7479a269eb67e0996827" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e67da33d5b6c14c809d9421977991909b0985def18c358db891e8b3afc712854" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "169cf06e31baa5f5e1649a8fa3deeaa63da2248d0fb0840d5cbc6b78180e4574", + "proof": "9675cd44d6f53c19dc195c33c8fcaf35749609678d45ebbb3a281a8c4588d77280285822f0788ff6cd4dcbc441c2955af9df83d7948aadc816906928072c684cc6632b8db8c3d8efddede590bf1d2341390172d29cb80a2f22d79f6d5ec0b42b2ee463ea598be954ef5bb521a31bb15c74777db20439b5ce8735354081589359c59ce413a1acc0d199e3e75c43ccbc387669bb3ef535eb30ddab7a527494930d883c826bda2033c9bd333cf4841f86b353fc8f7ba836769a323af954d61c4a00ca5e862210bf82865e96c0ef37e7795fd37ae230959906ec044e3ad86ae31c0a706ced8f17fca117b0bb6c36baac777b566f24b9f6ae3227a7c65804773989333ae9919b4ac8731844bdebbacaa46343e1b20341faa433df1704f9117de9cb77a64f3811f95471dfa75fc7402959ca676d005df2b7effe10728aa2b3f826da27caac888560eaab976fd042f47ce0efc9655e1c3f87b751e6f5ba9c79d8525c462e354fbcd0a35b58495971c6b6f283f0330a09dddca58e118f5a6cc980dbcc34ee71ba09cbb76bf867414078a2a9eb157cd9a8698e5b5a27a20c45e918537317c414e2b3f01c7ca23bcd58111d18c44882c25a63abad0cf1ba4e6483a2defb39fea64a810952efa73b4602886b9f8a52561886f9560e36db62f8b1f335404602f0a79d3d184051319dd09db1c4e53ea9bb20df893c9a38246289fcab3b41cb55cebaf6464b251c44a679881963a9ac0bfe6738db1ea5d78e5741f4d6051fbe5128b9a4f1609591d45541159112b2758b6ab3b4bc3671fbe9bc4706b1e4fd9465b07318aa9991b7f194e7fd2b9d915913a4bae39540b0cbd0af932fc3b6b229532d1e28b114038a4891db6cca8a3c852f2847be88cdbb9cbcef47e7247f2ed707b304c0df805cc222e4bfccc787ea006abc6f0935c52faf7c2bc6d906c2a0560b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "18856f0c5d40bad05475fc47e0f15e3b96d9b2e3188a62436cc68c5ee5ddb850", + "proof": "d2f6a8990e5ff30bdc29f741223161b96bfdbba5ee7592ee931365108beaf418c0e8c334c90cfbdb15ef5d4d8e0d83e8e9f212bc9d27cd8df9191dad4692277732317a2164c87f478b20743e31a83fc6a7dd23757a44f54601d0dedb5652b679a657dad5faa29892e9650aa4b2b17e4a997e72e6dc2351ece90130edc5f6ad2ea29521085e93d0c1780bc532b83dec6b39b8238202debd4b890ddaeb428f8c088ddd6bc49ef16435ae4c17b08b901d41bb6c6fc99d377ad55449e4b79ae4010c838a4f0d1b88824c8e2c243b386927f2e908ad6be170afcc00e36a9784b5270ecca3b3f961a5147c8c0c917587f130ad523d80eada6724277de4baed7f149951327e20a7a64a84a773de9d9f8d38722cc6dee3d8bbaee6f451a5d234a5781a560ef919c5058e9bdce0051d120cd8902862aee1ddee58abdb471a81ca2ebc5a08402f357dc55268d5735bb52979016f9cfa224cbb51ff1e0d3befb42ed844ad597c95dfcb3bbb05e35f26f1564bf722820abb1d47ed15e2a1297941ced453183d90d565c473eeeccacee5cce9d96c0de71dceee20a2fb1400de9533802b1e4529a0c8ea1754e51303df92edc15b67219e51c462d78c336cbc80d8b1d2376843681ca2831a0cdcfd3c0821c1c0412a756439c254908a6f7ee6f921dfcfe5e24b6c240e70d9d26aeaa91d913a284fee4f13adef745addd5496894df16ef7a7dd144740ca6fd2610c6cd49effe90385031193f2d8e09b0f72ad4d82dfb9c659a1a011239b410a44d480884d7b63682d62439a9632c4a09e07d98525f1ea7e1f718350e1bb6436fb05c30d5da770527a32123b5e20a3cf9ae6851a13c74de4561892a69e82414b1d00621d462305e49f050c06568449c1f85f10a09ac590ca2ab0c0a3bbf3ea26d8a7ca1d8b0fcb41f4105240a26c5445b7554fab9df8ed7af9a7e04" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1abdffbcd857c1c388215486299f7fb3f4dd927e1ac4eab601451141299a1957", + "proof": "82855a53ef9287b250be27e90268e0524ac062f36dc1b5768cb4dac85e172e7a90292cafee523d4f3bf4374fea2eca24ae2f2c49d451aa32d7127949b5ef2e067c1a2760ab18bad9b47b3d41044b511f74cdbbfa0f026866cff3c30d91a03f3a607c085a7a8e42718d1af7d5955bff45a5a4a2890da4a541424b13235819171389e2f99deda8b9effeb61fc7afad0b7e31190a0d2a3bd15d5ff036d68c45030edd99841ad4347c0ba97f843748435eaae6f9576014d4a3359c2f9383b8aa7d0d4312cdd4d8523044c951bf2d2d16304714226f9cf2a847160933a2f8770880030080d243da85eb3b76dcb3796bd948aa0eb24b1c07fd09eaa438610b3a1253773c82660b1fa0c3591358c86cee97f4040689dfc7ad143e69f15715cc456f2e2ffa82a4633773e4d9266343d885ab4a5b0164c7efebd46075a60c09b559a48861f65204cb4404398394096d0f9d044eed60c67d2b1759de599835c3e0c7d258289c3cbf7df749a9f2d9edd623a377f6b3901395a8d78d41d1f09d184b9e68e13c94be8890c862e30961db269ca8369a91ffc37a6b4823815e79de8eacf141f456564b80d94c137fa24a76fccc6f4ed44dfe804e75472ffa7654f14e79ab20d768a6658127ffe3b219981c9661c005c85631d6ccdff33e5b6fbb4cfce2bf594105ce0afa3a35f55e50163a8de96e70dc9b1306b9e598944af3a576a45b71d0b472de937452b7095f8b09c62aedd1bfa7cf19d86abcf2e868263cc9ecbc4f5a34279690f33f8e6280b1ad4ff65a759f077f809899e80cce997368237fe23ea2e840785c4cec8a01467fa93dbbdff1c030c018ddcf36d18e14d7cfd402353daa977bdd2917cfb040c140518cff02ea15c476e7eef519719008660d75affa67c36e0d9454748f23065af15714934b4511d0456e4a3722c9737ac96af10f40ee9fee01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "383c8ce8369c49616a7494a662ba26503c5a758d29d9462f18c483d25f8d6f23", + "proof": "7a2cf2612299a822c7f8e737c5859c57c79190df62a82b5bcf06ce579931f54a363ae12f4c8d405bbf370fa97f627d6e494f0274a9545d445f3348b16c03b549d8d68b9b68a39c5b51f116173024baae8895038036a9de2bb9641b986d10901112878203f222383d070176bc5759af92d81f64e38a0bf25e6f75dd343a1c857f6c8ed660a031b40d5ce6f01e646d83a9567f8a0a1719aea10d3c3a1ebd11840fbb1d44a985c1f509605916b69a16c3fd4bea418dc9a1e5c8a6fe4aeed96faa0ba787b9589632d771beec1782698ef7c350f24faaf71daacc5f2b3d070443c7075470467f86d29a6e9fd1b9d43dc2b18a18763efb568ec08ce8578764a75c0f00be9b3533bda7930c274c53621d5581827084c9638de2f41c5f225e5e21338f749e5202efc0efe7a7c00ce763f271186a4db4ae9572cbe52fae03c295c71b47542a510f76da40ba24f902b5990a7c96b67169e85c7e1a2d9c6208a52c7c58d30fced7d725662048c6c78137d1b1fb4a5524107bace16eb438c9e61466174bc34db4c4527f649ae06c7e3b2c8863d61c681f8b46ccd41d6a7e5c1dea80b3a2ea1a001da1e3c53c3f2be7f26d3beea053212c1c7c3159e7d2962b14c8120438c210784bb4efd9b64c288a340b2d19788637668a25e3acfb1a7b8dc2bf13b345ff6922c22674d83c0fa519898f0c72dabb78f31e8539fae15ba59a1e8a662ec1da028e18f60c903e25790a0f7b7727b77199f464ad60ed1fb4b2a0a3122469585b76a016e9cb9f7a806f9250e809ceae959c9dee2ebf03f7b877d9d78bb23d467f14ee8acd4a4d627f6b24177a894e38b581fbd36badfe018fd4ef8ff99eda407b1b681b33e988d8d97c26338358166162130dd34527375d55544822a9cdbc9f020afa3b5da98a6852dd5f20e7f5034bbde631f18f96a0e8a418cda0dea426cfba00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5ce6a024251fc64d43654c914423238b555b6eb8aec668619671d56301b04b0a", + "proof": "2c5d5e149bb09e70215856bb6197c08a9528812db0d77cf2ccf204771dd1cd125e33780e10d49db88ef7d9b0c7b2a39957215cb067df285d9d35faf9a1189d34d887e9c3c283fb1f13cd709b74e62d2829efcb27e9ea603b81d255116ef484726a9b36051b4436715ca4b371039fe1aae68e8daf6111d938c24a1b9d7aeccb4e51372bb2fb3a5ff12c371d5882eebe63697652cb729d39246b1c566a014cac0a0ac67754b941232da691e8eed1625be41e8aefc74656cef1d8803250e7e5ec0bd5bd724d80664809cff40192c45589ec233db245d0eac6e8a51c2572ba50a901263e035f068c9b7984c2775add818fd89bb95f0be684e084c7dcc6f025f00c71a26b886221104a7e409e5cedd80cd920bf5f480e1476c1299e1adb491d5f7f5bf88d36601d1df02e07512783a12f215b468cafbd9206fd9d7010e2eac49b3a192a518f2e23a7b7620c372494b673855aedb23c08d9c123332dd2201e143d9d687a42085cc88a15d4f58db32a47597bc1ffc2aa3eee69ed447514d49f759a917a9694919cc42ab1f4d2f92090d0231e0c47cd01be0b8654aa5f52b5ddaf943c7cc43ad5688b94955a7aadeaff5d6c994a543a1f46f8ed39676d9d3f6562984216da15a818a2a86965888e643434f67def5a542620acf51007a18b6061c59e7d1c508fde6735a0d5fd71b43f9851d0eb1cf889001316f8746032f80bf359110763c64e4d0000678a64c0fb516a971012e7ed0bdd6e2a02b831e20633de14da6b3046b4ba985ed7e011daa326666508eed70e0ee6ef7504a2e0e83a2be5ca72ef466a68a039acab4dfcc0277c5a3abb330b07a7e79e292dfd8b03f0db559f136e770f351d03d057ccd9023c00428e303449cd041f566cd53db2dd80652ee775020193a4850effc78520b383110a9a3cc939651560c328801bca7569cbe6f0ab0f06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7ed8142dd4481d96e66ef0698ba8fe158dd2b55631e165a75529caf88100f25f", + "proof": "7688ac15868a6c18ea97b55305d75bf6103e14c2fdb4b9b79fc30469efca87354a153941281ffbc978e1e2d5647dd9eff315fa34c55442772ba3e4cd2575731176b3fc557b4334122f0a6ac36340179007f156150e1d7b28d4790ade1c13051d7e17b2eb2bdcfe19694517a3ef1ace39440cd99e92942c51e98ebe618f430907b92c3858246c8761fd3605391764a56126fa27e189177653249d9394d65229042044a0212f8de082c54453070111446e1fffa464a729671136e5784411fc440a4f7b201f01ca6feff76861b047a1ef17b73606b5d90e4700940e38db1056930796f6cc42dcca9254958968510e8a7d719fe88495e46aa0dfa42a439636af702b68ef87d40efa3a85a6c673a8028e750684737fd81326850994b7c362dcef5e368623aacef08eb568b94e04f85c378eeb0a2feb8f59f07ec0917772adf5f1300b388ee04522818e417dfa223e4b4d7274c14f2402206c6e0a7514490cb2541e2864a0f3fcd3325d09ad2c06eaee8c98456682533e2c9e3ec85cfeeba14425d818865e971999dd8e51ab14a539a1b7a86bcf114664c4a8abc5031594cdb27ef51ef839252d4843413245718d7070535605e68eedfa4847263a053239549829e41d268660cba21c0621d44c1698a5ddf2bc463b4f19d554997ed6f0cc5fe3836d4b602fadfa09d36415be54741416b05f821a0e580f4058c36ecf8d145cbc43af027c15b943ab42c7e9bb6c039249f0715a79bed2725226f0593c8c06b4026f1f4156d99ad14c2f842fc009e0bebc0d776aece70d48adb70f0b0cbc8d02ed112e3b6ad6c913a1d67c60c2f6e65408e5dac5c908ea0bffe4e8fce6300572e6b6f37a08b2aa93ce0972a1d83b689545bdf8c54699541cad0343f4433719ce32ac5e0ff761a1a122be1fee6e31595166f19770c0167f1360ced70cb3487f2af4f24c0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "84cde35492d79e62c5119cd78d71cff728d1eb98612e5096566582ecc37da869", + "proof": "fe6b323b6f366270118c1a99e36e4782bfea9c3524f4b3ae12272a36a4f7784836cf2edb5e026546bb4b757e52c2e38ef1c9fca2819a1cf24820a560d802fb5b0c64715e078b63772dfcc3d94bdd5dcd1dd94be8fd102e7b7c383c9d14069d404a0ba3605f650ad7aedbdd5606fa74937103ca488b2f37eaa984dc606dbc4008ee7fb5c24d8835d3450c158373fccd4d94390cb88625c07c9d2fc2bbbd7c0a0dfd1c3fd009baf3c2e5344a0fc42466537b1e287b66d8aa7065b5d67ec107730f687e4b985486ab1e604925d1f8b73f1c7a500c6ae0be48c417ec5f2ca5a9060cb606b0e983ab0bbcfb7459385655814be14f007d4819ca8d05cfce96252c1c5dc6227c1ca14f7dd8d621bff4f3480fea83dfd27c8db87b888e7abc345aba1b6ab6a414d774c2d31eb7b701dd498ef29aec5abe9923ffcfeb34ca38f3b3f4737896f6b2450bb2f664801bcdcebd6f91615b3208f928853950612d9bcae84fe5730afe05d4e151c403a7de1a70e83ac233a1666452e248602e9bbc44b5fb2fd65a42aa309504207af4b296d6742fe10a78c30c1676c66e8b0bd69cc8841f4ab4645ca164dceb08c12da2b27592b4301367e0f4262589f6916bc4e0cc07cecd12100453eb27259b7ae2430b77fb7e0dc15ac9bde5c9de28af3ad9634e4a7d98f71a5c83a5dc21e415121bec53f1bd981239a85161c92b165254c1f6239f1a15a84e525e8f6065c3aa195d547d7a59ac64730ebde728c5c8e8d39b7f260dbe1d4e419e280127c47b8c4fb16d5a5925e5384df4d3e9e208f530422e9c24bbffda071d168339af7d86485185813a75ca85ff610639f4c8f076b9f06c7995b57fea805210ae3983481defa157646a02a4145a78824df9acea150e2a85cdd1eebfa37404f72e6a31079a8aa83ba827b8bd4998ff86096d8fb1b947809aadd219dab52f0a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8a55bb4ffef6af3f4861a95920800eabf47e4dde73d7a377c15a1ba329a1763e", + "proof": "96fa3c68445e2052509e116699cd666a6b46508955e8a92b1296d90c147bab33e250d14adcd0b528d62b44b4e72b50f5a201d0f649a805bdd34fdbe3e9fc1609c89dc524ed451d0520f391e6be5a0655f2f1a2e6be405eab9f9cd3dfbf6b8352cced29272a532802c1fa46e5da449b2c759b285061ba386fe5c7e9614bc58f6211f787c2545908faed335c115ce66559715f8a965b32075673834c5c1b5bfc0dec02b25f738f8fbc4472baa8b77d6275440c3dcf0b47b7c2ec5f7e983f9fd403fc2b2cf0f626c0bd8597a74dcdab495b65b430fc3984cea4f07b903fbba11e0d4e4f1ecc55b2646ffbada454c2d5f0bf8533e4db4a1da503e21a89369562301e446b9d1a2671bb5fe63a46f3226d25e70f916be3c6c92691bcb8834b9d0cb17f92ff61fa927fe577d7099d50258a5687c718629937c7e8fa233847bdb53f216e7c79cadd1846a71754da5bb39110cb2e5f2d139bef40d3a477f4a2a83f6f37679e4e7c19ef55aa3cc50e53ef46dd4cca4a0c92dae915e251cb890a4f8f392a6a4cc71240c727718342a8eeb64b681e564cf520fa649dd21fe6bbe6c11c41dc3ef0cfdbb54e11179b1d70db0d680cebbcc5fcb62293e1bcabd029b67fd36ce5183e6a19d535c6c58bcff8dc0eb1eacb907f92ee6b332357a7654428004208ca6268b42afa59916e81cc8605966f7741434cc86635094b7e2430032e9c2ad4a969fe570d80da66a621a93f51bec489c97f8406032051c525719b08ce4a0aed1e1d322525ab842b62ca0d0316e3a01947d24ccbfb214846ef825243e2af3d66ea26f23272b51b39dbedf4cfbe57bea73bbff61abe926b9413b330c59ee13d4f076bf65752039071d2bb3ea98f9ebd6b7089ba5fb096a77b8556e06f4279564b03052fd1833c7dc6c04ac70d3a4011a04f271a0460bc3c720cc2f19d71f3156ca609" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d02c49d05cded62b2ea4d47efaf08029156907de62a0d694e62599185f0c2b14", + "proof": "648f5c57b451706460f7be2c748cf9504bea8956cfc1a2f695d8a4c8a785422cfc22de0378ecbfaf089aa516af6c20f66500b9e06542684f805648694fac311422fd78450549038f7e8854416b6fdf972d61c1adfb7743a8281008cec1d5667bc0746c548b8a590c227d751134639ae9bf5bd99a8228638ea31850c6daf5c3532e8c4ae656e2b347d545f1c740d9469b6805a39652c0de3b7abc9f4fc3e19a035d513e96855640f5026907f6661682bc9299a8d45ed3638874c0e835709f7008313fab33a614a139f7acaacb02bbf97cda4891648efd06799c9f8fbd545e720d84d0e50e34673f3dcab4a8d0e25745bdf49b98ab50a9a6af8717671faff3c6433a8fa685ae2d7c0e4548caeac4ac012cd768e5df4bf2b23cc9eb91ccd273ac18e6cb5fccf8512b90451602e70983e616f5c9e0796d844a81241054e0e5cefb7bd477ea571870d1f4e2ace1dc3d66d39be8e2c73b0b3ad14ff0c2d1a7c2352b05bccd7c5bc1a8dabb71a4c924f6248447ee38da46aee851d4f0b821c29ab4776d4eb42d719adfac3dfa5098f27b44ca94d4e89126cf1cce1a941a01ce922926039865c64fba7b7bfaf7316a8da07abf30dd5ee1ef8e4106f4910dec728b25bb73a4aacb53ce9b410593f95e4c8f58a2e6c5519254dbf8f3fa6afcd8c01d619206bad94054db41430aabfacf056c3d89df730623048cd0ce0bde9a57627dd3b76ca69aa2f6f521ff4d16370169195045dda5943c05698b47b0c69860266230be5c6049bd2e65bb8ac309f83eb1c22c1393656810ee48a6f066c337f0965e8590514ac0eb0f193a69a1d0c8790c9c893eb540cc57725c16a8bbc4a21ea5e0ee9e79cf7a990d49098e246851afc177ab78de54d3fdb7b969fd33b1fa9a728ae308066307e65a0a99deb918d1e30e446631c9fa54a4154586a8d8ed7f93b8f7e9700a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d8b5d7a3ccd5dd38ddd50ba5cf5417fabcdbf4fabc614dfd5efe895da1dba81e", + "proof": "84937554b81d485a1e58af91de8b3a40503ea2cb2e5fb8bcf41e1f816317ac6e76e10353fb03f305af521b9f3094d9f7a00ada97bf48b2a030d159f0cd1c8545603ba942af3b395b6896504a4a76043bfbad1cc31817041cfd2fcae5ad7c572890028571c16f9cb9bb777fa37bb3ac70072a0351dcb31bc98810a63b4ed79b692537f9ce64867248e746ef41d49ac1c67defba09a01e270adfd73e5471e4c00507edb7b874b4255878cc0bd3ea4271e9da7b6c1ab6e4e11e74e6f0c099d7cd0d055ed421daa263a56423e91ab4c4eb21a17362170861e8720649614564dc2b02c2edfa3fe5fb72ae54ab9762c3490becb3d947ec1c1c3d0133b3c4d7709fa6779c7f8fe643e00b5a20e43e042621e946f8847998265a30d1efaf2a34e45ecd793a3f53e5412c143a8e2422d8a6f0eb2ecce05cfd46299dd5cd9a682f11bfd431864a004dd3f425c03756ca9222c38cb99dbc1f5001e26aad245aa70fae4ce90e14222a52b023b4a25acae519d88eb663ac221af064ede553ea6b9abc0b10d45716dace45c4df42cea3946068c96409588748eda5bb3d09ede8e17f533dc9122a14c99a24516d230b5f0ab528a81b9dd0e184aded477fdb3e8547ad1c883478030c32cae64fdf2eda22756ad4072ffeebe3f05534079d7ac37a2d3500ea6b95502cceea8e9693806fde4a9e97329139c8d97d7de686c1b47dda5ef79928f71312a4962294dbf4c3ec7e191c2cb91b1f2e887635e730d69d236b06856239265859bac06972912eb83fa40047d806da242585e578843b4944118f2a0f45e6680632ccdaca8fbde58d7ba75e83d0c69a751be9a4a518b0043a9b01c168d2e1aacc14d1c4f8bad9f4e9a8b9fadd2d6d0ebdf3bbdfcaad392d67db9fc87bba841ac40a6c1f345af458324e50848c721eeb9d7fd43027ed8d6bd12855c1173093e99906" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 39 + }, + "commitment": "9ebf62f0b77adbb576dcf2eb952c24e10414aa5a30bfd96c9db4bd0888c3ea5e", + "proof": "3426109770adb75bc4b42fc1648dd9f560d02efba71ffb8cab4904b53fc96f3972af124f59ac06f6adcb975104925ed9cc9fac3ba2efabe445ed888df040e763dea84064d56b8cb1234750cd49ca3378b2766ac1075c4e4dfe348ac1eb3a285640cd13652c30564403881d8fed820756652e2791020e7c140bf0f7b4462c500890c5485c6ac275fd2aa86336ea8f330f1349bf750be69360b1c4b5269872b008d9be555df1a1fcd250d8e4abca7dd8b8067f9ae14cdbc4739df3d602171b3a03b4739776692b9eaaf26ca798987b5318aa5563045de2d6b0e501b2461a7e9c0764e4e59c8ee8268192e71860a3b43168e994fe345303504ee9e6aba10403231ddc5c8df5f42348ac5a61d635a7f00cbf7df06b77bcd36250cce7b85a22d43526408a7c6b9ca862dd30f9f5eaed849f51366e4c41718bb584093aa29b8501a376962b198de068fbdd2ef1f3e50d6e6133afaa3c5331de04885b7d989090c8ad5288f832bc3c0d37daf36e96a7c1f016d75e86c94d29de2eee0d86f30d1ae82a01f662e594ed3a1c69f65ae60e81a3c70374331051fbc2d231aca33d0aa820a624243b580caf020a952815d139d59d7716089e69aca7a7754d924e7dc7e7e90d17e6281ba627a468e4fadda68d817fa9c5c99f1a826f35f7573eb8011a59995d0f0c66c18e85e8d4ddcf361a939e653144d3e2f8c69728db84bd738b2a9d40c52faaec82544e0fdcbc33ba0ded04f8e38d1f5f41618ce88687a0254f1aee8f492988ed8bccbda5b781162ad834f0f199831eabfaf8db4729db460d29bf1c422d15aecdc21b758b2d55dad81dd7cebf964505b5b3548b298678205dc2fbc402326cff988980a8fcbe883e94400a1a139df3b531a1f1fcf17a1b72cdba3eb8ff86036c2409295f1c99bce7104e04fa6a8b5580c307b7bd76531c4151936468935207" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1edc08b0b03d17f096a8b04bc96555647ab201bd5ba1381e142f9193d223b915", + "excess_sig": { + "public_nonce": "be3437222a70ce9a4bb52b829dd1181a680e4107ddcf88c5319484c7289e7728", + "signature": "9a1f66a231db6244b75b28faf1204fd06061bb23ab90a745e22e9f4d0d8cdd08" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "845fbc8a567f0e2aea77243de9109e64ecbbc3d3c21c8f9c9c556a6d5d03fc1e", + "excess_sig": { + "public_nonce": "e0aca1abd8ef13fd0c3578c7df50dbb53276fc992da6387697ef17dc0269bc40", + "signature": "c9971c9b52aa9b14e1477dc7f2cddd2a23f9771b0cc12753310f28beb4bc9909" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "8463314dc85402a5c5ef9ce0e6bcbd863f976de9946634eea87deb412663924e", + "excess_sig": { + "public_nonce": "44266aa446cf31558b77d1965655b553e4bf86426aefb0b16f6c99878017f027", + "signature": "396970c9c5c5372a7a3e483f08fc48bb3f70cd5e035ee9cb10a783a9bb2ebc00" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "b802bcbac833892b762a5d621cf16c7b9cf18ce09f0a4b59b1c99ee7798b9277", + "excess_sig": { + "public_nonce": "76432d5d66ea00c4a6bc3f650c765a3cbd8ca647b1abeb1957f86488cbd8c000", + "signature": "f6cb415ffe12ad31fb3c7b44e458819adc0b1ebf351674875eefa2787d77790a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ecdb5ec5305fa294a52716ae5e10febd90946f09e91d360bc64c4ca1e6f4131d", + "excess_sig": { + "public_nonce": "deadc0bd69cb7345d3d1e9eda55efb2ce541fc0c940498862d9db0cfd837e673", + "signature": "4acaf3d2ffef9801b95b4c1f86e432a22b39a12cee608fd1fec5097640015507" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "a242566b435439edf454892620e74d3a608b8b41d28f45dcf170565c85650b22", + "excess_sig": { + "public_nonce": "361d72fba7799ec7f5b44509b0bbcd1bcfecad381c1d5ba9a86ea554352dc94c", + "signature": "0e34b2ebd8ab68228f2906792030e568d95ba848623af08dfca8a23f54fb1600" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 39, + "prev_hash": "8d3bc05e277d7cd61028a0d1e3c5b442a5985ca089a03e17c5e5345c184390ec", + "timestamp": "2000-01-01T01:40:01Z", + "output_mr": "0b86846c72890e738c0ac0c8667c9cb3703ff62c2e49ed6d40a24164c1b3ea73", + "range_proof_mr": "f8479cc3df43241008b52c745baf3e1acff7061d8d7ab2c466fee12f6892953f", + "kernel_mr": "c26a21c50039261c87e95d59e3043202033ca51e065ad2dd9ba73f93d585ebf2", + "total_kernel_offset": "6b63f7cba582fc56241fc8f7cae6bf6e74755e5a3308a467c6acde163d9f0e0f", + "pow": { + "work": 39 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "18856f0c5d40bad05475fc47e0f15e3b96d9b2e3188a62436cc68c5ee5ddb850" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "383c8ce8369c49616a7494a662ba26503c5a758d29d9462f18c483d25f8d6f23" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7ed8142dd4481d96e66ef0698ba8fe158dd2b55631e165a75529caf88100f25f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d02c49d05cded62b2ea4d47efaf08029156907de62a0d694e62599185f0c2b14" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 20 + }, + "commitment": "eac6bbdc2c2be436e1df778ba20342b4366c81b5f1e450d199698bdc108aae1f" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "08d684457d7d8c5a63b3e383a0a99eaae634d450e3c88f004111f4a1fa795430", + "proof": "80e07fda3cfebcdbca6cf307ec448f49b6ffeece6844c01d592db8a698444b5006979fea4c0383c02541c5d035c7a376f477c6a519ce3d14ca5b4dd885f8d4450ae5c70bb650daabac1fb05aabe59f499d603cfd7dc8798d7b537bf6c39b5a3bd0f31a734e28ae494d2c52e9b6837e80be365a71405e21a9599c46e879f4952acec5908c8b8d37e5558ef52101f998cbdb67b68c5136ccdc00d1c68d6779810a2b980712d068f30274066edd950e304c43337ecaced6f810276b836f1b8a4a0ce7fd7ef30f181cd7e0c291a01a9f5674cfc161ef32196ed8dc2e949512a92209ceafbe72d5c5eb0470d17cdc987448e5d2d4c176a9ca1a2bf28ab30442f8776e0eeeb839c809e22e3ef09174c007f7701958aed60b683fda41511b3f9a938366589b408bf770e7aa7769405fd4121a7d61cf65291529a97f3a93a8e6cf0a813158b1274e275d262e3beba104e37040a6690e5aca7233c9739ded2538aa3d31675282933ec4d01e7b024328c444553d7404672db2b54efafa81a10aa785bd4f1e0aa95644ca72f8ef36ea6bf14811f0e8b8addee5f3c729d37340e63e99fdb87132bff2d4fc5ce590f79c5dcdc6b51763e30c0ef19913df14730ecfa11461c442d6232bb17107eb7e114411907cfaf273f91e51048202358d22e77c79f484ca6f2429d24b201888cf6056f55e55d57c177e53d3eaf6d3b4d6318a2adec0815a6f22a2dc0abe31f0d00513a71c0fd16248840b8e22aa436284696950a88fb72c1c706c8af35cc2fd5d663454db1f83cf46aa936ba33fb9938b483b6f5a5bf47e727eef3106d2b24c8797126ec658d46a703c98cf3171d948555266e102e73b5758ecc003a616b82682efbe3fe7a04d7b2b885e76899ffe6de5ab4c6e3c841c3404026afa15b29f34562ab9f2a7550625a7226e2699a652c4452d0b840fc5eb0806" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0cf7ca8b1eeb45db0cda7bea9b39d77b07426e10f724b8854a00e1dcd1f08c09", + "proof": "e6499fdab2fa1eaac23879685cdf771c77dffca6aa99b1701771b2518378d33f50d3dd127ce24891a5ae8f8d9ea6734666985ff5c6b881a3104a58e041f1185610b7f3a989b4ffbb50766c88e118ae4729fac2414cbd3ffa163206125d0b197e9e5c57642539afd0ff2ab1640461330260cea93fc79f64fb113553b0cae3790cbddef4f07445a1d9d5fed400caeef9442a59ad985f43b4e9fa51fcffdb709f02b66bd58c1791bc36a71c12c47b55ac63aaa1b45627655cfb9d7b205f9b30cb0f67dca0dae136591a1e07f84b7430109bd1bb0df31cfd38a9007b9122d00832009e2fc326c05417b49dceeb6aa2048ad71326ea6f1179a72f8d01ca1ec0c8f639e6cf5fc4b151ddbfea17284600b1fd0f40af3c84aecf3ffac0b526807dfba3052cc5f3ebb5f847ee9f8b2ede0bcef6e2dea117b661a69a1792a7c5cc0d2e8b34f8bced4a55f5a5f4820a9d4b27b590c7f59de0f2c86c9ddc2b6a41108a1d202046dbc1f89c72eab9935460ebeff61dfee77c88054e1cdea389636fa911d9bc79d028eb5dcfe0a55bd1e81bff70d7257b4caa3e44117a405f8b0a50c3a2c2a073429ac00acd673da4f2fdbea46ae2eb708ed8ce3f362ce42135afd52881a0c1294017b1eeed389cdbf5976a732548108422f1e09f4e1680a94a34f98a984fd65d9e6a3f201d811653651fabf4b403a09152273ed871ae3a8d6c694e58be979f188e98ec5f2e1b06f33116c500c7bc6b29b6973041e0fa8c3d29d4cd69971dd07214f46a9a5445760c61527ce0a888658cdd3a015dbd95438441f06e0ef60e4f0d0e52e7884e97754345184c1d83e5de453f930b9f36d58c3dab060b12d7c65b704eda65a3cc097aec3268efdf56a229f2d2cb6a5df71b7476eacb295adccc2b0583988045fc02de5df3873e035f839d273c95da79ae8d7ddeee2f7a82ecea6706" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "22592116a769a1dfa9ffce17aa64c8995da0f9990adacaa0d9e4918d8bc0f826", + "proof": "fc9a8d7df9df23c9ba510e79378ecdbf20b3f3164e4054f9b0a43bafd321f318d6ea164412ce6c4db076f4612d38e1bf119a564067eba9bcc811017e61ee3635b46189921d0d013326fd16f24e82b031167201f2289f8625ca45f84fb8818a1a042049ba787bac2bd2ce57106e4935ad5cc0feea3a45b4535314a86b92ccdb73311a6f8e5400eefbe71942d47fc02ac2d040c2b8d88c9f421ee581f56a18780265dc9703994f1ebeadb16305fe77df3cdda139114e1d8640a8c8159269512909a369c377b85f0c1b66841090fe66d32595c9064aa8adb55f2e52ebe921d30f05829a9b7ca48e5fa77e6bb74952840c0ec8ee8ed3a6c50765b1b4a476f62f4d6dacbce973e9789cfdf0c4902c91fadeb4e540bad29fa95ccdaa66784d8027c220c417c29755e53af4332e918137970cd7c52317d23db09cd9702a8684585d2a3318b37dd2993956c8b381e4c66d4954621b16ba9073394681f1d91322c688351474bba8c37c850f24a5c16a9f40857ea4d933825b0bfdc3975e7865f8480b733f3892c1f20281982f008db051e3c52433569e12c88a8bee8480295dd9fa09bd10a2f14f1a49c249c16c0da9688462b12725ece86d4a7fed12abc43538d6dece4930c04b34b39bf6c077ab599d1fc071c648993b014f05d2bb17c93325353613453e9c1674b58fb8c853adc4123e8c9791a930d2ee9fb645900117b60a07534d60c80169221be6d1c7cb0a658f643698bc0a4b85e71d91a30429ea21bdf6618c66a0715573c073926ddb7a8135f38d92c9a71174794c60692a8c614ade7bf37367c4cababfe162d5f128bffd06bb9aadc027d0737e91fbc49daa90dc8da8da681cf817b296d42098da755951c4324c1688840587f413ce9df49667dcc966afcc01ba16ad24f7d08daa1d3bed7552064cb859c992072eac163e03498f9e4d81fd05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "38b0c1d73dcf218b40909a67b0f263238e455d0192023e3db0d5b102e8d1dd1e", + "proof": "265469c8ec7f8d854b3472ab04dfba3d76c73bd17e450b9d4a86134230c38651c8a30cc8c763d97d351633778d55c7b8c6a559687b98eb6ab5ae1c1fb81cbf21129db8acd1b7f238ef17de73673af486e5dda98c6125fa0612d32305b7f7ec13e4adaec79285b0215bf743801d149b72cd999a92760dd464bf24f25f25af8700bfcdabcecd1b4dc69d8611eb8f5265a1f0c495539f2708aca1320b1f29ef8a013eaaeff37391f1cb24f0aab162bd93a35d06affe67429dad2cc605e3912ef60198a2b8e1ddb466b5687a563b8650d37b3fe9a17dfb75dff2a4209c1238b68a01f69a15a268fdaee3905e27f5f96fe9dbb243d7897bf69f17b49800feda09637642c0ae6478886d23c380bbf51ed713d7f568d55e779ab7bc87ab24907d3d472840bf653f0625c337e84a9e4384414d1eeb57167873fada315db54130d473d4335c44e0d27de97c07a676be3b6f84d70bb9be28055e0e5d02c1b7b7901bd54e2c60c797133d015ab92230d0e6defab8923668a125da3ba0cfc81153c997018a7e0e9fac514fa842ab685b1e3a1a81730101520849525073fb3a30b927bfc3de64aed52097615428be6483944d53a99448e350cd390ed79e88d02b9c0aeba3b41d3ac84a31907807c2cffba09ecb2b74f24343d43ed2aa6aa626d294d4ee34b10a3008c5826241331ed8399e5d858edbf8e436f474f231092839e083e2a849bb4d04a381fbcbe8cdb6c645a7557a9da9e57b070bd78baf3f8ef26e704bc4f662588cd4c995d34d8f8f44cc7d157a2a84dc86afbc81627741dc94b41aa22d0fcc68a49e2a86b59e3221a9e044f7a4bcc3c66f6a7f53cf516af4b5b6344e6d17835539283be5eb468d9c32ad78436af2c41f7509c8e4db5add232e7e8cdd99620803f6cdd68a706ea066abc31f034c4dd9a93826a7e3b21f1c38e0c5d149f1200702" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5845b58667e9915f4ebff27c4cbd2ce51fec7d1b709ef992622e9e0b0d11084d", + "proof": "c8ea3936dc2cfb2ccbd3dc7035a106fe69a4541499ee75e57faa1363cd584833f2bfb1926f4fe957e79f626966f885bc55a043c0553e080407beb6da2f80e471702431e379bb6bc03d5f3773b0c689ea6fd59732647433b827ffed13f806c456d0599260b2fad9dedf5eec63b6bcc71c262dc20d29bc318360d5bb4d63daa9729d354944664c45f8ab80f1b4274cfbe22664a5539cb65d7d87fa09cf8512660fa39a6a71e0a5db5cbec7e8e4f98c53b4faa4dd4e48def0b943b098cf7699f101b3b473ee776aa99823b7e622c5f5290f642824955373cf3fad9e506803580606e6d6c6e13db9e9743c584f0b0938822c03643b49972480f3ce47d0817ce18a26c4fd99cf7bb8929327bccbf76bcb06b9cbc2fffdbb1f6361dec5e2f6db68bd6e0cbcd63e6e5b75dd2cf46fdea8e0f1fb80c3c4833080fa191ead973f396c2c274456443ec960f88b3d2065015e293d0b9741abf568b6b8289a149b2e2582cf54f0e9a832e7f01d8f5c5c97bc94e714a865a1d4502d7bf0947e7824bd7c8a8d16d82ced71ab6b90bf5e6696f5fff24c70addebfa12a319dfa78e95d73ea3a096ee4499f7858af4a5f565e7d525a464d3b0ae29bf0ebe4d5c587a61ae22b934f6860a521d46d4281d5834a5a2e9477acc46b7aa75ee719181e013b7ed9fbf4d07212992e1cee9813e318e84e95dad7c63599a98977ae95cc2cd7da68f10103766706cd78287d28d818902a288f97591c5412e166d60af769f042f86a7048ff140b34a8419533426d39207af59957a745b025497bde5d3b4a9af7aece1c46e184537a1e8e312b4661029e1dd7c2f013aa9c68229835de35b50d89547f3e2734635142effb6aa83333695b73294029d6f15235ae1d538432760f1153cd0b7b5e2a060984b61c8baab692c1c0cacb987992630eb2cbfe4fe8b278c1432296803db504" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "90e10914265cf4a761c50caa09db3085306f9390d6c0e7be8a9cc6188425b75a", + "proof": "a69f146a6e9fe2f07437f41f49cba51cfa0ab825b99bf88e5a1138156ac535101069afac7e0eea930d7d590c6a0ed026ad4f5536843131f6b4fbba2b271e9121769c8ca603fa01c965c3fd3e53e8204f23b225cb279d40ec6029b6cd6ff91e0a8c402a37f4e12ac4837c67338d00018814fad50b9f13d179c1125543e01b6d3f3a4b16b03e7c815e89ecb70b8f2ac14dfbca57e52ad79c1ef89088877ce8e8074517c18506272e383161b6910cad1b90acdf154272a57aeebb71442be2b53f0d146f604e4190b8990cb0a05e1130703835cbb2c1247181e8bcb76e589e85aa03b27cf620048c1d2d742ad38b4f5f1089a3a3d38165700d8bafb2b285a42253298e6e1cac1f9e31ebab7e9f88f24c1d27ab4c0b36333e4810658034d9b893a33e545b2ed530952cd6a0d73535e93c9e81c0624e912a8bee6e2bce8481f810c96a14be0025e1acdf07bd170b9473a393200abf974a3d0045069af0ec195c62343794c6f331c91706ad389e65dccf3e9a96d4b843adcfb4b7d53363dc414f4c01680a2d13d4be33989b406dd20489c8d05faa66f0839b6d904efaff2a6589b12e008adfda469da57e5720adf2fca065ad3fe8bf8c852cbe302d081bbc475f9c7f5ece5f16584d7b86b99e432bae0c153d76c5abcfd9e772d70b505b3b1d3b79aa07d82a53789483c777c2d7abb5b0241b1d3fefdabac9505fa61e92a900ccb47a0950e774c60da82385bb1af2ec9ed0b0cd33106b5b5856919bddbf63e97c519535227800352db79d06745d8767f8093de5ab78ab5052417b372c4886a4deb92537aa62432e57c43bfdcb3205b72c8324ccb13b44a2edb8e48d46a1075c5e3c8e0c06ee3f0e5bf3cb3d57ac450740ec2e860ff513d0d6043c3cdda1513a9dfacc00354ae256e4c8746e74625f44299ae192eb65425c89282c6cbf69d94914f79408" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "aad0fdf576af3eab620d844f57e7425fbe3dbbe1f2cc6182c98f55f16d672f71", + "proof": "06aaeb3484a15d5b196e219b5aedde7f4e66f76b9410aec0a276491a25418e4728c10cfa08252c87a676d506d8e8890cafa6767ab8bf217aa44ab715c949d8428c54acff0cfb6e00bf9bf2c4a9053a50eb0156a8f6284c3e257f2e4246ac520c6a298009318a05bc671e1d246078c98ba79d794d53302eb5bc356157b9f0957d806f67f60cafb1b76c02d87c66b30d46345db88a0ddbc4ed0598bbbbed31490c651f123eeabbc1b0d096562077c2e97b39863fbd5398a5a6ef0a0902a65bf4011ee26145b5844d1f2132e967732f4a79e23c4f64e3b3e8ab50530d0032a797035c99c2093684796d8c41feb61ebf1acc743f36880ff9e4a04814774e7230e611144e9b2001295072cdf538a53c8fe124747d63aad327a71b858ae71d9723cb1b7e13723dc02cd1ef2af80d97b63dbe1317aea03263455c9d66369f6f51091d60c4d54b14d352166ac47858bc60bb9e8d55a91952f222c51b5da20a4f0b6fd50edaa211f64c3aa12991b5602ecc0c0ee40f2cb8df358ab6be2c4e9a6bf5bbcd38b899542e2ab2efba0bb15a26b06657726622193ae5e1b414ee7d2262bc87235aa4ba39184461a5b09acbcb4e8b79e9dda6b157feb6d6c8777d9e536ba2cfce112a769483a1af2da63ebd81c8a28ea078c258ed5b37427569f7e77968d5605b47c43727015e2fa4231ce1ee8947ee6ad508f06f8897fc7c7e2b12948464281914d818d09752eae172329654cf8d6d1ecd5c4dfe25e5032aed35aeb8f6ca852f4716953857eb74f8570b2c28844fda3cff1608f447e091afa5191c0752b693d432dade8ca4c64f5497dbe99e78430e671773a9859655b7dbce082c5116419105473f219c5ae0f0b090a25837a57f9bb562307ef162e503b3ecb2cc9320cbcb3605565c2af717372222627ef737db95069bf160f51e11f254db61969e55cf2a6a00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ee7c8c26b2ece933e6d6da03433dfa785b0278f0f2c862b60f7db2cecad0ba37", + "proof": "e83c1fa8c7bd2481083e511f234a15b56fdc6a120bf35cff4238afc38d26a85740e9ad4c45b068bd328ff0c1782c2a1a4d73bc4010e90bc822ec15225ceab537c20c542bf1256063c8c7d04bcb1a3f89f516c35e3b335b4b04b285412b5a4b326c1e2b669322d3066bef21265db0154dc2ac757e1d8f8d3d60d1efa150e4dd71052be95f260964d78fc78dde1f8e14c0b02dfe52bda76d56fd123f13ea1d5c0277fb60c29a79f727c068e1791b091199772b17fb64b9ead7e1dc042f47f7db0175604dbfb5db5d3d6e11e931a256558b5d303174f276f8e69f27bdc2d2c2c5036a1579d3aef8e2383b71dc539d4823cd3c099c016a0add8e6c28ba8a1ed08a66ea183db6ae03cb640f722d3f7a97de1a7446dbb05d7844aa251006cc109df55ee6a22289df7e6d30973522ea16cceabea72578119cbda0704b844e27b748252aba0af53892523958b2c3883f3e3ee19eaa66826b1b731229fa0e6d97efdfcd1bf6c3bbd520dba25c579d050ec56eba62e1a6778dcf734091a3b9fb95b9e66d7baadd6ae45f7364c947cd6c44f8fa87d8f866431014f89dd48188fd547dacbd40a8dc5ba234e3a02e00d444ae81ddf974cc138c54ade86f9f6144d2956e57fa555e8ca335d249832ebaf8867dc7ab2b42d29445790250456b6ac7b953874617501e82dae08087d49eb450f0f66190f678e3e6106c4cdf7e7d2fe2303aae32890c809b580d8cabf592ff2e0e3ee1afeb49df567b58fb71a8f7447144452d9d7214dcf7df157a927efac750eb5bf52148ce1a48c9aaa1df7200234a85b95da2073f2889722d728d4b0f553b78190d2afe879ddf78d707c08c261c0b6ecf35568916d635bc1b40b2ed88199ff7da4b5f042bf952680e2734cb2fc9f5c9a1386920055523e9afe71d0c1ca6d7ee9842ad0976eafef35465b89763e854fd2db5311706" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f046f1eb6098cff7c90320d8a2866c0639c931420f9ec7dfbbdbceae033a4f47", + "proof": "a4b0899933f74b38dd3689ca7185d5fb7761fc8e253831a03dfec02b63b28142303381512a78bee11a9994cde9c3f191b2f2711819aa358dd8c16e41a15e7904f681cd0c2a5ec396a5ad6bf4a71c90dbadec20a69fe6d7b62ca4925ef131f82c2e84d6e2469a84e0d96342b21462a9b41a57dd15924d010adef9e8df9099615f83ad71b2c7b37ca8583b62ac542e71c0092927b22c96a5332902d91585c8910a5ea5a8b39fdd44ddfed3799068ad48b3e01682b3013440a7e2f9d6daedd1590afa096a4bb71da7c56f85256b14e72bb9104b7d0f73f59a7201fce0390cefff0d301501be3485a1e1f8d36b814869fe7d0054c391451da85e8b7b709cf53f7f5ae494a3c63bf7a5a29d5a99cac13540f08fd2a700d1dfd182fb2e03fad696bd76a6c3af43bf96b074322322abd5d4b6e70dbb3eb8da22c955050b010e6da4624ff08ea97b00d183751f9d01a09d93df4e299adb97bec6da043bc5950743a58b2800e37dfa322cdee13d436029010702eee8d8b71288c5061f3e3d801bd9de687db2057ce0926a8d129b24a0cb5f46c77bf76d3139d9f6a4233ef2b6b1f8ed7e0620f83efe59e097fa7fdb9b7a4cc2c1c968d91ab67fcde489412208233074855b00cea51edca06987d7d69f7a8fe295bf2033d8d8cd31d924231c48682d56cf7198aeee73ccda3146020a0ef476a0b4445a885604fe001c6eee53c11062c050126096f2ae3ea6d8e0f4c4ba5f83fbc01889a02806374163f24f45e12cc95efe74a8a9a4f6db001cdb1ccd910e2c10c4ac75b2b74444d7adb60919e6c8cede3f1c0c09ab57334bf95b4121f892e4161b456b15bec7dc296ff3f35e6249ca87f8392fba8e196ff78bd03e7744dbce5116852427492f62a4e70ff0f45bb056534309b1ddb619afca2948b3efcfc32753e6cd501c6ac27b94dc3cc30d51c9ff465709" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fabfb02c65b708f5acd5f277e5c3bd7861a10d5eacf7ee03f9ededee23653025", + "proof": "8cb869a855c9bb32a5c421f64710f646485746862006dafcdf6a3f13d551710bb4bda30f49379fae768de523b8f9f7f3c21cd021b715e1608dcd160254758414a0c6b60fd3a5abfd8352d51d5c6d8b12bcf010706d7123d8edbb6ff4faee7f0ac88db22f55730339f470568b8d2bb4e274bc865ae6758676b6e31860b29cd747c1f67d6eaf1eb0dc016f8c4b9849378acfe383951afe193a7b85e923ada97d0fe5b660704e530ea7d6493ea5099c873bc5d7c11ff3184623788a99021f9a86035b9d086e2404256150a680cbdcce4f3dd0f40628445f45763857cb4bf71b090618330c47c51b4f8c95157ef8743dc00aad76c58f435a50ba1058204c552a867fb868cb4d2a7ef8ff8db4c260f5e59823784264afcfc104df98b762243674d809d49b0db44038e7a15733ba466ddb304d22d1805bd8ebec5643ac6c5baebfb21e8c5dd12f66ece1860c7db62589fb844034f4a52909521794206339757203671df2d2ded63d15ae49927753027e8c16739767281fecba3b7627c41eda8348c51c767cc790843116c84989de7c69bd335df28c93d211fe0df8b368961b9ad67f0654a4103ddf9234fc26a84a578eb8a81930cd8e487d04afac981c714bb980ac57f43c9a9cd84480b75f5847e71484dcd5432587cf5c82d86424267f9badd7e91dc842055bbf2a8646ec0995f7914b93e9499f62740424919c0d939af18d03633d4aaf07b45f1a50329d80d31019da961e6c188993d5f56755853c709ab8acf141dcfe431cfe1d3fd3c52b18b0869cf29e16615c26c55e2c8c06e4a17c3e6c8f67c47e4650d78712174b3b99bafd15e556df19b977c1e9ad4250b70541f6f899254471cca06f14f3c1bc9b2c4581fb9b5413a6d1c8bc3b44a6f15dc4aa6bf48e033f82aad5f17c89d0b6fb7ef2093cd9e7d43fc083b6f89248a178f8bb9aeed006" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 40 + }, + "commitment": "b06c6bd68f8831a08f806c8e7d52bcd79a8637afcd25b6750382face96e0896d", + "proof": "6402482307dbc82e94be6610ad4ce72868568e28e6dcf50d32a008d90462d7021ad4bc2f36a3c6042d9ffffd5590c0e9dec696d914d6ea01968e1217ef4f953c3a99a2b080800aefe6db05f8b6b854976f719127fe787f1d1880a15ba6bc680e848f5efa7973704a98deb70a9fbbeea116ce25927b8fc897eeba16047062aa3df878f9de79c781c3c4663945d806c2a43a0a07fb095d99043020062a2868c806348ece7677a3bb57506adef78567e1789b02c98d42693647ce159890b1e7630ff8d172b96eb300b3e6b3a0668eea19e28ad55a298d42817d06fcb8b8a402750f003eb954a7ca7aec124b86567b599a9c240a53dbe0453b29fa82c066a3b4356faeb0c598bf0617ef96bdc1eef24870ce7949f93d399e71366b2fa14dc638f5490e1ece1252196f866e8d4aaee31fe87f198ed4e101a401525dcd59a753b8fb10c47b8d244690b5c2b7a1bd509856617dc4245775097fb7a7c35884054ca36b7dda9c96cac8cc011227d0cd83d6b32388d6ac6e262b5947f3448718d85cce116128f608f310b1d5fc1dd36eebe3bd02269f98099d91b2b4cf359aae8f74abd3088c7bcae26f6e72a4a5403da877d2f190761eb4172cc948432af7f141a233863fa009b35147a94e856fa52390f49bd9830bd9d8721c91d98c068326c005c6f10ef6ae5894c39f65f712e117455f0111c194945b7247aafb41788844aa890527747669992955656cd9b1d8fc2993eb79168ccb169248116a1d63e89d2c564631734c94a906666364fb9a3c60228fde34c2e44136b438dddadcc48447a37a1ffc4e1484d8bcba470ebc37d6fc7ae458329fd61761ca209d962980cc67aa44ac7e7de7d1b43098991bdb1f4a974b7f555f8010ece88f10d38dbcb7140dc09934960a036d1f9a811f05c78dbb7b2878702d41c859435267f038aa0575d07d3d658b0e" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "14f7c02f3a5171d7124aa5a06ca88e0fa7448a0c319feca3ae133f0d4f3ff318", + "excess_sig": { + "public_nonce": "0c7958682ae13fd0e3c6f3f1797ff775523a58d2f8433aa1ee921b1eb6cadd50", + "signature": "6d1a9c53cf203d224f72c6a6d961a23734d3156b8c17c95bee866a180b6aff07" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "5438367145ca38fee3186812363832a29bcb178c8dd6c6ce182a8f1ea9a4e263", + "excess_sig": { + "public_nonce": "4c9a2d8d68dd6923ad1dbc8041067c2e2361ff2ca22c000bf5eef8847a151420", + "signature": "d8f8239dbdb36922af952d122e9deaeb37132c073c24cc75a3a0d23da098650f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "98f293cdb65e528d8a767d8915cc3ef4abe339f1e98760903e8d7c7182d9015d", + "excess_sig": { + "public_nonce": "fe64bb366037578874e2af751f1379589a88ad0bbc92338d9ef1765995c7c45e", + "signature": "a997b27250e54d87b2db8b0ef1493a075ab4e298203e726202b3033e7ca05b05" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ac45d90ac3444ff36643149190453f96f3c38281f7d24cc768f91dd2e7b74e03", + "excess_sig": { + "public_nonce": "f83c452ac081e9616f4d953e0b5acb729ba39d36df3ec22aeb504d339bc75175", + "signature": "6508e05282f857b4a7a479f773ff800a66d6e8c7f2e630eb8103aacdeac1300b" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "c83dc651266e365cd39978922ad1462c914714e0b639c2d8b24504b84433ef76", + "excess_sig": { + "public_nonce": "1ac63424f5f72eb31c95264f4dfed423c34ea7ccb6fd3e8a51f1efe19dfd7f1d", + "signature": "cb649c3e700860db2b6db3075f345886898751d36a24b05de7b4c63e67c8570b" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "4c23244c7530100c077183d600513345a38cebbae237a5f29ee5aac01bad2036", + "excess_sig": { + "public_nonce": "70baf473afda3c05360962bfb48a2644ae7fb27b4caf78a6166d7d1d37ac7710", + "signature": "005ad0a4e109390ad26e7e284771a614d8d8802633e493ca7681e0be9500c90e" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 40, + "prev_hash": "5a62866b26e125e346bf7268e40c1b67540ae1b16d16f80833a92cbc41763c9f", + "timestamp": "2000-01-01T01:41:01Z", + "output_mr": "a68a699af211c981f0e633c2b0eb5744eb38684b317d931fa676e48ec861086e", + "range_proof_mr": "2114fa1e3040faa8b3d4b4b039b6b20bb80cb56984734ba63a3722599dc8a6fd", + "kernel_mr": "29832de71af3dac8a32c8d1033acfd7cd9eb41e259daca2569ea13ec62c1b470", + "total_kernel_offset": "f077717af828542517ff0200581e16c0cecf94d52a29410362adc9b44cdd470d", + "pow": { + "work": 40 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0cf7ca8b1eeb45db0cda7bea9b39d77b07426e10f724b8854a00e1dcd1f08c09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "22592116a769a1dfa9ffce17aa64c8995da0f9990adacaa0d9e4918d8bc0f826" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5845b58667e9915f4ebff27c4cbd2ce51fec7d1b709ef992622e9e0b0d11084d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "90e10914265cf4a761c50caa09db3085306f9390d6c0e7be8a9cc6188425b75a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fabfb02c65b708f5acd5f277e5c3bd7861a10d5eacf7ee03f9ededee23653025" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "028057ba28d953a04e41c21c34c377db41b257121373b20f74056dc569d61b62", + "proof": "729dfd6aeb73be47ee9e019a728f87adc3008de89b85a17b4e6ad8d829ef1a6c8685388c65f93d8e982de8850cff7fe4fc00d4e132a3cbacc0777c396003681c1e8f555b5933f6c9abea5c9ff4172374dc1d33429d01fce7789c851c8537551d0456b152ccea897d7fd69fbc3f6dd3c3d2e37a1879a5f044d972da51e96deb7e0280bc8c87473ce33ba8cf3a086cc314d69025b9649fa2c1a1d8235c3eb6a908e3e225185b006cc173085ac8f41faa809b13080d6b18563559980163e5f01b0e32ae88ee24d020873a50cbf236d3fe78a42715f4f4a5c12dcaaadc83ff067002bce7f7a442b775aa5d4382c718cadd2f844a6c02c0d749caae1df96062bc14578c625be0753bb9536d464740cd1e3f6b1c1d557ae990f53a5c5e43fb8af97a0b6470e68caf5a00af468fad40c6510d2f8117fd8417692cd631bfdc20b9127f56fa94b9c426c6d5ab25de549008873b764783016125eb31864030a913778697128659f845c328d6bbac3dd4489c2a74f577fc72a1717c78e4848ea4d28db05a05bc13d302a660b0d20e3fbf4d401e9049f9c5408fe217104de6af38b93713397ca8a5cf6caed8735155b8f096b27e25d5cb46563dc01cdd02b6bf3db411b7663fd2863929645befbd2bcb14f9d8d2d286e7a30498ca17d897dbaf28d3dd7abd3c08cce356bc2239dbfc470e7cd514dd22e25844060345410a12a2e6f08fefc856129b7e6d351ff7b8c1a67c83c00ae75068e7982a6d9853ca2d5902d96c75f6436a1f62ad52e8de51a360ce058f4445976d1b3412a72aa913d727bb879f6e4472dc8acda47e9762bfc23144d9e86850294d8841f68b4043654744b3b02ccc8d097fd5cc99626111da67078931ad6a4bc8a33498774d6458f514c9ca0df4a1cb078535ec81070a29ca8054f37b039736a99b4c764ce2bd1797f651a6b373f37a09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0e1fff2896c17cd8df838fc7c7afe3622cc87099e18618a01d7286759b523f7f", + "proof": "a876187ebc82cef6e9d31785b1496c09253b7d1481218a155acc327436e4e1148aa46c1fc046bb94d7d8cba9763a5d2be153548f739c91aa5015bd1b9fa26f4760e9bdaccd57d9000a480fa46de5ad750e1b5b6be4780dec146dbc7fa8c5aa36dcbd400a05f30a8369ecd0dfacffdd3317e01c2162256dd5b92b900906d8155e6237ed2eacfe748617fb4083dd752831d86e718dbd60fde9dc7f50462736e608729a9c0623089260af1373b9196a7145d2db3f7964d1aefeede303ac8c9487066a35f776a8d087e494089e84f16183a1cfa6ac40bf63ba3f1af9c3ea92c44909a63a0e81d2d1e645eb3f6516c40c3bd408da7b9bc992b94d52aa395f0098fc03d0fdcffd6caa8f6f4907e16d6fdbecd33d20b070d7a84c8742f36ec94bbf426f688f61c4cbc5266f2b44f8ba9abf70829cf0717026d4ad9970e48dcde77a9c706a7f14da2c306f217688534bb75c55ed5b861e4d3371cfa957382402fabf9827c8d88643a70621214428f403a6c82faecf2f9c0d51fc2687d284a487ccc9c86b78cf8becb7768085eeffc7fe27d4b785039b0a5f8d754b04e65d854913c28d3132509ace3d8efa5ea79c2b52ddbdb176f47f6135f94fcbce146f8d8dff37d10878889868c384488ae7c681e674d4a3c6bee75d807067da56fa83f1b74434d1301a50afc9389cb8cd1317a1e056e63ebe517243ebe23abf43375113aa05e5de3b4eba46956cbe95361852520211dd836a3080751ae6f94dd286fdad9309ae50479681fd79e6474b730e3e2c7505d71dea1f8cb34a8dc920dad68dc69c6b13760f448f8db4260e0f7e744c72a8ef7501607262f915a3d6fc5fd07d1cd8f6d4281e0caf83b83cff48618d91b7834a34bb9067a4cab771e4e5030a9a92a6b9e85704b6f4c410ebc6a9a92848a64e4aec95b72953da305ba0afe253dee8eae9204505" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "32ecfbe54ec7ff5c7c2ff3e51b8cb24e891e8371357409d951d03246935ddf1b", + "proof": "de5d11bb882be3d8b48dc08ec229dbe222cb821c24e3c8ffc351b2c50d7faf7da87075b5ff0d73e8cc9a6a980cae572022e0493d4f1c3775e4a278e5a20a4237be50b514bd15c68e1b14cdde8f40328a32a3341cd3c9aa5337a8ec481d4535575cd9acbf977be633af0f1928444a84ce151f4340d7b9cdfbd975a38b195bf56c75aecde18e131969838f3ff49e7479e2bc06365f2536d0e7bb784bfa53bb9000a275facc155003cc2f46f40619222ec8c7e44fbb8870678b37fb6628ca00f0088997734484dee17e68fd6260f431064cc772d3ff27716318e95a0f44f876c90c142a0c63d9e1dbaf81493bcd933218fa20cf9aa3f899e01babae1f6be1776112ee4704ffb27bf4f9a891865accdac407b125f5ea3cd866a044a9dd6341f942534a70cb6d7c378b9bdbfa54bd60a6c47c4389e4b8b89adc65f4585e26b643d01bc80567fe8da4e425d3954b18b78fe9db14d91abdd378fd49c49d1d1f2bd385305a85d960784ab96b6b69806c3d61d189cd8521f76fb4b4749db7971bc4bd5d586ac56b0435f5fa192933b5f9b68ee973285df8ce8268b21185ee7971c04bb07a70de838262885a7309d1e5ac73cddda3662f723e2b1ce88210b50693a34ed56918bd7bea7a82436a3d4a8b1724865bc3893bc73197849b9f41cce49716716f395ab9a221665f11d716bccb511f6d7c781cd2755ccac282b1a2d23d2c9cf455168801af31c067c1fee1688879b0e76799a974630b295846e459b6d8fe64a00b37423febab2f9054b9c97a9aff6951213d5843fd87a5b1fce853f004eb6824bf2c72424459f0015dd3d61d783bc5c7893f9e9a9c7bed39dbefd18a47d2b316131f66e1ebc2bf5a3ee20c1333b932847e4dc9e91cd8978cc6bbd3e252acf3db9004be3eace3e94102f0b4675fd3bca3c8f3a1f726e83075e3f8f77eb00a12c75b08" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "36fd99e06a8360c3a7a387cc0a4dea00d8e886decb2f6614db7ca1b8ce38ea0e", + "proof": "7889b2b7ec6c0706ad017b5bad45303f0c47bf4e258f36797d739eea90fd245190cc59c849283ab9dd09807b5532343b002f54cc47c5e2fc6dece6185a77b22a1ac6c4b1737f9ade08f2ab0c45bc9babca9221c397440d538e528cd1896fcc0f72e918fd284e985174b2e90cd6ffdcc12ab1a5df142c441f330b9a44a0703957467d2847d3c62a420a795d8d4fa4fed6bc76f4aeaa9ed6d4f3647a62aaaf7a02e27140cd700dc43ce57b0b86a4fa23b0bdd83d845dfaa65bc4f608d2ff0d9207a57de119fd8cecca44a3aad35a8f3bfa025351d9d8ed056f7de4fa814e384806e65da20cfd99e9da046529618cb37c97b65f8211057aef6eaf199cf3963c5b37cab0ddbf7b5d3df54e6da846488b6ae40ed04584afa0bc76e2fd68a76040335892492d1e4c7717d4f41861766f7bbae15819663b6d88aaebc2b83133b5897c564065401f9d978af4e91db4cd1432b42e6d6c2f01ff4cd5eebad7c1ffc929617006778e63f3522fc2cf0a5a7aae589d31ab0718b2b2fc654ae1addfce7498050fe426a825f0c0c032500139486550331f89f9a31f0b961b416f1de09dc0668e498ec362855761a49f184a64a14ca3b7c496583469a75e01ebb18143b0a9a12442e2cae776c7005653515df902326c750685b0ae2c6647503282b98f7a09cd1d58d8d982522c5f280b41d16e3c358348895e083604c0ad85afcd6baf299bad3d24bcd10e792053d8cbcb553a171f6d6536bb96ed3965742f276848493a89fe90346879b62d72b07291c680438d27695b51189021dfa2e38f737e76fc2b39135941a005ab3818bb9cb989ad7174a95ced7988a38c4a67a28cdeda6bc16affbe416ebaa478eea1056004a082c71ea68dd2951b8a8efcae89388fa0c8ff92e7a2760912b1920dbb9d2e48c5999f9e1fba30b5154f5d890dc448704c8c36b0a4b5770d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8adf00b0ce24bf66b30cb1ba98b94e326055ff113edf9c732f02cbfc6cb25e78", + "proof": "9a7573c7939ab50185dd1aa2bb81d165f658e7e19e72486a0693733b2ff8646d2086fc86626ecedeb4831f67f63ab619cbf59e9fad48bd33ea1d62bfa70240681c93c47066b7655cb329b02f5ce647754ff70b2510da804eb3b2dd948469ad2f0efdacf6c0817138c38b571e715294648a10c8c2dd5f6c96f4e50aece5ddb42a655d66647d1ecd432df9845a5456a9a7af47bda2cc261f5caac7d2505860730fe4d2fc19e44883020104d722fbe5338fcb14daca903e5bbcf151100052b50f067eb4a1edac3836533f50424de8ebfe232287b1611a426d8941f359e31b366f0bd4b60d02f9b0d426decc3b9ed0fbc7d3bfc24c0328685f238b7d0e4f8a9677475a1f3e92beccf25432dbc5ed3c451a00e61068593d722aa1463b531a65d21122b240cd21c0a1ca12e6848887bd7b20c781a246141e7c412ee091dc3c443f4c45f4475f6cf63f5c3a7585592ee3c501bcc7b8a9932692e39503fb1903a2ce797d783967751d24ab378e46fbc7eb305152a3cb305bf621506ead3dc6b5b123635bf4b583a7d22098930358ec2fb087408320efbdaaf3c0e501f89387802b91b15a820b2976176bdfa1b0ce9ce54523725c0ef0dc23f27f838ab2ec73847783eb57365c121b20ec91f6bc60e161160c060552b582d426dff31b4f20586b87cb0d304c4a2c1703f8a455b76e5452c2ec2d56568f8678a9cfb5e85feb4f503e83c66a78fa69480a0919c5aa997c785359e0ae6f5cfed7aca60594233d58c041d81a396c84993b7268601bdec2f0fb2b273c2a0694e42ea768d91a82209f7889a68d7248481c67e25f11c20b6a772cadca1f7ac38aa346c7d4610a33cbd14730edca2536d3b7e1381835e835b98a6697a55d0b86c75399e05f3935f6c04bbf9d3b9809eccef3518020e8ed397e64f993a056821b377b344f311753467dc59f2bf6830c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9a7e3d86ff66843a884c5e17f118a26810b91010e003ab599a6d8c1c38a6b025", + "proof": "ac6d911206982d869b2d2d1e607c2b1205b3f0d51bd1d789ff3b88b804e96532946054ce3651101dd0b0f88b1c8df6adda71fb90f8e621729bf5df4f4ec13e4d2080555b18341d6b2160bf2f7953d900740cc0ab7f8efdb619c82001d24f9865100f27877d9b6e12c0849b006744879ec8dea54c081d83fb6cab05fe187ac46db8dd4eb5054fe26af44827c0047364447095e9bfa2d5d848644d79799a4ab109cac06f17710bcf97c36151ea2e735cd1c938dc2bc460a0c3428e0be03058c20e8bf81946d1ba7e9d1eaefc8d269080c3ebd0650d640d3a3c135514d3c5d4660e0c8f96c51cfbb32f628b236feaefd4583b8a7e6f2d0ddde3f5b7ba7e9e3df80ae28d25a7c6ba9fa56db4ec7eacc2d4b8b135a61d7c0c44ceb3f4872a7c3c1d4826c2e7c64eeaafa5345697504fb0af217d69efe0b8ee05660f1ed13a7ab768369e1086e2e2e298c42c255281059ca2b41fc7cfd4fd5acab222d1707e5f74a139fe0cd1d0da7410cc80ffd7ced17e6dee2e707de360ce1b94e2a2e0c9cfb00f0478e687ca0bf116b9637b33250291390e0ab11e7d5784d1c279406404f219020ea6a508806d82f8d2262d8f890f810b71d624992543d17d65077dbd2cefd2ab24007d1ea105a6c8e85f23ede17cc46c32401b46891c8062a9aa143d7c24266b211a299d52fced15757a4c4d25b76fdbe0d5351000adb35dc443ce2ff7d6bb4570d8de70f61ded864ede8fab75801b506be9485be4f01b7abc2c00711fa45b325fa825c93bd1642741fa416bb962cbcc3fc4e2782cf54a8f8fc61c8f2f2a61ee390a2dc38f6ec309b1c71a8b9368bd319d83b9f1d07c5e720e16b7770b69d09c299c774048e6a5b02f24e4e463048e74ab39f94445d145d6ddb5faa05e8a4f6a042d6d8bd08e9d62751e745d8f861aab482d72131499717d310c6b0a5938e71c01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a615ad7e90763895e31312e3fe49458d1baac1a5b97637f66f375dbb418e0453", + "proof": "683a74fb876da3b5756464a3afbd1101ce92f260cb505bf768c018d84577ec0ec805eb2b0423a4a041c2192085974eafcbc51614c56fec5da97b074cdaa16053809f45a1f9d3ce6cd45b193eb45b7f16fa7ebc0731f15fb8efdcaf9e875e1534181b32519e7372569625f0984be80feef04413aa058f1423b0fbfb11dedd99723f1cc4d55ae8a5fe6ca5e99dc48199b8ba73cfee1f5efbfd88362ed000d5cc051a7b6c1bdcdf1d597d918baae46873026a9b103017a721142f34b409fe12750c732f598e213fd3fd9bea757ea6e8be19263be2f3b4751ce220e3bb9b5409320d6aef5869802e9bfcf0b80ef696015f95a507e774060faec5f25066a94450a6658efcbca8a54ca1631d7e3e63522166a03e8477cf150ab2ee7f95fbfacc61327890d4a5c56f6b99494b7bf2b4b33d04dc31527f56ead2da34db7b9c1d4f3d30420ed56d2c45d74357dfe4a211ef24d5e1d340fcb2aa4f9c3ee2bebcd96241766d0cacb316f2c0766be6375ad9409b8141dbfba2327560458345c5311ec9094c2ae6b7e56724b1264cb0a0e26ee96048b89baafc817623fbb76ce26cdd4108ec3920a4ff415204ded68160bee318487060b75b0cef69bef4e72e33b017661ab5380ce73d8e7651e2d9d9ff580e8b21580c57f9d6fcc2585401140f5581f0675247421877355126173f0c9f8d766cff1812d598030ee60bf85a868a9773d3acc43878932090f16b7420b4d3db8899b7381e5a5354fd736e10d81b3590484e894716ba6b652df0b3c55d4271e5fa29bff3aa53fcfba3e8fe9f3a62b3ca0f9bf9ab138a87eb845d3c4d2288d3d762e842c95c4da7eaefcca6f00ee89cfeb5e2ddd811c23eb041c1d1c7588e75833574153d5bd590ccd358faf476e4fa987e33a236070adce8864c7a756d5de57eb766babc6bc11dbb60515a64d5304d9c1c95afb301" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b2d67240a9f563c2ceee1effa08a419b283a85be5c68f79ba65a7b4abf74044b", + "proof": "f03fa4d9434edc44e553c8ba795dbda77cd4fa7bc995de45d0d7d1630b2969279cf4eeaf55652565b1558b341930ba6b66aa15b1080e496a992db0a6ad482753a60633daf2c7701c01e43882e7dada1ab41fea72491990fcaa8eec01e9474b6c8e7922b288d8cae0be2ad4b55dd980042e9b534d5ab4195df280d0df6b3f6d2b846fbcecb70b1e2fa0e569ef2cdd0601996f788ce1e874ee2707739d42cf52007ad2b48f007306ee4a7b999d5b58c137df0c6f0a467e5d57cb66182625a40b0f76a337f035f2cddcb64cdee333d5ba174497413c2d3d513b02d0aff0fe3480076a1f429d055591c37ae0cf6f235822eb657b0418cb9ec6712b0a9c45b79196759ee42add11a59a2024ce07e578cc9c8cf09a7156cbf2a70ede7195b65ef0666028eb3b238c84248455957b0fc1d3747ccad3eb29f53f200ee5dae475e8db405cc445019de1681c9f21c49d39590cc0de993367a22303858b9b275eb47a14337c32b285c8161144e6f56b261516ab080f8ce6f02d3fcc953f61c952149fbc9512b070eba53b7f781d55ec62b749d96d98a2a3500edbce5eb7baa83dec5e7f946df87e415e7a168e7c0acee14e81476dc912061b40d337586f615fdeb23904f902c22c47c5076a252cbd9e39349fb64af34420559bc53e9adc03ae7655adbdfe2d5637ba1ce8c49b08e461b420b8e6c9d754bbaa710fa85747efb6e972fe71787830ce8c71c42ae460ac991121211840b53b6c6e76f60f3e8845fb4b8dc29edb09d061412d3b6f978bba6c51b9b5bf6d98d77acd995a7106b29b82aa2b7ba3d652e2b5c182d72383423de219529394670ae5f074f95fdfd3c4d9e133a93f975d4122f97d637c1b05309f6f16485ea2ae2a8e61afc7140b193060a0edb4eb895a09d559c12e9dfb83eb86987371d507255620e2e5227492d5baf2ad9519f47a330c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bec81c5c5898d299a5497340eb17b6b7315285b864331965a8c5218903be9373", + "proof": "14daf417305fd6ed5f64e9cbd97347f0d4a8a208c5f01256c76a268746ce0069cc35d5d7b42d4a101d52a2d2234f8d296e2bb7ec2fb10e68f18cd376efb2a74f9ad3f295cca64d72a1ba2b5eee8d3af9561f29bd2d6ea2b6582df4490a87113c90108f9aa76e2d08d110081d4c18837ca7f51d8357f99c315ec76454d92dcd0cb1a4ba0f1ef203717a3d35c534447f9f5a51f7ae17bb641be7e801688a69b502deb60e87eaf624a55c972411c57b960174f51f48bf0cd48dae4f165fce7c9b0739aa1cec33553f0b0a4e30a2a70285cad0b971435c4735d718490b2df110df09f0c26c918da8ec112b46bada03b1577a6b6a8b37d1905e818f727544ced7da36e0b16eb84e99202ee3c4bfba47b3f3dc8795ceea6c8a85544f26448347e7955e543e116f00d625f9ea187e10a7600c980db7a29600bf82252b9f331b545c2f1dfc9899026464cb75f53b9283296f16816d0d9e52be2dab0fe56146d1f22768141a7a038a5e4882d0ed6415a15f49f2e49cc60246bc9e33ba80f7aeff6257ac7032e57a1ed35afc2d83106eb586ffabbfe037ee234cf67bde5f993a3bebf48844d2d9cd1aff175167a967585e4dce017a0894babe28f523ab56f2995eb9151d37e2539961644fcebb046c330fc1f725edd4508a57db8842d90625a44988f95008786ee895afd932cc70b3bb02b64f345192bba0e234d09cea5892882e511b3b124644b33e97251d4f8cf5fc2525e7c958ab6a7467435baad8272afaf27ee70f28d6794607588fc58b6eff589e08bbef551fc0cd51aa0e0982f486c0828866687c92fa29f4e412b668532bc3a807fa9a8aaaae1d887dd34f4d1d69d62ae5b13d31082a90650ad0452cdbb85add3e4e4c2e230b0f36f7edefeccf05dcb54b22d90f7a929154d00ae5c7c6a241467db7d6ee92ec6d873c42d01f6e860d2bfc8c9f01" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e8c71e0782b19716fa12ede7d40810090c3c4292a9e4e531970803d7ecba9e26", + "proof": "c00472181365d29e16a62f249561b952e93ef2f38bc30e0e0167f8295bde7172fc0a2f8c96b84ac5eda37ee5739dcaf8d464e5f50863b7f508238bf1b77891117417c78d64e9ac8c6cfc50a111e46fdb7cd499cfeeaefd9979f4fc9f62c6cc630c77fd597be418e6b31eadb6766f78fc90a64f14bcd4844f298b064283f1a9747c7426c23144484e77ea232927b6b664fa174fb4bd850dd9d001ea50c74cd1057ea820be488aa060050a4e4fddc5d27a395a2f8fede4b513702e4daed00eb80e3289844bfc486c041812453c987753eff801f875b2526a61c4f6145f2ab0990904932d33c91c64fb6a9b7ce15df49894be2402d139e1379a615a4a1ed85d5631a413ad3ba00ecdcb3c91e2bd4bbff9217bab566c53683ffbaa6da98ed12c337a82c9a6d0ebd46ae9931aeff1d964b4b9eabb3d3fb6409965d69b8ff2ae9cea195c8d460f359c471816df57daf5df695a913e9e61b43b6e5331a0a84746705918f45b229d30c06647ea317978f89aca3f11500cdff7a65a97fff36be2af2f9272248016b6a3aaa2ffd4085a22e995e91973231b9531bd5da3c52a9c21157a3d411e63a94ae66469a24b8e458b2c5d8e8c0691678f01ec371466e3b46aa476264f02abe258358d30f321d9b77617a45d3fc1b33fe800c554f15588c69ceab2b817c80fff22e893009b42ef6bddc41401326bbacb1a0368e4f50b838cc9834bcf103a84f1f831b3856950e0f0158c845a4f674ba7ef3ab89c990f08e782006f4108b42a12945633a6820264d32277ddfc3032031a571de6a4d6466d015d2fbd874be69378b7f4965dbfb957d54ba6fcf0e1f937af6e7d3b97de65d40ca2572e630867edc79a9d3acc76d9fb54047a7d8744c156f7091965734855710d6d81b7cf021d55cd574141ff662f6ae299506424161055dea25566f8d38aa1a1481927a008" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 41 + }, + "commitment": "96d73daca6c3fdfb3409fa0bc7fc0ff276ed417b68d11656d763d2163fbb411f", + "proof": "8cd36514b2d66b68b13fac2ae067af773d853a167e6c06ab2690e4a9d90f782c3ada09b586033267afd0c80efdb0c2856d4e04e0e8ac96420e30887d39376d0feeb9e7c3927e7d64877996156dc7a9eb09225fb51ab633e0f7f85501ab0d655992a4438b5d0943b70b74a52623cbc8ca86fa65dd1d3ec98223f7545ac43f207e5cee6ff03cafcae4d80705b21fc8e5b049090d98acaa32dc33fbacf8f320d403cf440894e885dcf1d75aa03d58b9bcfec516b85c7beadd544aa97d7e226ad900103e8cc8496fe3e8076ce59c188388b4bc5a61dbdd99da36bb6d4b6b7db897029669f32e66b67fbe6bcd85b5187726a08b9d19c40f974c9b9957ec16b0a8043904011fa31435662d8c59cc9b035dbd3e3a197cd5cee0f1c1a414c75fa7e3b2403cc51308129a5ed69b9de5dd9994c0d8693bd6ad9d3d49eb2053803dfd5d94390c9c2c3c89a6415797945c35cd5e7defe914a3522b47a717db07968468ac571c463da6f2a7a7487a83f6b5409d285e8020afc5545b0968199e9137ade341583ea89bbaa95847a27f7b64ea0c4bdeb42a48668320b2fc7e92ac26c8fbd951f25f026a9f211b5549d84b6d62023f1a1147d0c90273588fa6a6f4e1a4b3eb863c61384d4cf98d21c1b461993593261e67f7213a891d29b42f81f76fc29c5ea04d41de385b54850242038c90a3f86a6137ea21a99c37ce6683c49359119908cf8f3f248070632c82f8b3e7b26879e40dfdf7f7517f4235ad7631268c69bfeda47574e283fef9ede69132241894c0beda8224a1e872958d11e8c7a4a7dd71400ec127a23dc0f945469f361b72260036006feabff5947db55548cd0d20d03a89380e72b69c8e2e641e1e7360667befe380f15fa3f83d54e2f00b7d8f3b4fa72b6eb50cd46052095c646fd5eb4eb2565dd1fc2ad6c19ef74613f8ae6a843df5c61b910d" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "301315aee34016beac49587aa509b9caa3c16c5349d08802cd7737d521675b78", + "excess_sig": { + "public_nonce": "6a923a2a3874921f53ec71adb5e918dd1b2074c88a1c1eb9e95f3c5e59214f22", + "signature": "bfa24146a195f967901bccea02461b15082249e2da14ab5fbc94ff405116c609" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "5c484dee52fbdb17314a1b381d5114c90b662c138a3555007309640fb4686205", + "excess_sig": { + "public_nonce": "9868170349b77507a856c990dd41a2936e9244ccc6e1461acd797ae2cb3b6865", + "signature": "ddbb2cf39d154f6eddda6f065290e25566d74929439f964794ac7e2eca04c102" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "cc3d5bd19c2ffb3dda352f86ecc2ab33bc441b3390f3f467e06b110997707329", + "excess_sig": { + "public_nonce": "78437faabe841a94a504e8a0889de27560f605076054d0af345c93ff54af7c3d", + "signature": "868d9c1c13c6ae209be13e66a2c57e7c1e0d862e468bce3f29bf9e79a3040504" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ee6c75ee87c807b6d93e7412e69f64a1a73148ac665eafe9a12f9c2b44b01751", + "excess_sig": { + "public_nonce": "7cb4623d9a551da09a3076c2ce784e4a7d655a5fce027e2155a79e922549f127", + "signature": "11f3bd33ab77bff83ed961b016799ccd56b5c521cca6904642048eb950d73000" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "f8331584b576a1cc63f3de04ae27b8c44e149f32c86ffede851a69d119768749", + "excess_sig": { + "public_nonce": "104d255bdfe9ca16c37ad39014124ec2aaf9c1c85050687a169612f0a58c5b50", + "signature": "5ccc9868fe43f8f03ba81199beb3b26cb45d925b2d5295faac13afc348abb905" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "2a635b204ff01eff70d4f91eb317294d90948b4204228bc91ed70cca72d31f5a", + "excess_sig": { + "public_nonce": "0edf2042d32ab73dea89711539a3922326fcc7e66adee8b8c1c054fa33ba834f", + "signature": "7807ead5c38531068141ed7259503debe011e09e94c295ab5fe8ae465aee750c" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 41, + "prev_hash": "77742548a551444ac74585531d3260caa363e65551c0dfdda4afed0151cc8871", + "timestamp": "2000-01-01T01:42:01Z", + "output_mr": "ee4a9844d8a6feb0085444dec086ea5397319951fa416e75a4a04ca160e73ef8", + "range_proof_mr": "fa53a9b7c6f29661132cb5cdf85d66ab494358c73205341c9eaa57af4e967751", + "kernel_mr": "8ba49af9a188c47ea2a68b36878f36f5b33274f6561b6ede3982e9419ef7b78a", + "total_kernel_offset": "09b1638f593ef7a29472ddf28ca5282de218bd7abb3feadab49092cbd8d7fe0b", + "pow": { + "work": 41 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "32ecfbe54ec7ff5c7c2ff3e51b8cb24e891e8371357409d951d03246935ddf1b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "36fd99e06a8360c3a7a387cc0a4dea00d8e886decb2f6614db7ca1b8ce38ea0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8adf00b0ce24bf66b30cb1ba98b94e326055ff113edf9c732f02cbfc6cb25e78" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a615ad7e90763895e31312e3fe49458d1baac1a5b97637f66f375dbb418e0453" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 21 + }, + "commitment": "c0e32e53349cb25555485dc71b24fe3d6b5f146001ae7126c14e942944ed5b58" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "080945672643dad18edf2c27eaa5624760bbf8d35b0712ed530831025fde4b15", + "proof": "449edd214acaf5253c5a7b893ec0d0d117350a768a2f08ef8ea8a750c140e35d9282c7ab4dfbc564878c32fcfffcaa9ec960133eaf7f5ae146363a8bc0271d36bab663eb775c1cf3cf88763fb94b486c916b64dcff745031e5898525daa471592c747aa80fa2b210569c04be8b264cc62367265ab9dd8d5f47f9833b8ce6ff5dd7a98012cd61a846020d4b7febaf1ed0f35a92a26f109b49b2e864cebe42410a93a47381869e5c71fa2fe86f06eff6425e5d77fb2a9d846c6d59cfe05014130d5aa1a03653c3a3ee68f964212841fef93f8ccb904feeeb8d0e5c83c47562b504d237a1a9a9d6860ef2acb3d2c8d5064f50cc511e22a186a8bf0bef5bb4859107d4d2ba46f1cc46809bc166ba944b53b7162506938f1348d8b1da491da69193799e4b8a5fc028814ed959a37ac58db154c40bba97b4d62ab69f6eb2d7036388178697c03ae6c362f03381fbcc3d7d359113d91f06ec920bf55dcd55e37cf9e42e4af38a6da3a33c462bba86ac1d009f9536a147bf4c645a3b46ba2e31b4e9ea29c05b0777c4b4969861ffbaa33e385a94f3d0d3161fe8340b20ea9b2b15cada448881a2972f3ad7914b57a04bc833faae8bceff504fbe1e1299d8bfebd0c30c1760569330cc65c93b97ddb50f260edf545b5d12fc48c887239bcb3ee1e1af7b7bbcc901e7ad9d2388d0d2c2ee988497aa9ed259dc2cfca23fef4eb3703eae374230db36ef58e099bfed5424abf05fab1cab59d23c769808598c1b2f778204b664a4f08fbeeffb51b1f8707880bd1b3c2241220c2dc7d983c215edc62f4870aa34aad85e5c93805500a94e238652dc9753d44a60032dae9643da0070748c4f540d112ccf6e11bd11d8a9f0b57084f5fe8a6c7fcfd986394417a9fdae8a9ec5560c6ab1eff5a8cfb33d1c1a29275a75e81eeac4d61557388ed13f22e1473462820f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0e5fe67be52cbc67754e0b064005df3d1d4c6465ef95bec0f7eeed8e6adf4c68", + "proof": "7ea305b3955c46ca6a2068048ce3127d306c27216a82e815bfacde34dabcc10a3808596b36630cb7134f847be5ac325fb5391467fe0871141abbaaa3d163c50b7634b5c1950919b0a76c125c83880c305c967cccbae68f15ef63729fc2e298280c3f6917a62259498dc2cd944a37a45fb0ea7dfab9cff44a33b27f3bf8d07a7e6f4e6c2b9b6060517828bf439c7e8e0c450904fac3d2732dac0eae0dd24e100bae32bd7e64a1ed96932c0a47ca1ee156ece148692793f813b7647433c3a2790a60d1b99279936024a89a15e30647d501fb0758dc78c843c43503e006d6c4e103a25a6e95f3c6c9656a2c99ed9993af5a3273fa1a4eaf02d85436d3b35d545634c213b1307a6173e1243eb90fa7cc63cb18c6e4669308beae1a80eb66cde1c84a32fa24f8715200e6482059f8f1f77a2f2705d21dc9e723c92b5315ad1366d403aaf7b1fd8301dbe32d808c4da9fd0c50e7b40ea051ecc8f1f4ba2c7f822bc45d3ce02ad79db0b64b9ab2e99ec2e5bc22cec097360fa9c41fb941b014cba4fa308edba4b1a6aa00062e82f43849f4733f369e7548a626b105cd17eeded82d550f94943cd87ae7036324985ef8d826a1e2ffbfd0d5e1666ccd1c9587b8ea8fb8181e776f9a87a530b5a50033a8df57e0fb9a83c34a6303bde55c91edca1327b90c5a5bb6181cccd4b26a6559a1facd970ae09b10cb9ee83e2165d44b0b9d21c05f76fcdda67c7becdf959e52216955f673b9ec00c39b3098d039f72d6b3e354944d8fab97dd115b6f1fc80f336066457b9a0a4039c52011374901642105cce98276c20a22a90edd7a83aced3fe46e514cf8e30856e3b8f9e06c0c30f0acf5e4361fc7c9e70a6d5fd3403253e0b69617a5a6330a737f0237f46a21ae79a9bba4307897659d50f7a7a1ebf2d6e46df4b420afe81d22ebb14cfbee921d086873d2100" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "28051606a63b54c9c8490b1081a8c78dc009c9cd7d9c284d5825d500d39d4635", + "proof": "1c8710a5317539425f88b383691d7c8e470a54709b2f5f572f7eb0f732ce2b338ef6723b35f6bad37a8ba4c132163bf49807ee0eda421bfc2cf787521e46c6757a9211da0958ef6e0796078fa20f7ffd6e35c73cc17d3514189d5ecf39972c1598d6412ac3c479de67616a7e03801a427203fa4e3393c10c0df6da0586232861672db2dbc2c3dde84ec544e65d53944acb50bf5253b4dc3e8236977f175ce40f38623c719a0fe54b92e98a1954eee19569c03e5f7b1e65dd7ccd07eaea7554017e555df9fbdd5114d1399efafdfcd6527d28c79c86d071df8c5c0e500cbd1d00e624c8576febab0bf8d5bcc76cf134819024f77675480a5624de88a5a0064c6728f2415b9dfe5eb6674ba014b3bc9f8df280080c38fec174294e7df4171253425e3c0ad04cf4eea27a212aa2b17e9ed3dd9a3b4c80623aa733094e769f2ca661d0f8f78558608d2910567f187300e4599aaadde6068a7cf4b4a1f50e4476807dc2d73a94e1ebd4f27bd4ec1c40c4c4090bdbc9f6465e9e7205ea2f555b51694e6481da8e33715c6fc19b507ac4d20ae936a067c319b78875bb63bb6d2350aa7df08c216d06a7d3fdf5a7e96c9cce468d9f7a05606bffce27f700e79e6d7dde7f9c254ba0d4c2dcf22cb1d8c6c32eded80630ef325a9f80a6f5e08a6ce8b995247ac006a1b6f58480db1ea4984f8d7a1730c6925c6cc039bb9438dc019cea4f0a5aa8bffc76108bbf0999741142eb52edc42da0e7776fe6022096e1373d8d2f1f4e674ec8cc16a86a4bb3fe676b62c1fa34b8293300a08a2ecb70441e73e3736ad439015ab23db3fd5c8af00fa50d3f5cbccad092d64d2127b3dd8ff7131b3b59bada89cc24586f5256bccdd6c384f8b0c579f005ce9aba12b5a3450ca43f36051c500ee7797d39e0833eaaa3dc706f33fcd9734a19979fbb03fc6a07f0e6320b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2c48a1559e22e6a10f089a5464d1085381060b79c58db028dddbcf0360899272", + "proof": "8c10cf736769af53d665b8ea0456230598912306e0b9756245a34fe14a97a80ed6863087a282b1df9a0abb9d295ec20649bc94af9f47f10f2fbf8a0e0754ea112cbc727f761d9d0b27742cbe65d18f60f5bbe7656260726ad8b71a9cb88221797c9abf89356ce88da0c56bd954ca5051ddf9f21e631eecca1a90bd20a70a3f46616e0d27257ff700fa9fffb27cb420e967007ab6449e1b2f4932e85036a2d60e37d66136d10cb2606112078a9f101c7f294ccfe2b084326b02f7ee9c840be2058ac1554c6001ed6f9bc5b2a10197756133cf9b011df12b2fb3a663d33f195a00cab93ff764fa4a2d92b55570fb5afd3de97e85f45f1324dcff352d102b2dc12cc64806975522627e872d8d5fdd42bbd50c0ce99823e01026d2b23818d072f8773cd148bb47d2c9f735cc6a50dbbd1bfbd04a1f22a7979811e8c818cbe6e62b0ac47f04c0b8dd3ae746db40b56e5b12069dfa0f52479812bba6a59dce2943316f3c19eb62c0c3f504632cd3e3571ce37e4b5e74ef4aae4c29d8dd1aebe18828069cd79229a3bbcf7601d42c1fead7c1da64a5f09d063350f24dc2240ae1960433aada6b827a17fe2cef1d3e88b7abbacb640dc924369c72bb0f41bbb39410cf77e2b7004cc17448929c2b67481f43449e85028f2fe2bf7d56e88491605d52b4307262fcb14899c6fb68dd6772d83f43d663942188222cd7c339c52a354789c055880e042c3d78f1d224b3cd4ba2262315e0f447d211d9a9455fbb2995e1104510989b9f63c8c325655626fab0f96ad4e00d18772b2f1b01a2986ff5f0d54c6b37508137b14218c612baa7b5bd91fb1ca3b1fe93f6774c88297aec934dce3ea96972b551c8d50ed548768215bbed8899c6990e7f59b3a28c60ab6bbe9f27120103116ca5262724f6e6b3e0b45de5628a78dce3acc0295c0284ac2de462b5c2430a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4a6507c8333b22fd3a6920d62141771333fc1d3cdef7965e617c865f9da3626e", + "proof": "10356e21d27c44c70746f9fd3e013688217a011ea5a7cfb27fbdb6e10e7b8e1290934557a68827453e0a0c38eaab9728b2b7b20f50029504c946880cf31c56031ee79dda2c337d9dba5196808a23efe4bbc7bd6d4df960a6eb6ad345c287a852b6581027610bcc47269a3c378c567744b8ad54787291b5713b3f00d87940ea2a17e580b23cc516684c2ae4ca86d72462ef9e6024cb18bd9d5dec0e9203534204fbffe87a7f3d6555b308808f619ffcb6827ea26c8afd1d3e3ed423229068920099153043b350a3695c49177f4184c3831bd461884ccaa9f8fa633ccadb68fc02b252fe8f3e17023caf875da77175c6720762e4b56de77d7b0eadb290504e902a1238a071fd6d7985c0959b28edf188d01c0d6425c7340a69c8c598f86feff8131cb1fb015e9761be7d9c0ea82ba233a7e05fda6ddf59e2e37f82511b140926605e315a785f0ea0c6b2de9432503e955b3ca6ac2732bee226caf77d4fc51a2049869c7c8ed438dd58adca7f4ac0fe5e3b9f032483d42417095460d4e55cbbf1406a1428a1d223b75a5ab37ab4123c4f03fd0a2563b5fd9bee3e5ffcfdd468932f2c0bc1d72541b3c2d2aedae84cf77a70afe4c26df3683b7f1d1b9a65c9573323e8fef87de99cf8fa8b75df6b181f6e62ea51d84b68887a2a201f9093d14d0071a404131e1de7adaa9851aeb2195ff5c011e1168514c98122c8afd7c7ff51406e9e783664b0448d5530b5cfd593b3d46167358fdff8987d1077f493de68946022867a305d5f2c695461c8ccdb201af1709b118b8a57e76f1e7c929d4f4641ce55a8b2841b076b89aaa55b93679ceaf730bbf18e67b24ca7da025f0befcd29c05940a526868086e82888dbfe99bb8dabafd4a7c5de76442a6b8c64cf56ea0b7e0f676208209bc697c79b3cdc0560190abe01fda4bd50812c4ad0e22dd03b48c106" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bc01d0bcf1fe08cf96d3b335fca2c24af57c9122488e918a64bcbff50072eb42", + "proof": "2296e3029ca464c002b27e0e511bdf56e7a7f771c8c869f3806c07ac1d49992c980c468edb8df7e6ecdbfbe76e2b6158d9aa88f2fc9b100b98d47cd5a8acef6a02dff55eb5be9d3afd10ee8dbfce1b2dfe13669fead44d4b61b9c8f68a43d55f2cbd14ea460a40d5206dd59b946c0f82a2f8f0e2277affbb053f2ac65442db6fd9c2cd710ca177268ad0e378ea151c46b65c35d5aa7c3cb265003fdb6d92f801dd68b753a850a57e61fafc0290481606bb345c1edcc77644e57f16d37bbc8f0eb5fbf719ab340460f0b6dd2fdd81f95abd366e4ce301faa27b6053c863e5590dfe1b24122c7a6216461e45918e48e4959f4239f1b447619afaf2c9238eadfb237ebf1ea04b1da5c1b33587eda9eca70a41276bdf1c6df91558ff8d5b44d2b45d5c64f95f8fc53957876191acef3368fbe9d86cccd7e4ca8e3367aef5ac1d3a4276f4baf4a83f75c008b02b780caa80249393e149cd5f8072db58e6ff0268fa17beb28a0f08b0fee9c16dc6a727b51922d3b49c4b3391bda07133c4c9b9c8b15cfcf70855e661f87b5f6ebe1b341768c07fe236bfa5de307bea912f3b41ffbc19e0f23a700be9e0d91c6b315bbfb2a5f17fbabfd839cb13353634e60442cd420b1ebf66ee21b598fb53d34c9526f71ae61a979c2987462fbb3f8c64ac2e5b9423707db03a4a39c2dd6c5c3afe11fed1cf10e7c3f62b2fda53b3742203793225505627aa113dad15bf43b13899fdcc73639bd5e6c63c2adc96686e2f0c94cdee133c53fdf90a591688fbaada7876322a7989171cd6bd07a36ceb44473a5f49375b345e4d3aaf2d1debb98277745b6c0509dd8d665bf3667bbac13f12126564ec5ac6d2ed10539d87dab63d4775e23d0dca1c5784232efce81bca0a49a8a35fc40034412c0867f317473097256a8a4c3bc54fca5678ed5f4c6b31bc194c62624d00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "c0d6e0fa1d51fa324f7645bf43ee58f9985951812a0a80ebaff306d04bf64c39", + "proof": "c8301b66f987c3cc85ae86efb6bb91539bc998972bc670832d52f0b47403930b1608e2a68b4aa6d1d8c8de5273d7befa12dbb0dc472999e78a9fbdad9c9ce15f8c2be94feb1886dbee9c64085ff9f636b002e8c80fb5463b86b46e88b2065060f63a2b002be9dcc4a3920a7410bcf643c091f44462214d6eea2af86802092626283b27ba18a73db61fcf0b6dac6caeecfb1c4b27014dc3a10e385494274fed05f2bda04e5f40e6044e40079bc24265b2467bf9120575ab88e38303561512680d53fc9364c7ca390a004405ff299cfdb6f3f761dd9b12bd960568d7d5e745240b5a623a994ad1f5208a27ef2780e9d53e44b1d19072968b9881e69ac0ffc2717ed47695a371c43ac1c5e41b44ac7c7d9e54100c09791ee42260e33f761071d848a64ccf90e9a6df26a49643b24c38462b765fad0f2335a592317c11a4feef153b74ac7572241aed070ddcc347613a2edb80b3880c7419638a0eb992b19c25d37872c24819ba835cd4e7729e2f0a15e55ee4f3ae1793b17baa15ff34475617bf3e4c5b06bcd944c67da77a533e29324b08b7c1ff83ed127d7a4b410fcf1a449b7754d367ac98d9054d282bd6a984ca6f3eb23e56ef1fb8835f20b3abca48643c28f65aaf85ed4b0e0edb17410a5f8ea7130e46833122e0999cf1afa6a3b882cf0ce0134e59b2da556ebce75424835935c0fbeb192969586de47d0f80f5f4ad1971546d10b1528b5e296f0815e2deeab1e379023bba6f28a1e349b2f55bb9749e2304375e8602588be9f3423ee6c8f8d2141d428327115899a39d219e2a069bf903f4b8d2eed7bfde35d817aaef3814a1d36fbc54f89203559be4fe684fc78af479fd388981826ec43d79289b9a96de1e53af534cb57ac163998c7ed6ea37c12f060bf3feaa6fe5fd720ba4c20bc0a18989add61324eaaf5876cf16edd504bbec0b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d02769d3ac8fc8c772642e74f64e5effaff6b278462b57129a836d056f67813e", + "proof": "e6629f5c45ea801f38111acd425b64efc97c1e201b0e55859cd9828c01885c27bc0aa82057fd8260a1a681395ee94156c4bd6781f0c066dfb29391d3760c4357806dde35deee47a4c757d137c489801094a4b27e7483057e41c038c323336b38342c37b9d6d690140b675779abfb17da64e877dac05c1edb8ff24811eee451092983c37a7e69aa55bace73d9f0a58029114da10055c9ec3d8b24fc99160d730b0e96df3d8a100f29a1485106602324a56b5aaf44ebc0005df068bb0c6c7f6b053638d1b5e9f34455017d6ec56bccd21d434d7db8150aa1fdc8780a0b4dd73d0e50865c9d740d489f7042d62d066b44988a3c269ad9d037c2e7e6218ce7cabb08903408d1964763e6f6995a7ffc8ade77aa72f3de32b4be95efa6deb547faf565546505bc19cc3c645452f5d2fc6836c88a7b53407a5cbbbd6eef51145fc3183594aade880e12ccbbb9998e00fe9a4fcf4abbfef9168e01d577e35df57717f940d25ea301f56a2a434a912be3f4bb75c2ce68ae084f8eea546909e1bbf1c86363ec4ddbde087f93b66e43850a9d8a39e68b8c5f7617d7e048f3f4698efc44957fa00c537ced55f5773efbea1f720c084a1d60e04ceb5e2dba969bece3c5636a7c0a1346e222231555d988fc0589ca482596b29a6302c5ad8ef9782d6093c1bb01ae5d259e6af88acaacce42edce4fbbaafaf2ca3c89e0f05ca27a762d1a706a400601c84829b61d791e406e55c98aaf7b4f782e7a4f5b759e68213bcdd34a720f9225202d6badcbb3f075221ae889aeef6d892d23eb2964a9535fc79df120490950dee4d1f13a8d7ee938e2dc1a82fc997c639b3ab0488c465bc2e7c5ddd3cd3a16c5376b6cac6b5d7a548434ff60cb192bfc017e4f42d11717dfdb537f9b170873ce068f0e6b1884e72206519d01647f38ecaffb9d1f192e72b6c1ad1b8b7d06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e8d75d3f726e6342043848f67741cf50ec6313e82da83566476d97578b4e4244", + "proof": "1cf83a0bbbf26df8fffb5812edb384281c174cd8c9590c7b97d33e086bcfbc102cb9d63de5651b50b06072eca2066ba368c38965c9e93b9b14816d6ae4ecd14e00435576c101f605a6a7f47e30aed4610c1fcea75fe5360df927b6fa21127743c2a0a3ab4a44e2f214db2eea24d417b8ea1890230e939c4ac31b6468d7448c29fa01100f86d9e0b88f9bfd96ac9a8df3744d4840268a5f8d91b3d929433e1c01f78b730d4a45f5412adf519c9a4f3a883869c6b7b8b99cb9172dd3ea9476e50ef40bc08676184afa2748e58c8482492cea8fdfc2282a1bf0fd4ba1f1dcaa150da2e16b514b2f20a310c21572b90f8a31eacb87cdae8fa760f6678eb723d28822c8c4a5edfc69026e633059b1ac16cc688b703790c10be0be0136669ac5cb497ab0c8d4f62752970d5b7aed069d275cea22f27baf5ee68d276957e5384c67045a343e5be4e25f95dad64a1daa7d4dab9cd6b74513346f30d7ca78bb5fae4f140e224a868909fb432c634d6bb4282ea8eea2b92f049967153baf083fd100637c7c3ac28beecad823bcdf9c7896f09b4086e7303350dfd364e3662dd0f45c6b1c2b64ba899668a96b3f2f9c3038c8ab6895d1a115517211c5c37023375a91a00020c81eed9c7727fd508b025e58b5afa389738334f57d379d06bf3d09d9fcc9d54e68f3ea39cc2590e2a892a8ef48bd0935adfde19eff4e83376fa127a5800bf123da6f97f29d940f3246dbf0cebd1b34dce128dfd9f804bce9ecdd2ab29ac11e302ceffa671d14dd2d7e0cd433c65ed19980dacaf96ba84da2268e03ad896bc43e7ce1b8755e2f7e7462eb75c6f41e57dae2e42f030788b8a1e8845aa0220ef47c281a04beaa86757e90ab7ff4f2fd013775a4fe1d8ac44537faada96078b8c801aa1380008ccc7719540b2f7e71b08449c4627165aff6402cfcd79308670ceb09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ecb501cd0173031770507fbd935cec79fbc91a1e39c4f81e0429dede5c1dfb00", + "proof": "dc3c7bcc6823fab100a0431362a382aceb321e730640d26eac48354fe63a6c167cbf7b8dff5d13a497793fd252ab5791526a4302352377c7e01ae69cf96bdd23c628eaa2f2c84417cabedbe301fd1b507f3e8df64082a9dba5a7cf51eda2f239662f361c1d477fcb58e9d5ded6bee5cbebb2e870e2162c58d33acee3f079aa65d294ef6c52020e11f8ecee27fcae2ecc0f769c063f564db09552c29daa15230e758240ed1152f6cfaaa36a95df136e1d72bb3f54d646c3228dabf5e88bc570011f614b3a6e56f2a9b597b738b16d6d1e0e99491eaf6377c2b23268055d0c0006e240f9eaf251210c32fec49f160979a8e7094e8c8e4b0968fa4cbf00a501950f54af59ae260caa7b8a09dc6ccba91198e6688c18ee3e0e03d430af1a8d28f8059854f4d6252630cd93fa47509a154a60f4d3dd47cf49b8cb8eeb4e99a046fe7118d5adcf01e63bb0e05988a32a0a404a086ee36ddb21e28c74877011467aed594e71e3b10b4dea9d10df9a697034ac0d12918cecdbd921283aa5468436874a4b44a1d862d432081842d638d04942bbf5f70e72f110b9039573b1fa97476cb91a1e366ada2d2e2fc7de200643b7b208d69d1927ed58f4bd02cc1232b6f8b4ef1e4c611eae5456ec4d839c0d969b3ae9a3f10afd2df168d6b72af098c9a0ff461ec8a4c38e96d1675b11c0065aa329e74e5bd7e0685b2d5abaeb0c4804620e0d6cfc659da902b1ad90b8fad5e4beb6738754ae134a277f5d353372707134380140eafa257e060e8e945b74e6e6bdd8850023e468ef8529f56c2356344f54d6ec65d8571773cbb3188056f6488d5ebf0f19f8926dd953b6080cba5805704b987b412d1ce00dec2df8bbf00035c55814ff1b15109ceb34945ec3d1bcac1ec45380033f8970d98e88e93a94fd25b326c3b61943ad36c109ee7b419d184e9dbebcb70e" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 42 + }, + "commitment": "1c44eacd3b4c7e9c9b164012ee69abb88c8ca941e47902baa7c9f1b543a27951", + "proof": "42cc548c1183cedc71057531adda8fcdc70f10845d66c1eb15ba63f50593d21b76c4dfd3f51cfb87c8b8b532aedff5170279ca7b35d7d25676e75fdb6f541833821e284ebe386f5649f185fe327c7752a2df67171e8eb95a8856e149d9f13907208bc578417847b5f0aaa20e1dd98631d836ccf52c30e430f89297094e566428e52bb9eee8fe2c294c15ea47c96589b119feb36953dc65f3ed818719ce6c150349d6b0a20cd4cb47303de9780d4280a4f3114f3f546d88e94ba42fa243471c03dbb11eb85e733c0ff532505a9a112f929ef9d95adc8c63085bd130344a557d09b217cea0c438a4e66880d7d781dbf6738e867407a58cf74017a91b388278160c2c19ea8e0c9b552ed67c29eb79d81d1fb124596f029781c70b4da72cae4f7304841f3f1073dfd1edfcebd9ff339e265c7f95af6c9535596d5f86ef247069b7657c6b52f6f9d6605db4fc51c335c0c534e567459d2d7d11ec20af5df10b6795200a32c2bb433bb78b9d5d62f9a248ee5faa1d130d56890681ce5c86a8a6f03162e08e47252898255093b33cd0a5a259a675c56bd6ca69dc420e9b4021530e5c7926460bee2ed2318d793e01eda2d14d6bf1c8aa6f2bc1970c652f9585f7d4e5345ce17cc599da864fdd897b47df61e41aa508b5dede6bd3846df737b41e17b658ca6470cd8019cb0302bbdfcc0adb514d0f3b3f1e2304536f0cf51658c6c99808c2f2db96778cd134365a1d4aea207650579bc1037a6a04d1496da8cc3b351777105e0a4d9269258cac1d2b4f134eb18da19dd568278c34e1ec15e5ccecf7055f0c7a8605505775dbcb3e5d01ab61490adf4b6bd69fca93e4f4b4a302ccefba25f06c70ad262d37de59c5b0f73ff3323976e04c402f3d923c4064e3e6427f190632ad9aae1bfd95c3bbba9ed820c87ca4aa2d7a067a4b9a4b359198062d92fa06" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "020e264dd79735b64a700b84d16493861ce02b33efabb038f7a4a900e854fb2d", + "excess_sig": { + "public_nonce": "ba0222220e9449768e97d7340341204b41d603211b450ff04c8448fee472f71a", + "signature": "c709fe3061d92c27c227cfd7850f41f8673ba5e785354163f862460322b1f203" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7a2c14a3aafd4b9a71266d95cecc6192c12a0351ce36c03dc620d1fdfcc8ad36", + "excess_sig": { + "public_nonce": "b095359ff6e67b7dd493c539f1187704f9f10b72436e588c52e0c8d64071811e", + "signature": "65f700dfaf17c71dfefee0066fc57d1207305dcedc03b4af633a8b752e6fa106" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "8818cb9ebda6bf07fad5de814dbde9a71ed98a8e76b0c67d1afe36bcd9382466", + "excess_sig": { + "public_nonce": "948dc69ce564b55bcad709359cd62a42ed1f5cf700f966cddf7ebef8ba38b760", + "signature": "3e52423262d67afccf9d6a1ce5c38616f1a61bef03b1dba9b29aa830a81f0005" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ced2e66dc4dcfe633b0470827dfd62a78264c54cdf0acca19317dec4946c7d59", + "excess_sig": { + "public_nonce": "00dad6784bb991708d4bf90aff916ce45cc10ed88513ad97421a5ef8b3551716", + "signature": "0f1b419b19688321ef8af59313881d8124e1f1a6cbfc9ecfbd4bf5503b591b00" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "ec30d1d8bb65807a4a37cccf5904d314c7d11b6ba6ae543f6a14cc4a28d87446", + "excess_sig": { + "public_nonce": "42d504c75adacd0feefb2fde856fc632f2f2b4fb62bfbf73ee519bbc9c965a71", + "signature": "cbc62ee451df0e46c947caac613476a42e7edc1e1c17dc663fe7b044980c3909" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "f4726dc6dd4062dc89e6332eeaf7d900978a55d4f7aa939386b213ffb49d4a54", + "excess_sig": { + "public_nonce": "aad2294208f0249fc533edf62abc45e860a76fff88248fb88013da1db0460759", + "signature": "31d36b71df28f79d1313bad41d316182bb1cdfb0abb79b2b91cfe1fdc9272e0f" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 42, + "prev_hash": "550c265c2b5dc514185d67c0dff7142105cfb5593f64227032819d0a4d3cc057", + "timestamp": "2000-01-01T01:43:01Z", + "output_mr": "d0a8761fcf4d0c22d76ed937ad92bb4307c6b9732d64a6eaef8388d270e008db", + "range_proof_mr": "4fa4468325779386c80010afc452430c1f6057dd4d5da1a09cb3b5428106309f", + "kernel_mr": "47c04ce564542e94c53ed09aa85ba33814e9fae4a2a61e62fb00e48864d06669", + "total_kernel_offset": "217f046d94eb2b06821e2a948bd25bed95d729cebe48398174dd1095ac844905", + "pow": { + "work": 42 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "080945672643dad18edf2c27eaa5624760bbf8d35b0712ed530831025fde4b15" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "28051606a63b54c9c8490b1081a8c78dc009c9cd7d9c284d5825d500d39d4635" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d02769d3ac8fc8c772642e74f64e5effaff6b278462b57129a836d056f67813e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e8d75d3f726e6342043848f67741cf50ec6313e82da83566476d97578b4e4244" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "ecb501cd0173031770507fbd935cec79fbc91a1e39c4f81e0429dede5c1dfb00" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0616354a874abb6896ae843b3f1b83e389659ab9c25c83e2d26d9ae097023945", + "proof": "4ed1935ded7bae50c21e4adb5649a7f7fa57f82451656062d9af6c266bcb926fdaa6105ad86ebfc3b7caf74a131f85afe4231e019a9980c9fa35f6b5b5187766d088543d0b091450096036ec694eacee4faef8d80a74eb46f2f2e7cf20c7d039cac4aef99b956a543b1be542f6894450e9df0357b637b4faec89cd2d196b7d2d69b785505f9d83632503dbb443abf6113506a924e822dc9c37e670d0afaa240e7985c927d1327498c374039c3e445efdd68dc2913ad332ea3ec97eea715768005278ae46323ee866d0e8fc721ff74d6c8656ed8b665f78f3a2840f9ddb2ecb0b7cd76c3d3310694bcc90021dfdcc0da02937badb73c24b2f6dabdf96056d187d1a86f43bfde554ce7fba704d4447b8d09b5af2de6ec1eb33c0af7f6b962c215ce67dc605012ad117c6d4a36ac98f6085764b71ad50f0da8f76dfc8a24a1b4938ccee65468500e2603ea1c6fd7e4efd7666d900b99594129ec0c496a4b9e4a823b0df137517f58f95478adf355460710bf52ccc4757b48b2e42a358a376ac78556e2d37eefba6bc8a3674860e986a7183195cccd856dee723c097d3cac80fb606cc073c679238b5b8256699a15f62d9198214a2c830b0afe2233b3d41ad09c7320021843f7ea7013a1318a66f048d2178306dbf1b091a7cd6c48d427f6454750e1af1515e70003636219f110303cc627a8e7dc378f315db415ebcabb40cb19939886c98d842c3458e2f204aa019d8bb4da0ff9e37a5fa3b52646807df49a3392eb04c653dc368d52495c91925364735845f97cd01bc1ff1682ea1e46f23350c68a28958c751b45a8f497405c22c5e9ba27b2a7abbb4a0b342122822f7c17b1439cf9ccd89d42fe43417f74a49562d1b96f11f416b1210d3204e8fe2f4d6d0f809da7074bc7bec125fb8ebfcd144892b3daf6e1fdb5cbcc6952377a5554e90cb0f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "128ca558589cbfa599c0f51e3b6d15ea8b874653e8a346854437b1de9e6cca17", + "proof": "1a2f65bd1bfecac5f635d72431983551f4e36ea275ac21e2c3d4dc7995aa574b2eb89bcb7e78758cb4efc53fbb81f5102dbafc4bba0ec6d42a868048ab8eca574a8758299d82c43335003212a4d3558de89406f1770efe9b9172c97891891a5f0c8df2178b84622621a018e328fa52d82daad076c6483ee6548d709eba914824411cf522ebcd9d7131c177fbe7f46322eee545eabb01e5a9ded5eb98986d9f07ec71d82e3810c953e076742993ab679b1e5c997962dd0c2982b8d45f572e200abf7b6e7c2d0a1b9f0f4e7a6873177f866a58b43a1b701e0e1b621f4f26cd2b017e846f0952c6033c25019b5b60a073700d53f1079ad07244ad37e399f141e753dcb06a63b160f020363817c09024208d5d8d7754d496d9ed07003393c9607016be7a53184b3390783a6a812c6c2ea08b4a4d16ae8f8a7b75b96a1bdee8b49e15a8e590bdd844e364d2f278f61348c0f7e4ce8b0b294a2af078eadcb7d45a44691820e0d3de414e4fd3c7b675205b7b52777fc85ef42cfba8d09e5f5f030dd10db6394de65210f86fe017141329e08b1f9a6aebc6f703b3840e48c4ad92eeee66da3f252cd0f11b56a38a262d84af68ad430280096eff2f4537c272be1c80e65ab6068dd0d62280b68e6e87df48b238da03ec7feb3defe80d23b1876117e72d715cb10949d478d101e137753438cbbc74dfbb1aa766d401b24e7891d5813c760a2076d01ef1a9b2c4467c582608037d749d2e183bb5c17f825685d0396d4e544a90177c9605dcb67a1f7ee4dabeabf326999de36b2415b3bd8c55462c06fb04224a596c09e027a40e513b168ee3fe8233ce4e9697ff6fcba408b91a3a0faf8f526b06498d2daaf90ec94a1b11d48e1928fdc02ad00d1faaf30f1afe2fc1fe0e0390f951ad7d8b59d321173d731eb802d2a07729ff357a07fbb22b61bf9231970c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1ebf0972576205b33c8ab0c57a263eef115a28097671f246197bbd9a12d8f24c", + "proof": "16f4487942ebf37297a67899d90530bd7e1105ef0ea29540163b063ed8bb7551e86ccb6df767b71d28b52f28b8337d58432b809fff877bd8eb7b59b61702b932ba66cd40d2f1ab5b31c727889f3d9e8bc7942437e5797edaec25e7106c6b636c0433f247ef27b344013bcea1ed6561aaddce83bf33f7a974534eb529ee1a6d62c9101600a802977573d7dbe36ea2c04d3927cfd758e07a2ee59d7cbf0dbd5b0cc140e7bc3ddc4a809c9e5a480248b53c424a17f8a989142333ab4226f738d001831af39a6fcab436c9855f2680e0274c08a2dc982dd14aa1f272ee3a527b0903fef5d54e30e007b2d340028f4618690277e2251522e77690c6afbd1ae91c1e7c08865cd48d5f6750a50c6d7a05cdefbd4403f37644e07a36914fdee38a35670a06b12c09ed4906627450f71f96bca968ffa9c48905ce7af6585459606abe796ae41c44d8d8b1c308b739afafaffdd5aae695914315b2bcc510bf97bb1176de0c3a9f073812cb51f170473cff0d9d0595ff5f2becf7d45600dde74d46fd143b7fec1618abe2396ebddac21e402c70064a33aa0d10fd5fe59b211302800a1b1c79161f14391268e7a0ddaa393f52f7841c364cff3cfe0dd67849917b3e6e27d22c0c905ba03168550f6d530090f84f547e203ecfaa66560eae7065511b889ff418ec1bd34883ff2537f19fce0e726add9486d5308dba16c93524eb1a535866920104788d0cbbd5dab331ec1f634bad20b48018cea0225954388a22c4e01550c108ec80e8ad3d64bbd105f2fcdb529ec28c301f20e021992d3961cc177f67c7aa6a4ef8fc0f3c83f661971299de2dccb46078c56d58e576734668edd5acc568796aa06c8dd2f0f41ef112bd1aa914ce3711e43e87d83eb2dbcd39ccee437e99bb009787cf5798921070bf0aa43744ee0d715a391c6c555359a1294e9a8052e37c00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "287edca064bb17c445255b6c05b1263f9a87137c2c15a1ffcb4f6e7601bef26e", + "proof": "4e9316e120bcd9e2aca53a3e0502cfdd6f7fd6ac5f8945715124947126fcac0ac6c8d2012350af481c0facdf95d5359edabca41fd1a31d4fe2fa6031907b55765e648ab669bac520d82f66329c7b4fddc1066e4bef10e30699769bbcb6bed53ffe6e6482fb5331948d3d0db40a7805ef36e20fb7c480f250521ef16b215db05dbfaa3439cfdcf4aa97f36d726d8ae0e078a8abc90fcb32bd56d5aa795a1f1b04c7539fc7b62281f8ee1618720983aba10d7a683b84a42729b545b4f15a61380ebf3aedacabb84d5305f05d2c13982019a70eb90c14d6bcf7bb6d71495689430be241948efa241b6697e50df1d9bf5e6094643d728fea23b2fe87e147822e7138a0b201b7b262777e6fa7bdcf1ef817fa91040bca26cbfb915cca85d439173e0fc6ce6cddf6b126500faee5227abe0a3d8bf6888bfe579714adcb737f58d6591a20ee0b8f8c5cca1faa54e49c92f4210a16919a3e8a3ea13ecb4f878f10bb305e6a199972babec53b47fd71971daa3e5b4843e1476b85963636f7f42888674d3dd08b2192c0af1bddbbe36c4fe1c91c3097720df5d5816eab80cddd88684fa12df213b744f303e7dec39cd0d5378dbd2de1a8a97259490ba4dbb00183ba43cf59e0e934a6be78815579afb16db5a61afc32de080b9a7ae5c6bcb5791861a203548a21c69ea30b1a12525eb39ad90f17a17edb63fc052e9d36c0ee1de4870d8f799640c169d9e4b4dc5792c8f18720929ddfa59fc38ffa5258c49f53e4ae9b714aa0569aec72be008aa14ad0aa2c49ba750605e08d3a303a0cf47a7b11b4041e32e256b4ad5022a4078c665ff9040b3ba02e13eabf366362481a859e135cb71b333f54102ea993df507aba7a8e5bdb6e56b44d959a157459e659b8b73ba522ce0ec411f19b305ceaecbab3a9c10a95dce7ce44eab1a2971e3cd7c6fbad50c46804" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "4e3a02301441d276ff0161b692935dd84e8736c7381f2376c9cd65ace56f4149", + "proof": "709658d9af8bfe95e0552d64c6b7b0aaa239465b1214093f8254297dbf9e913b8ce2ec56f4f7fa5bd84c8811db49c0d88974c58022c799b22c2d1ef31d1fdd74e6ff052203db5362170f55a777b09aa5ec2dd40e5be57d11ee6813d94b76f463606adcfbd4761875133b014110de358942dd9cb25e6812f42fa0e396dfd2e55ecfa05d25ba5d0c2739733450f2be0a861cd4ebe8d3748a814822083ededfa303bca58513a0bd19e685450d507bb1fe07893679030686bad1a6aba62303fb91034ddfb5f3b87b4052ad5876a233b1fe02d4bfabcf3607f96340a2015ac2eb7a07aa48e6f7eaddc042c9b806e803df274c78b1369d6f06ef20fc96f59e1302005b465aff5a1ff47f59f0aa480cb7b4ca88bf43d88375b695ccb51f75b2aa32d968f6a4d739e0c109a22d8e0f1f3f3fb951ae504f8b1eedb6f9fcec6d59bda53074261bcbf08caa5f185cc6fff5d478353dc1400ff14ac5cc47b93724bacaa8a56ff04012a0f2d72f72e7d4d7a94fae470b74f4c83ef4fa9b2690a322d5d7cca63dc25226ec2dc4fd28c03f3c5f5d282d2cd85c0bdfa53849b51b02b249b5d3d7360c3035f19d83846fe2ec7981524130974ce52293c11eb296900b0220eb16bc7fd02f748fb02f8b119aa6e4f20bae80521cb016ffca730cbe8b896b86883cc911569db740995a8ee69aca4a4f788385118630a9b420e5286550d64ca2ad68757ce0b802a292a233a51ac85e82dd628e2a0d3eb7bbbd12af48cf8aac1caceb5d5a20be06d38ec0e186a1ca9d61342775821509e23d49f64eb7a63d2a92f4f6a31b78f46e481f759df1f8a8c01667b2ee8e075058247f4bbf92aafc31827e70166e4096ece7670f1f8b692b3988b1a3ce77f78ab6267f0eee38d203b9303369f2074da4659b300c793333f2c062c321bad6cbcbb2e22a41743680ae12630c04b40d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5a0b9e3ad324739ff36b00c0a9678fd4bf9dee1cb152e2f9f18f2b65dc843a61", + "proof": "9a3c93f6f7d3ae9f210025dc5ba6163f0138fce219bb7a38bd5e958f3b597a556c78a6698d37ab70b89bc6d59229c3c0a2b78342ce7f28c504c1ccbbe91afd0fa0839ffa2e1980edba456e9d491420dc66d55eb339e1d4a91d973bc7fd4b247116b05876f9dea1e576397026da04539db4d76c235eb2f2ec64e25c41ae240d3293400adc05cf2367b833270365b893f54ac9f68d048d4253435c2f6a4cd9e2084fcd996fd7a4fa4d19cb95c0bf815a9d6fb61aa57421a42b2b6e86138a05ac0b3064e30277f7be01b3ab367a9a4e9fbda7d8afef37d63994eadaec20ae471e0892a6e041d7ddb30cd6010a2b95372318b7cbfb5f291c22980662c758448f9c48e2a56decef9ddd60843cf3c78faa0c1e22aa6030ee081194697283348d1c1c6048f8805788b4a87fe2b7aa1ecf460b29b621ebfca82c87688151bf7bc668025134cb480d8a0d84a9cc9da2445e750670a12caafb2cccbea7023e80dfed9c1644522189aeb392a02f6d83bccdb6c440b3a6ffd5c5f330bf257e87b03425e68c29fa5339b06df1b796a386b9d29694744936c037204a241d81aa1d5cfde344403b60bc7cb218d76fb5f2a14172f6ae1d2e1e1548f146919bc5d12345c6031ecc52f0e8afe9335f52b68d769f02f9bb73bafdc6ee869900b18322cfa9e969d95700d8762e8e09970ed61d45c8bbe72a7022ddcebb38f06f435a316920e60070f003b41c947c99f94e10e6e9cf7c0bd5bb458e8c1ab084e1adb4b3fd32177ab4715b380af7989da32ce332cba89dfdfa3ae7cfd3af45728e4e78aa2c3fbbbbb20b641607e7f18b89197c467385dffccd9e92440b290f9b463c7c4c75d6a2589f8769a4bd0d289adfe0940867cc14a15c4ad4addf9b5f8a54a37c08d1cdbe02524c062c0e8991c761ed3f79aba4ff0428500fa5ee79487f713f6526a25c14ef400100" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "60b30a94a72b2a856c8f8fb854e3438e1f3fbe3dc9039cc2ade0ee4559c98d6d", + "proof": "f2766a38c37088776d92bccebad15ed09499969e80370da32b510211dc09f20348441d1e6f3a18bd66dd1e0af10133a60a8f3af18d96e7f78221eb16b6d9cb2a10aa95c810fe485e62119577d8d34f7fdb7c705fc4d828cd210084b33d3d892ddad673a3c182999271d67272b3429a77b57a034ed993e67dbcfe4e13fa6a314a8bbbe6915486a24ae35c5e850bf310a7bd638e33636ca4ebc2d4e8c774d77f012b8fcb290b1c763de4ecb44ceaced7c5a6c17b68b99a84bf2814bdc4e0e2f10af4f40ec06b0cc3c2e266ea83b9237ecb95e4e446b982308ebfad6be60e51ab07e6cd516e4aaa46d7094d55a08b799d978c0c465a93b416b4a4fb0fe1e8678747901b89c2b62c632382233229cd9821395bea46de12d29657bec3737311ce500c24b61d3bcc52d440e0d6932325072f14d5a644c51fbb4eb2e4b5c00f89350c583a83305e94d208d1c39b30b7f98470f653423159df9f03f9534092aa4ad3736552a2ff926f88e1a4e8a1169ffe3e387ca65f3ef7d73c4eba33e607b03e328b6e8a056db399cd92728396fbfd2f34fd32daff33dd11161c91e25b65a0373de6080a612fe02e57548a28941c0a771e18f0bf3b2fc02483ffc6d5d9b9b254ba935b16fe6cd8ff4cea3f2ce6fe5275d0935620f07851f0699cf62f6302512f041a43a4e743e46fd6396c6e9b9dc1694556f79d5a01178d9644aa49e7c41d3850697be4c5b36e9640039bc7cf57168c42cf7ee7f5f682185f8ee889c6bfd80669c75710e99e2b8a7fe9ac4731372ce975309a2d5c6e48fbd6bba37c4497e16150206276650630b6b217ba8de5b448b1593a3bd4c88fc8c966479d6c7fdaadeb40ca1de1693f62363ced0d2284c5b48cf2a1237ba7bd0c03b4fe9362a7863a685482014c00093ec08c824b10d39dfe6e4b2cf296bd950a3d1b8c5484c675705190710c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6a95080f8ed760bf34dc8f9e624f9127ee9567103e06b20c79e0c0d81dcca47b", + "proof": "ce03a092b6a71c711819d2c5f3ca0a111d78c928433a3dd501af4bef1dfc6d54544f5559a6f589398dc564a9755751b4dfd6b1d1a1bf24a8d3537ab123ace459a8a1a3653907e00cd28b9aa73a9e0a70e5861c58bf175180648184e1a1c3a714f2b14bb88dac405e3375dda93be24524bf90fa601b897802fb32a94f3a0e063b3389b8cdc8d2b945bd3f2ce3fce34a0dd032dcc735eb3233b20646112be87b0aabb5c9dd8c7c8949e3e070b8f8d2b36f0cf354c7e671802d7030ab131b405c023bbf1c1fd05f827613ce4e11096a0698dda21193a980f8957b46384a7aa551008c7ab60e7d9cb929793ca8729b87017335f4ea650550ca80c4dfe15cd854366c7078e1af74c10c8ff2d65891dae0cfd27617c457dddb9d0dc0de3bfc2fffa504ce7b63278d0f6504d7fcaaad26ad5ede2a6bae75a5818f2752d94379cc954134e2f17b3d34284663a2f2957444af192c32faf8d94097be235f3c452ee5742765f21a3cf88aead39f0594104bb2f5542d36746788bd4400d00bd2d03100653f31ee6cee9a521dc2791184e210bfabc2607c9efdd9fcff3491f01a751244617920caa757807b2ca06f46e0ff1442f96aff455aa8173a240f1375c8cea10d667002426e30772aadd3db1c80f693b78f45bd86b28cdfa229e7cf726b1ae7e4411e45263469d0818cf7b8e8e9f39f8e9930baa90291805fa44604035bb4d2499bc161f6f10090e59d037b20ac55fa4a71bdddd901a4f4c656caefdaff6a4971915e072eeff89e49061cdf6c5a988c39b911ad00b53fd01fb41a0cefeaf93b0031bc73d4ca900cd6396d9e8788b30d6c7cc2b49a8fece507f9e4f7c695ad36ec8c6d1df5ad1d3b73adc67a3f3c17617a6789ffcb961bae110ba1c728ad0518b297ec0df889432020bd2a6649290c66bffca3899d31dec9ed23443c958e5038f8c6cc0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9292e58ee0ad329534a9c587ffd0636efeef8fcac96c0d8c81d07a8f8efb120f", + "proof": "76631fe3cd71c1c5e331fe925ceb8871949206ab47d759d3bcb9f9538ef1a66110c6a011df20169601c02b5ffa59902ff2dcd2652d744d3c1c4a6f4cd406fc100e71f07a7a1ef08bad3e1d8ac3cef2b7e352a5ebc2cd4ee700264df30e3ad307bc483c3b152596f526ac8cf0115a3e007ef00b360d0bdf25560516d2addfcc3321d3d863b63d0898d5390e3a678ab4b7ac4a634734bd463af03a4c208337be07db7d8ec5e41d8580a69bdf592998ecde0987ae2cdb5cc80d4932eb4788b9b601a1cab2fc0aba51383e1d26e83704a3a9f9823ea9887913083ff0935f9f78f30f46ed8129a527e116ea2e2259bc9341e3d1eefa978c273422a96ca729f931446e78e44f64ec13e1702a36e5e1b8f0aa3c061629c90073383552f480b093a7b5783a2fc8c5ca2fbba16c32ac9d5264cc2222bb552f2f9d82976365660f67bc5a57ba099f5c6a6a241360a7fb46e437dc7ba1db5f345def6b92197e8c42e2d89350da3d5ae7c89ed6cdb3dc0099cf9f06079f68d65b6777c48a6192df14f200274d02a75f8ddd0dcc83130e1036f7fa24de7c26afb265e9353919c8fae080333019d88ae0c29c1297004607bab7b528cb386a360c72989538eb7f3eb4cac26347414cb91ded861297b3ce33e03b6968dab6cbd6fd1056fc9621bf05e0109f5beb0c1a87bb34386612df659a43a1a6551cd0299facff23a02a9399c6fde1b6408062a86ce1d8173d38270b782c77fb2419721b554727b696ca2ff099f7ba4ab55c105c813db50e4fdf25ae02b2ba5da4db1533ad5c7122c9045a24164dfe024e83404a8c4c88ca2fa826cddfacc0937fdfb35006072efbcb1fcea37d96ee3f3f19082cb2b815f4ba1cf5041b106b938aca9cac5ac621491a996bbc04942a902fbb0d134d18540e0f50aa20f53bd1d7fe5cec506928494d0954ad8c278344d02b9109" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "caa7ed7b940146fe1b65b332a56fd5aebcc1eba77516398a248881762c184e51", + "proof": "e03637f2cf5e45d5fb77a240caaf6c39e4daa599f25426f04d3f990d7bd14f119a12d4218a534d0eb7f718b6c5e7d91b558306c6b7458d1c64e5516d9ab02e483432abc8126b159f0c6dd35178de0306bc55666903eb4ecd4e32e53b4749627eec3d1a2891b8568b15a2eec7a0a0e093dc61cdd4f9b87a09e0572be16ef4a6471944edf57991e481674af0374324e6a4640ed2a092d77c210f57129de65b9c01639e827a3f7ef2e57c2eafc220968db758b4cece0516b3c4243c66874246ce0453389999b3927fa43508f7b4331ec0c0508e0f6e5d778a91436d69f50feec1070afd402d54e4cbb745879ab7d5924e1f7f1795cbb912b2db6c8cf68c1623f031f0c84f8c126645c56f789018a9bdc0c3aa33a59371f7b552461b2215b66cd87a48a7373933c97bfd26b002867d40461d20f03001225b0f6afcfbb01bf1c96c5ceaaecf6834ee8e66a59654da44b57a746c9f35287146cb8caec4ae1af953fa1c562c2d70f4692547a7c66d404c58f10a578f30907e66705649a9f12928cd8150f41504919ff043e7e8f831dca509e25369b03be75dd6a2cac43db36933df3941eeaaab1c914d246a72bea6e0fb01206b638827099fe3af731cce72d76561926976e22cbcc8c783e82228a6507f38ede8e1d057581e62709205f81d611acf39556a1ae0044be1d485a323e2976b50d59e7f719457ff49070e9379590464226264aedf984c55a5e7a4021242b34fde689c65f8472f41f4014abf25d1f65e8e5402d006c6a88c10e63623aa73aedd475b56649062a0858a69b924d9276c8ceddf69a0e9eb667184004bac416955acb0b44d16fa6b042c91805f410cb85360f7e93fdda2c53c7854c9c9eae0018fb48d79942ce4a8a96f5050d6e11ccfdb0c000409053c77620e2dc62a01ce8fa060d7f343a52c0dfbb8a990ced7b046b297ffc60a" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 43 + }, + "commitment": "7e8091dca6e8f0cba091d67979c5370e47f12e2ab56e9b469a5bcfba3e983401", + "proof": "44adfabbe610d86ac2cc80a1c528c84f782567367d15ce50c063ea6346f0905c3067a467dd37a8976fd5951d9ba9e6798974b43666edd1836292504a1871b547a824fbd4b6e2daf6760f2e437c9ed0d6f5d68bd8dc4b14b50ab2436dcf77bf7e940b5518c3a5432160b28a16923dfd4e0b6b8a214a15dd46bef04f79766f5c62ebb763d69ec48001034556db339e8baae1c24b0df56a01f3a576b2d82593940841ebe1dd95b7b3f7ad4be834d4302d253923cdde922eadeec05fc1afce6ae60c97053e7866044f4cd9a7c530d9feb0ed0d5b9a622da85d46760d59fe4f448702007376bd0e064a340e2523b32d95ed84da0092363c12b9aa3fc684a4efcafc1c0ed7a50e71b25682e5fee538f0c53dd96ebe265f009d6e281d9ca228239ec963d6eacea35a011b98818e532653a5e45d3afefe4b65c52b09fc5922d2611da46cc8a64a4aad76713f7a9d71cfef570026a78ad2a00cf786b8a8a4e46e2bc36d3ab0d794e513a32af66004711abff557da62d99d600158cce55d937c909e6b360e9639a2eca8f92991a25bb8042943cf813521b05b06b12b9ae8ca0a8643ff806796a7c182b6061fb62d0363e640270573552a97be008ec5c79910aee4126a86618e4b2829d263c568ef5f57ebf448e0cf77f055cf3183d7d7b6217f134bd94d16d6f85c3259d40ac6b52cda029681729a5a206bd059a4519f4a9eec599817ce2eec3259626126d3d6125fd03ae06f7ed91cd940464bc17158974b2491ee58f222c2826c61364cbb612441660e669cd282822f34cc36a295a4ecf63691e36b360e68c8b30c6d2abfd577cd9751fce7499cf2276c8a1b4d9d01c7b89254465ff21d86528faa414a34fbe207f09517a0d6561b5ac8a7d5cd039488c958589b144206825fec37e949e16ed112db5425bee3bcf40ff81352a1e22ada1f062ba74f3a06" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "0c73b3bd674852e3b8a3d16f05d1eaa09fdecf3a7b427de7d68aa3933b42552a", + "excess_sig": { + "public_nonce": "620d5028c1c92f6468133ba62698f7666b46cb9ededf347f07ce8ac040628f4e", + "signature": "a10c4362d061b291586d715564e6e691a21ccd3efd57c4e4c9ec260c54b09109" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1e5fe4b3f8036b40c155b1eb8326a0d4552304b1b17ba2c241dea76e35477216", + "excess_sig": { + "public_nonce": "3045cdc6a968e571dc599a4e9ddef1432936ef89ddbaf509c126a6c62e721034", + "signature": "fea1943301a49e88fc44c7adad77ec6d41b05f1137baea88b65264a06ad36f02" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "2c63062095289006f82355c562540334d529c924515e9b708f05c1cd2428f53d", + "excess_sig": { + "public_nonce": "c0bd1ffdc8550bf02dc2d21bafe771254ccdde9c2700d8df076a327b94ca4622", + "signature": "30dcda785352ad7920679b5c4fb141bfc50fda3f1b99da68a218be61428db109" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "708c73fe47f2676825802f819388f79b4c1e646b86c62604d9e5f626432b4002", + "excess_sig": { + "public_nonce": "52fd867be67b9d5d375e8af4b8c9fde07e0744d873de7ec7d6cfb3f0aab55f32", + "signature": "ff9982e7ae020f86417ab53eff4d6bbb9a3492d9953de089a523a749ddaceb06" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "a415b640ccd133d99da14f42776b3230785c6b2902a362add0f36cefb073eb7b", + "excess_sig": { + "public_nonce": "ee624fdb388d056b73ae084456ba71179fbc4ba2e5d1ddf1f3b3bce8cb41e62e", + "signature": "7d1b8b7a579d7321c1103624395c213af532b6f48e99d80bcc2a72087f2cff0d" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "94f5f052de8981ee649c432a1d06f7106f7c7ab57b3a3a356c609d55c9dc254c", + "excess_sig": { + "public_nonce": "b6e9d31ebb31d06ed465415e25527503b25fda1766029c58161281c212a37822", + "signature": "845c5f54ed18b912008c1d80b4e1a80abca7068b8b9b29639bc083e8b0db7703" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 43, + "prev_hash": "f2154d47b578bacd825232845ef5c41bca8037d0ff07e1844e2897c46a799f82", + "timestamp": "2000-01-01T01:44:01Z", + "output_mr": "88ca24549e43817af378961e5f737a38eeb4726d7be232d23bf2f58f63d0bdfe", + "range_proof_mr": "ac3a1b00f103fa95bf1ba68f10e2e55e6ddf5de1dfbdc54db48f3c8addb5149d", + "kernel_mr": "e50a5309a25d4621cd00cde708131cc14acd3e2c186524e8ac9debf9a4f9a466", + "total_kernel_offset": "fef4bd24d1bce8fbb003c9c73128628107ec2986fb9fb3b14ede9bb12202220b", + "pow": { + "work": 43 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0616354a874abb6896ae843b3f1b83e389659ab9c25c83e2d26d9ae097023945" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "60b30a94a72b2a856c8f8fb854e3438e1f3fbe3dc9039cc2ade0ee4559c98d6d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6a95080f8ed760bf34dc8f9e624f9127ee9567103e06b20c79e0c0d81dcca47b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "caa7ed7b940146fe1b65b332a56fd5aebcc1eba77516398a248881762c184e51" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 22 + }, + "commitment": "044d0c6ac5247a4bbaf89906e13a97dc142c1e59c2f67f9c972697f004506f10" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2cc4b924be6dad39e61fd57d3652877e00467271c8d7db12e6411b1dc50b3533", + "proof": "92402728e109a177b337705ecc8fec5a3d74317c8be72fbb785a3e4bfae0ce0a3ac575b016406f206a159c558df2afa0d50dfec1f9d3a69e8ed693963b2dd310ae24c3199f15eeeb66126370336b100deef495ec3bd3981f890c02f7a7f62c491e97fd4511869bfd617c6ed115166c216b33d4aba32f1ffc35f750d4b0f64229d90e248b52fe952657d44d36ddeab96e86201969451c7a1f11bd44693c5f5c0a5a1259b770c32b22ef020a83c1d5769c3bd563f36cd6f77ff475522e99a0fb0e8d4aee505de55dfb264ab8af74af3012fffcc8b1af52766b4e9861455a761d079a8ef109eb164ca79818608696d0c3ca25fc2f26610b049a276dd47caf109a34a6c4bd17af59f23cd4ba82a9b5bef358c4eec513d5c893b5ccf02021ffc1690ca0de3f0da13619a5281a2d78a79d64d26d3187f2476110f71083c80e2aa43f3d3af4ea6f42d30b99dcb84580587b0a934362619a88986b12f96a59d2d1d73f2812393b430dcdbff0574ef59b823a5a7c2a1114117a2007e39f6528acbe49a01ee0bb8ea6f28bfce2122bef783baf2ce76bdc1e8a5abddd9f234398549307617ca826fc1cac2b93c7a40671c65b511966b131d4c1c53409113afeee44f6fc8378c429980348ac7adb8e4817e43f8ab3be24fd555761c535d9dc2e7b265f585a5d3e979032e45288b21f5afd893773b8805c7ee260a42e73e619e50ed77de2f341e09929d0d23d4b9cdc12ffeacaa80834f789f21152d7b402738ce2d416bcd041dc7108683e4b50f8cac9e78209f8b2317115338e2b808c0005344b67e7e09e14fae4f877c2373d5060cae9dffc8fa14192f97fa15e7c20a9f9b606d88898da4d76c4124cec5def881706da66fa3d2958e5a6588313f363676f4a3c8bc468a304b4342f4709236d1968b1994db32494e77c816122ab6546cd74eddca59b687a0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3ccf80a7b07e226047b4a21f37ccfd2dd4bf985bb3a498ccab3c2cfd90641877", + "proof": "9edc42ee85094ed0da954217b3a976cace807ad2f79219b021353fb6ffe2c501c24f869fbc78c55b67c6df9b91f80e98721c68ad1b0c0440bfba5def56244767e4f13e7fc7b1314d9ab870d3f130e4496e0a71a45130089612546d04e583257a50d9161827ed52ed085b3a357cfaf2ffdd9a5476d78f64eb30ca3d8f78f07f4ddb4eba3a914eea989b62350d4b413acde7f1dca02bd9dc9d6f38397ffb86ff087a3cadbce3b61cd4eef56383fa9cb6bd3e8de76826e983ecf21e0e4f26b6830260ad94618f1b2d8be615d865d2ab53aeac4f5751d72410cc8ffc9b1c29248d0da289b7600d48740ce33d8f19c3a73396ce6437d336a1719adf030c92f0671e15c478c10e2119e2ce8c098f2a47fc62eddb86b7747dcfeb9f05ba7db46d38930a2203471eb25a96d1d833ca9d6ee2cc89dfe15a73847c1ac49c1dab5098d23a141ad980096a61ac0759557ba8e5fadcebf599e53eb67dfc4a1d7c2d86b4b8f50dfa9e1649145438f399fa46f44f62ed2b1028c2bec02c2aae47003ddf8ff1d03c127c359d8bc56826da2f49ba3445ad24b0dd558282956e8fa2449ee5c4b3524d4e9f68a061e11366975a6fb5d626ed8c34f1811c15ddfd76b54c558e053cac1f5ecb820a73893055cc9a61a656713dc627eb57280b8c12d08f1500978c5f9f2e56ac82a47ac364e95118cfa6294b9923421996f448bafb1a733e709e39f7815b20509c5af4d1009b70792919d3fb5b7a02667d8f1d788bc2efb55f3fddb6696768ef7153efc47f6e9863454d511175c181d94448bf6ce1d12481fd715c40e964e45f1db978f8a0b8f1d677003ff429bf40ee0d4ff8b66aded95c67c6bca4683f002350fd60471a201dd770ce60ece5d5d9e6176503af55f8e970af5d9d71ff032036b2bdcb4765998fc7e008a7e5260f63500c15b9849d0a4ce9e8d1ab925c0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5a963f3bd17fe2300d1d7277a3c0c1122fa82e9296ec22d5b1f475408bbe8629", + "proof": "363534cd36ee4e79126819bfccd6dadfc60659ded30620c7efe2b293748b4850582283314ec011b3a1d7e3630bf48fe458200858a6921aa1eda8b7378e8a8f4c5ab0d1654ee14926774c85aa4e9f48bb32e8e59cda43a5bbd1c20ed507036208aeea8db46eab10bf68dc068716f14e1fc2c5f706d66312b0d2035a08881f9c1d7102c0c71db772f0af62dc10e2086188c28c35c1443e732bb3fd8e7d65793c0cc6eb00101a3ae8ee16e1fa7007bd1c6e32412c38713a68af1857d8e7a1beba016fa99d33ed4cb58595c48d55e413e1c83cc1f98b6b254e7bc046eb9df801600222c747dc8374a8dec984e8a1d3126e2dfe0913fb015e827aba64a659d461730cc40c92520eaee43db481987e22876dabc79d1619aa29cffa5644a47441027a6304a0e8135dec6f5d393269d48917894e11df3d9364dc892937f0953fd61fe22b000aab8a4d207d786ee5f07822f05a20866749581e5d2fcf678a62e841840d513c4c195412c7d96fcbefc3a37b42a23f159a097271b7b54802788f4e39fd157a04b338b81f425801b2f374f05bcb34e2512db30ea8ca9936a3aa57a6a61ad8232424a648b0c1d45ce5a72b138ded1c6f71778cad1b82c03e3d749a5c98abe33fc2b3d08ef2359f3385676dc967eb927c61d2032c6b2825ad8b7e4e6f4b142c60ceddac90ab15d15361c2e8fa20587e72d20abafbc1fc2e080d213e673ffba772b8d60b3452f6b15caddb57579c5fd6a3c851ad53eaad4405d2018d5b8fb8d760849776626649a7d051452c47d10f8732a6f6a1b786bc29e8467f24d9eacf124580956e20ac8dc7de6854a35517cbf95b166b60f795acb2c33aa971c1e52f872e217d94659b78daf80a98df1158fc0321ae67c3d8a1252ce2a02eb87e0849540dc671d2215bffd1dd871f265627f6482dc7d5188717bf03c1656d69bd53bbc20c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "786fbdeb2937bc42db1bcb1a7236a6ffaff81d7bd198853afe60f623af1e366c", + "proof": "843de2796360c0b986d46d5b2c957b64b1bee424bf6f07ca698c9c3f782d15408a7752f7f27fd8181c7e201819b4aeba414d4de5eb861f414ceb74c5eb79ae3620c658cc87479eb4c9e71e800913ad43da961f20e4887f4c68d4d3f64eca3e47be639adf996640d1513aebcef5c9bcb568f7cb5ee22b72d91606764eaa314057f24dbba7531a7a028e7dd94c6e7a0bf1072e07c3e82a4b724dc9b7f270b1b600c65d0112c8b86fbda0e9738a2ce36722b55178eaffbb0e5d4b98ca9afe34b20aa2d2005f475893f1ffdfa6131ac056657d295f1a2b6915ade65854464077d209426a7cca9f48508e94aac85c3ab343623c26e10093299b0ad303b3ffe354f7694a2247441cdacef5c0289ae61f5106a512362269ceb83d60071b2290b5b01838120d0fc6d3ab580cc94310655b08eee865a1f45b52079cc90ee831c7b511fc23d65da0a95f80e04a47626f687714c8fec1b1187753675080eecdff649c3119772a94b4260e46910651bd2c442a2a33d5b1a99993a4442924a43dc6768565353fe023692936b89d3d45808bfcd08de83b8112f81b66932f0595d2996eea81a158444cc2fb197a19aa30bde80ec9ead384483827a524f2e5b78d3756625794cf138282e71e891eda05516fa28af468a616a8d7b92fca83216d3056b923685e34219a9a0d514665eeff5e3add6a4ec82939198622969dcd1ebd26a9b763cd3a0361cebc161825e9622b5013ba2415cc3583237bf1f6b71f49f0b040b3f771c36d264ed668e03d265ecfb6daff1843b2859879d3dcea4fddc4d24eb94ae1ce1097404a54a2099a6f76384804fed9f92689490637575d9ff80dd8cb8199ab2431c3044b15c39b1df810f2c69573a5116602ce234061691942c4a7d136aa598870c50831c5238b623b808a043107601abf72345986b785300cd22fda3e7cb7d234cd07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7ef85b43a1124f4204041dc6b3ce420ca5a6ac314cb6660b8cb66c52645f3608", + "proof": "b2151c6a1e8588a9ae361f728bbcef5d6ff14245e931df824f6bb6eceb2593536880fd932a4b5729814b5298a0035501aa9a4df180cf3c31a611b815d7f6e90466bc932b6bb14ed855e644ba51daf9b116b3779abfd044fb8d87658f0310e416d0c7abd5d1d386a88bbe5e718fe7fbbc9a1c1932a824f8669b2c8bd9b93e3d4ad328a881162c7449325fcc869630915b6482cf20a43617c8b6947d561debcd0708dc6c79db112a6085c885e809ed74175a4d8049169a9f7cfac59a928fa3cc0edef6de3463cf73735a586c850875b94541374f612fc91e02f8b66cb5d39d4d00ecfd16550b4f599bd132b91910309bf0ac6cde745be778accbe5ac47d6176c698a85c67fdbd534efd4e5d8f3bb91d274e08b1c1389b137fc4b4c73cb2c0ee75e36e0e3c821d0d495cfa97e098ae840bfeb2a75d9b798001b082ca2cf7dbe35112e12a3952bc991652731b9de4b4a7dcecec86eca2f93ce3930d4e6e1b7725c7682754036d2e321eb00971a640be0cb91be70cdf20a1272ae8a065e49506924303cbb0de6d88d580c315664abeea4daa6babcece95d52c4d108dd574f0536744f2afc37880611e139d5c9a6fa5e8a4ff73f9f649ffecf2790def23164fa77f806ce5f6590fed33481b838535bd9b25fb54ae4dca08f89910d9013c8b59d57037882176ff27444f5028e606ed2ceadfc4c98c5cef9c37f011640069dc4f3cdb035febc55444b42cfae2fb167f4e6e3b0fa03fc6312aae4c94dae15af54d5377c7288714f8c628833da1fc7497cb1398068f09c46cf431d8a0f567d3a69904c4550de28a6539f61df78610cf204b355252356bbf766a1802d31ba6bf444a5eacf06cd74692f9b64df71d415de00a90f0c7cf5f8b6efac03b92f928592870b66df0881fb872a816150f6682793003545d33ecd27c8e2094e0f1a9c7300a3fedcef0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b466e069302e7360fd20b738b423fe9cf835f47c82e3567fa7ed90ceb669a61e", + "proof": "48212206096f459da8cb5ad985ee9fed81afea54d198cc644b4bb3e36625273f14a2fa1ddaf9a14144a5ff44dd8b21892b04c635c624b5975940c7cba6a402681674e867e993a4252aef667806fc3f6f26a4f0e34158190184f58217d33e3d1ccec9a0dd8dc7c78e7df4ed61b8df221488e768699f15c52ff3061adb6d3a490dbf3366b2e9b4f57e90e086eb7cf9e074039a3bea1b20a4dd7348e1bb9650430f53e1d8e430b7379e5b4e598d1a543811e7991ed584b59ad1837c1619586d4f0956c13205678e8ff10bf5638136b56fa0e0190cd0ab7b08034fff27dd109e28056e8951e754bf4b1a72f227f2f64483c75a499a94b04dde99e7a5ca075e71f5704ec60e05a8141b6f632dde8735268e30e27407ec940861bd3f985d245c77fd63fc68d7d532f33ad4dc93468d00489fb9807f60880427573fa7ffcbe5c276f504544573b045b3223b7e9a2e5d0990720db2ca1da839d040ee760e6b07d4a2397372a8798d76409e4453c53050bd9abbef1b938bfdf3b4c94a2984909cbc37835a94eda3c29b04613b46da33b579e9214809d573624976b3564a473b743040827a7ab48f40dd59ee4c2bb8c4aab1d3994105b05123e1476b468a6bc8840eb5ad1c0a5c3107dfb89424d85b622cb2ca95e906ae55a07a5e2193258886af4adf1576d4d3900532ff9f4d1e14c15339a9616f48b92967b064738c905f4d7a30ba9e4b40aae3b555b398120da325401a71af36cd453bf25cb92c49e1ae78a3acc09c4aa288f2d6be167a96e147350fdfc9d93f677b60f6c153db8d1173428407aaa41ea6b8eb4d0ce94ae92089b896a18edc78b28b6d9c0ae4145571c6449845d9cb332bcca3af3516f0e7ec0af0aaa450362b3e9df4866983fc85955da11e7bd2e900c2232888cf5fb5999baab08b5b056747df5a163ae27e8eb886a8dc50acc58f05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "be96a5e5bba1c9b096e78588992a063009d6e41606246fc2894630bd0bf35d59", + "proof": "925423d2fb0b6b90f0796dfb5fca0b6d9b9b69387c2ce4636f22fafb3d3b9252527e6b49552abe902dc884801eea0cc950ce4569294706a41ea3f6583e59027a72e403f4e6f8181972c3accba9f6e498d68011df156f5daf362304a1564af70432fc8c9201e8f1b8042b18e998f0574597c4e87e18bda7a04430cd960b8cab1428dd6f0581b9df462cd0ce0360ce010460eb4c74eac503fcd6509cb4dca59a0125515270e4c91a43410fe32646c76973cf5c303d10963967f0cf39a9d1a79c0efdae3ec041a3aacec33a03fe344f24dfb0b14e5daaef303075b6c838ab69e40c5e3b9add6a3eff2be67b9be504cddbfb3173797dbe0666ba1aa141039149813b02f28c130ac9bec15f3ce0608a9b7495c2a7afdd2f9b37e57d8bcb35f2725e7db4c25b85ab8ddf486ea79838cdf85db37bcd8b5a0f6cb877eecdc5cd64da032e4066a1d3fc6f99c274f118e34826a572f0372b9b3e9e418c4e93c908013f9c1c2e7843d73f786d0b6b50a253edf4bc920d32d4fb8cd133cd43526a5394503226325cf78eee249439a27866e35283972bac3308f19fdfd5dd5eae3f65b37fcc74dcd8dc46aebd2dcdd15248c06691ec846220335d402989b4a7a26d79cbe338347611715e494095238be3237f70708059c6e44508894a1e5990651d6790b99f48501f042a70230266b0b7d9a33c1f3cbf94cb5bb7edd105d9867977eb8f1a415ab6bfab7162a0eb54298931b33fcd0e802a89fb04566b1f977f862745d5b8fe0c026e334d45fd025757d7bce52a6124c530367ce094fc0a30c850a37577920b6a30544b14a5001efcb539d037048a4271e04563648f84b3120849105b7d58c91caadee3cfa67d3fbbe529e56a42ccb9b44823bf52cf692922570e82e0d58f010137f5d98566f7c7963c9b312a0ea55c37b46c3750b0fd0b04d860eaabcb58e005" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e6d693966269ed7df45e4e889180ec0655df3f679408555bf0d732b21bc3e363", + "proof": "acf20eecd0ea7d82d428049daf2d424e03fcac3fc5ab2542ba76a9a78b17365170abb0d5b9385472768e4a258a2ea8d0554da4ddef05c4258dad93575e0432250e5bcd4d5df54d6a6ab93d1458e6cb57b127f839db9b62f9f9934789d14c740bd23b4e326744a441aa5edcc82b17df7080f062780079cded807ef49f88adb75a7555f77d18a1bf0c36c14e5379a75bd8cb3be82b5b66d1fc23523499fbe5740cd6783b29a96701c37cc556548b3545ad74b3f495d956b4dfb8202b56bb536701cf7a1df261ab45125084310feba726e5130139a9fa007f17c01d9168147c71014c37f0bce53453582a88b5e86fd274be9aae3b26a39ca492933e554ed8ef1f709ad0426dd266fb235796254b2994e3b64952daeddb7365ab3b868a339d0f386540b8737c740e26bfb867af1f698ec0f987220c132672cd03ea2296ccca8fc54c1e625ba3e6e2ddeedbc6cdb9d8307bb76957d89a539e03bd580fb54034e801621a45fa89b0362825eef39db07662180ffc101a5e130625a29876b39aeb5ee838f0db14210bf8cf1ceb7e61b06c76148850f098e8636dacca94aa6388d796b20050451f2c6bc23f76a20c811c60e18a73ce30a3a38951900ed3c64340054ca76b5603ce33368f9a01067260df747ce14dcc54d65dac8c859d76e8e02f05f4d762e8a2784cd602dbbc8a6e7554a623b13438b2c3b4d2a8269585f89b77371ade40f42f2d6b9d7db2edbaa35781dbad5120188cd14adb0e78e18d053af496be1f066280a8abcd4e588d5c2a459811ed0fa2e98b1895897d314265ef42a38041115a3c09af04125ab0653513fcc359de5b57b11cc1b1c00fa38ffd7d7c721357466c743dc953ff5ff65062488fa92b4d3d7731243fc4c735927d5859164b4acba901e7de87e4b88e9915fd6bdcdb6e2f89ed9636e64a1c0756b262554794215a7e0a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e881e71881d734ca15c18bcf696f605e58aa0da4968a76e6a2183c21e76e6c3c", + "proof": "3eae723748837d3bad20299eb4c1113c2563662cb03568ba9afd1e7f98b09a4f06faaae6cdc6626b3ab3883b5d6b1bd21cdff2652efca611abd34eee9ea7ef241215814471ed7265d969f791fac4858e896e187943ec4bdcab2d2d8619e0ea172a4f8cdcc59ca6508baba68f9c978892f5dbd59cd5ec11c81e410d84a9de5e0402eca9b410f8eadc321b6720c241b86be64a7d08df037912586e01382676990fd9852b429f6dd21ef5d7fa16cdc90098159aea8b278169f9cc4d4f2aee49c908a2b8b298a316a26737d3453be3191f97827cee7f205d53e4454445324042a20b2a364402197253c97d5a338ac043b76b7eb46706ffd1c86315e5d77cca607d3d604883762c2417cae0fb0b3f64e6fcaf45dc1417c8a87ea735711669c9bafd0d72c2efae9e55e4f88ad1f24c07de30fb3c02e32270c69f868f50f3371d45391e0a3cebd5bed76b9d17b88883b43f74e71345b5ba7bbef683ccf2bda6c33c1a018ab31a19ae8444424df4dd3bcb74d8b5565a4ef280357daf6f53ccaacdc2a41bd81eeaac478dfab7c635e8bd984a0fd0d85549c0dde9429acc087faab44a201684bd9679d1ca6902082da9311d1e23208547898e1f87eee8dc7861c544835c71fc1684e012a1c3713f9a2c3de6600610448cf85fcbd48d5b373fb07c3e3e0d2f74bca7f1f27a5bbcef82723b482bd38384c5f2152c105bfca4e43fa2edcff9630897f43c43d985b1c6952e03fd0d2f5574a7e0fab025deadadec89a558c4895bce39a6e49fb2165f5df1b39bdd5e77eba8551535db2509fa36b27f61ac8c8e5d1ec0094f039205bfe3eb844610a774d775f3a5ce51cd6836085ef80fa1b88960474d7e0b31fe365352751a93133fbcb18c45dcceb5d9f040ad32bee09faa9905681ea472cba84a258860e15296e1550323537daa82f93becfb80e9e14f3df90f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f04aca421871231881d35918bb66fe368240c02d3004b93915586c1bb8a93621", + "proof": "6eb48f59ea925134e1b52202d2623a20064cb1c91446ce6484b982243e2f950ec2b8f88bace24f502fc26abc57a80f70066a153af7f8dfbbe27a3c546e15b94f6a1ea7a522b3d284d0372b9b00c84ec5ee984ab7fb259c40033e6f65efecec5a1e528417ef708fa300dd5cf7f73b4ef8a2972236827996abf2d1de6e7afc9a76705a1c152ebac5e9d3b9f1ce836652108156d45519c135aa89b0eccfa8dcb100b0ce8ae31e4490655ee828de86ceb522c48a98d1fa111138013629019f05d7000220c1e0f7ee7a7584df78d497cf6dfedaa2cea7e6fe1e0debf9f7d0c815430c6af537dcb95f83f6c75f2444c52f76478bd41614462ad144f09ed6643edc2236763de5c360a6ab5de265428afb37f5adb22e8e3e0621d11e79d23b6a06b10a32503ed9e70acf3b428798bf6a61f7647bdedfb0a85a67bd4f1eee0ea5ea6cf64af0d7abcd62c744a0a8ec82b38348b76e73bcad81d2b91ebb6fe10d80b569e25a4a8f7f03da5d48f5062c47d150cec9c8f50e0288b7a2e7b357fc7ebc5ae94f44ecb3bc8dd793e791037fe473eb05cf93828fcec7a99cc6ea2c68ddfd69843f6a821bc05489ed673c914828a6a006134f3b966fa694c0675cbb5380e696dc420e9ef142fdcb70a37496b1e53152b92285320d28d97f96acba5b1301c939fe442e4ea82e06a1d2b0416514b2ccdc056ef30f4a434f1f954c323e7ca377e3761b4ca2dc7e54b425263188e7629f7aa3c331ce5f0d896b9cda53692fa5f418bec602503e44967adecc031cc221338ba6e8603e55a73669546e32fa6f8c84f3246d1732d0dbd056dfb3d2aabe634ec4b5b7121a5eda8894511e15d38704e4290b0d7865540f0e53b2663760c82ace6ee41cde4b236538fb7c942c72d3893e0ecdaf05ca5303631c800224357e35fb3223bbd16fef905006383579668f57cdb96d7f04" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 44 + }, + "commitment": "5e50a8203264cd94e2c1f71e98c0be385ee3f54c11841c506e2f693f85ae8737", + "proof": "78ece0de95d7abfb2eb9ff4c6f8d567b1ec67209bf970cf7ffaebfef9374bc130e0f9a463e5529d76a1d897a85bea0fae9502b7f253f772004eb50a9c7f4256f5668a5926d1adf286724dc8b2cba4f73bec0291cf48ddee0426f83972c2f17459e6f95e5bb169e9352424d6fd2cecdfede3f48a1c321851a1f1f635d8ebbaf136658073e0107117f53acea2fe121146b010d21c9cf422f7369f112e7b88a9009ee2e75408c916a0d50ebd9f3a9f116cfd9867ef58a743dbbf78c745ba358bc0b77f4b097910b67d2e9d05c5dd21aaf151dd5bd10b31b08aa41f00b24cebb7e079ae32a980f96aeb660329fd0cd70a2d86079c7f1f53ae82aef8af6cd41829e462c366c0246ccef97069e9eca1779483d410b552efc30b733acdc1248cd82732276116b3bd55655fb6f4a293269380eb7d3bee27089996b13304d8ad1a4bcbe47c020a8234fefc1a1d6004f90b79dfdb8dc9a37044b0eb087c19cebde9d5275418283229741131f236192f8ee627df0c1bdbc85e6f7a2aac388881f4eeb8cfd6136c5ad461c94bd75c7cd98835747fc1ae5b60cbc3211f20b9e3333b9a131b335d4c357658fef34c34d064ecf2cf8269abb79a23536da0facdb9d416894c88f631cbac392b467f0c606a7844a977b19788ef90b98ed1d8144865a23641d317e2c0804f0bcfbfbcf0d9df78127ef0a8c6086498dc7402d6760c978cb13375d2817aac94115fe5922d9da547d309a0b443d372d491cf7bc5a6436bdb79151d75852b21974d34cf2cf777c108aff552039ddb0345a8365618906e4314afa85a02531003ee8b5d514e13a8ee4d26790ecd70f4365c803c6799c908bb49957dd695e0b685751ef208372d1c12c0667a7959f29ac1ac56da73628c108a52dd1d02c6002b2018679a323eb79bd82bc67b7c9542e714b3b949a35a448feb1596fd39ff007" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "062028732c33a5131d8d87f529ccc3f8466b929786c8e682e684706a9b608777", + "excess_sig": { + "public_nonce": "0ab75af6658f82c67dd2690223f38b192cecf7c797be9d962738d2cd25de4272", + "signature": "e16e4107ce3e784b66f680b5bf048a1fda58091d355c7a4c240c760e82c1620c" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "3623041a7e4545690c16a38e5690b69f53214aefeff4b05c90ef8b89435a4718", + "excess_sig": { + "public_nonce": "b813ae8b947d83dcef88b135c02aa37dd55ee483f669285cc5a065181d663727", + "signature": "b0e9d1b3d4357ab8934ff85812a21b9a5dcd86c28be2a2da620b18901bea9207" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "6adb96cbd6a7f086cfdf94148926f59c41ec621eccd8a678f732e749b1a6d85f", + "excess_sig": { + "public_nonce": "2854fe19bebe88400f55946751f836aa78a528eb6df7baa72a9a8ce3b4261611", + "signature": "a635c79e372992d7d3e1ce4c222cb958be0f56fa9533cdf694d0f20c7d936501" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "cc684347fc58d24cdf3a19a2c82742aaa59f380b57b6ab97a733c7ae0a94364f", + "excess_sig": { + "public_nonce": "6cf286685e7d53d2c0574cc57e47ee4875204d30383ea33b279d41775b5ce556", + "signature": "a0b13b8b61b4348e75a1d79935ce49eaad88805e33c22ebe01a4c0bd323e780a" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "d6666ea8d9cb83a6e90db1f2ccb445bd3604526310c0e78d0a2548e90cbe710c", + "excess_sig": { + "public_nonce": "46fe4261f4a020d943a573f48b2202d8f5edf19118f6b56a0693c44b1bcbb672", + "signature": "9bb9f03ad591793ae7f97da63375ca54ca50bb611644b1155b545c26d64c7d0e" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "28bd1492db878e83dc97e04acb0e7ed29342a00df50fab5c5a608704d1f1645d", + "excess_sig": { + "public_nonce": "929ebfea84208a778e0e1af26edfc4e685d66e1da408bd3de2a810175b790f62", + "signature": "ef7f51b6c049ec2b20edac9aab4a25b6d41988c7113821cdcd1c519244fd7405" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 44, + "prev_hash": "d1844a7850644e476c8cee00877f3c387c0746b56dc52346503dffc2d880a9f7", + "timestamp": "2000-01-01T01:45:01Z", + "output_mr": "80688647c6eb2b7e0f827517f8212a9dfa7bcc9b9d242e09087703fbdae2b712", + "range_proof_mr": "5e765525434788bb885369a0dff3b05974b2abfbd1efc245e0d38b2abf0a4fc1", + "kernel_mr": "00087b85c6d643c5ce81785b25320ae123f2e80277a05c095145bb6e098de18f", + "total_kernel_offset": "f655febab3d99fe68f2fc026c250d4c0c99921d8a929181ddc8b2c77efe9b309", + "pow": { + "work": 44 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "2cc4b924be6dad39e61fd57d3652877e00467271c8d7db12e6411b1dc50b3533" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3ccf80a7b07e226047b4a21f37ccfd2dd4bf985bb3a498ccab3c2cfd90641877" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7ef85b43a1124f4204041dc6b3ce420ca5a6ac314cb6660b8cb66c52645f3608" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "be96a5e5bba1c9b096e78588992a063009d6e41606246fc2894630bd0bf35d59" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e881e71881d734ca15c18bcf696f605e58aa0da4968a76e6a2183c21e76e6c3c" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0883c06e3cd78ced2e89a20b609163fe8bcef4327a0f5c0b2eb3a308428e447b", + "proof": "729a6b4ecfc68a8d9287fe412ffaffc911e476937530c7c1a5a11d8d5dfad768ecf42bcef56dd42a7f4ad2eddfc51502342adca050344ad54863a17f939b907760507788c1434dbc6620914e06dfd5b8974a6009e2345a51fd2cdce5c48d0a264844ab4ee3e50860b5bb2b4d7e8c35f3fa56b749e43133e5b04f7fb0d2dbb149cf3fd140347b0fb9ad34df2d219ca4df76c898666dbf72f1178bb97959b0e401e1ed2f3e3deebf677e09c551425f28af849615f54ed080aba709386e2c7efd0b7598c6410f8f33283ec20cf08a31ead8c94dbfcd76e772172ac2830d1b4c3a083c7c086175f5c52e1469372e8845faf556a7cf44283eca2bd8c9ab08152f35301c1fcfe65cf5464c1d6b5607e56c4981c50a359edeb777841b5e0099ea77996eb4ffec9724aacec4bfe55f30d48d8456a627ac47a546c2c1883a2eb927663c406815228e949a6a012e2e7f1b862f99c05de240d59d2c082b5306394d0e3cbc5436c84ab6b6264cd79bdd084e4ce49eaaee96ab3b7f9db007a9048980f4353f4caebafef8e47c56c35a5fdc40990d963961c84f7d5390907f23c8401a0005824b840fcb5995b4d72b07ff37c232eaa42cc487950d4279d622a5c9f387e0482951f04c5eb8937de927289124ccab95249a688b755ef192008c1b6b0fa4e9de70621cdd80b89f7731085a6330d8271b36e53425a8f8da659e15dc2a9771d241ee37dc51fc20c6a9bca7621b7a9652e59d8c9e808ceb0073c2b6f0309e5587720f692a9c49a4f9363c6e9ca12f54f569970eb174950f24dd75a1f047c27c4fd8e57074a7e556080a8d16cb56413c3db99d57f999ba136cd605adc36b8aa1b7021017c64d3676b9864133017d3203ec0e8024407505515a0696e5b32776445f1ca808efc31d9c40ece5cb647140cdeb4684bddc05b2f0e1fca18896a43ea49d4e3106" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "20ceb9591d812cc0deb1ddadafa1f75708be6d6774c36a63605369020d3b1664", + "proof": "c02981fdc2e44c17e64ed7e2d4093889ff49e718d3fdc608809064c3e9432a627e4a4428e911e8a1e8157cde554d44b1434ccb265e4663a8305c9b844af7d5497e1560ac69019f4316e199e238bd602cfa060ae6170a88e5d6322924a952b53ce6c1fcda21f2b0ea0fa6a658ea749eb0d7bb635324f6b29973c3f1b8f16e214340772fc83d0e1751ecaab730663d7c7b80b2bf5c2aec2f300d14a1d55d7861029e1f6fe71a6526d26d803ea7311c986d3fea9e99a41d14679ad3513ef8a3da0f6310aec1d928f6a62b33c9c2dbc861ebf86bce912ef4568564270c0933836805ca1e62b06f190b868e8319bd1f9f81925f74bce09c4d560c76faed0384252b393069ef43f4f0cb03015a747cdc3ca33f9eb58ca6dfeb914ebfe848af1e8524284c987dbd86ac178c2602d6a8e10a5395c6d9f841eeb404f109c2d17cbbec705ebe5e0525b39c62252d2b581e0bbb7158fa87bb84a141ff544239ab08cdeb177bec29f683c7d99419a460b17e944c7ece05b9282a445907e4886f282befa6da6068e31b26fd685bfcc21593233f3eeb80416cea3a290b52d147fed10326bcb81fd2f088a23e0bfa62338ff776d0dd25ceacc09ef2dc5b08a36678b29eeb965e65be74059e57dade287b7d51c694850d237446dccd38d2497f9898e89abd250d5212ee665062e1ea561cf234e22848577aa15a929b50bb00a7a5282e731a994c4e3c511b3ecfb112eafd45238b359e70006ae1b5f0a5b688970b7bd3cdbec91e0a869d7cb6c7e185a2ee80bdec8f4acb43b77845f9bd4c2ecf9f6ca77c2b998502aefdff23848214a680c21f2bd97ad2518891ada82cd5cd975229ff66ffe9825f4b921585ad22dddea8c788b5db05b23317326ee27014c19c92b38be8ecb8f009261595c950214b4431ffa0b427f1fa011febf16ac6dc95fe82f06c0c099e1e06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "28ab40c27ed9780dccfdaa77f55327daabf99461ca43581f7525d48f41c1db67", + "proof": "d06fefae904c84527bb4252beca3635f80c54daa992ada75f486105c2cd9ee1a6278035661d3e0ae9b2cd5f012b8bf70f0460f371105ee958604075c24c82a0b0e938bf1f42a0aa0d8aa6c8078be8a7f9c9dd911ed46b80fd354ff397c421303e8bddcac81abac5580acc5022ac7b7120ace9281bdf39fb77e4458ee5c999b07c9c850937a5bb448b3de0e3034b8b71b7dfa3261cf1bee9fc763415712e7630e7375080f308c04a9f65c3ab11928e4bc219495c82ef6b0ccc62ab11b1446230ec439decc98a685923008ba35bf1e8ff27da6c796420d23f04522efd02b78210126746d6d63bd275796f876519d6d09fdf66bf60b8f0bcee31275f7b3f635e419685f19f09fcaede17df9f66dbee894a180e79a5f11e22fc45d42d3127e822d57faf47be2617453304a0538c6d0fc3c7ae55ed7f8d7be952a865c7ebb3569a7609e20572d94854f241ba7ad6b5bd54eab4e38762059a4525717aa75b8893e0c44f8a54d20b592741c6704c7b51e71d2615ec8b61dcbff2dc7ffadc198c5db693ca6618af16ba3aa9ba5dd0ce61cee4125ce7629c1c6a3c12cb40bd83a80c9bd19268bfefc1d822e3c09dd495702021dad72527818f12a20c924e1ad41afb88b0ad00c85298c5bc04e406138ada78cc01aeb784a6b2a5dc6f8133948875a19752d42abff0d92f377c695f060584bf5e786b57a252323299f2740ebef972ca6456e1e3ae76998a72e9b0d2465f5e2ebcbaad01a1cca45b101864d76adf85f3feb3c3c7a4441a837d5aac683f1173a13812a15c56ac33c2dcf3400c98a201aac750536d340b9535b540edf884af5ce5054e4e176352b35c70144d3f284ce2951a34f59ec18ccb94b2da9133877a6b0f78ab9d545166d6aeae2de832f832e7026440f0bea54d5198aaad9c4b898e0b2271e5b1c67000e64e21a10836029adb7a39100" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5ad799d73ff35784d45e4041133ce4716764861f78d2b890defed290089ac207", + "proof": "203268fdd28d4d53c25d01417ba907b49f46fc125f790b4dcb5f2b436419d34d4c08a75cea0a6bf4b39dfa87f7ce7d416420c3597b3672457a35ae3acbf70f38b67cb3f83a8dee9bd76d49e41788268a8cd0ba7fadf19efcfec473c9e7e508788c35a92ae80e65ab02ec1d970f0ea284aa154efa4ae7c8a641735089a974f80fb48b745d9f9f0e509dc11ff9d0d54f06900c264f9308a147bbbf068aca0d0c0252916d950d92bad3413f904a62ba6293a3922afb5d3dc9a439df445deddc5b066ed7941be1873c02f6c307be9c7e933d002d4f0015c64dbecf0040863267ee0376892f1b4f99e338d40589665efbc8e85d30ab9bffe9edf5f166ab7585ee191a826b16ba15bcad2d2b57748c5d3d9489d285c2a0ff0c87bd24be3c877a576c5538be9581a44a740b5819ca4ee1df9f218b211f4ca37489a9fd2cbf0c1e4cc00c76d13a5cd98b46e2b6e1bea73f465fb73837340197eb6d85172a59c781b6db6c6841e95654549c44ebec44e273f55e3adbd5e4371bba299517b5152552cfb522f49ccd01cd5aa4d91bb53fa6266eae8fec6a256cf85c4923aa82eb827dfa4f08b4edb942f3ccf3b154bc21b7a692143339e7beade96e1d4f4390f10ce27211686ec61bf4604a6697ae2d7b7c698ed8d8c3083d4396a602b3cdc032a639190c6d9af4925a6613de5afc3b3a3fc3d27a7caf05205551c7c715a9d7d52d5e25051dc8a870fc6494e19e318bfdf4ea9a907f8fc830e39935d95c82a7f2b1230f53622863a6f4b8ac3025cf3ca9652d28d22661a18bef7d7aad731c9219176d455c09c4a96f846e154f47cd647372bcee27b90157059b14c73767c8d67bb28635d721c8cb7d8d5beb2917dbf44dde6768d5a844847354e2df54d1c6cba774de1ebd0e173b27ff348e9b1356d2544b36f5049754f05282fba720d5f4c1d7a12d1eb603" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68ab970ccc08e32ed03302829631a63dbaab273b39bbd7d8b1a1c6c692a0f139", + "proof": "5e9fad8f8c89fb0b91c3cb848e4ca9d0f44a5fe6ed43f5663cd9a77fab7a3b49e0b0311fcf9343bdee34df96caea9d880ca04ac97ed08424e3a1f5b2d69da43428c2f7cd0a374eb095658b3ad58a65b739f252076d232a3768e097daa0dffc52ba76110bfbd54b43bb36cb4710a3cf2d1db92d50dbeac023f967481d21deac22985494ec0d06a6e7218e639995d3990b310a07c4e41bb9e6f12ce21c4b3abf0ebd8192cc29d687dffdc121647dccc18cc56c08e154a41b60fc2638b723f8dc0c779769f43946e1282f446b6941e4ff6fe56905f3a3a6adc6c7738c891fe8d903e46eee9a26b5b715b1b60e959a25f714477a86d0ae26c0945c4af97cf7d3b8233852d3270d5b0c81c5a7f4033dbdd660a01198836993efe806af12fdf87dda4540b90df5f387edf6fb3786aff2538b944bf250bc59f9ea2fd8e9b1222a7e58023c8624e22d6ab853b8d4e788e7e7d08fae88e552d0b1d08249770159bb0dc73e3e70bbfc88e9f66d5ce048083fe6b2476162b45a12c0ad89224bfc4a02fc0f75544bfa40275369bf60bae12620e68b67c111599ab664f39c2cb6d735612243509aff0807ba4cd07f04656f8f744cc8bcfe75eb005c549718170dad49f4bf72621446678905a851f685c3ff814d2bd68e83f19c0fb30f99d4b1dd933ccd5f265240f94d977afa7b24a11ea1156ac548a2d0e8c7d414165835dd242249b46d1a31760cf482dfadb9241663b12e2068cfb2df0827fd0d904542967a85c6654dad6196aa5e41e4749d34ce05a692dc1699d2e43c3af578b2ff8b557bc08c4710431ca2c6499ceb80933324ed004a730637f5b817f32eef4a0d91f105801f8b43f246c45842caea5c99cf9346e9d9f0ba143aee0dfb492f962e4b13a925defe292e0df39bdc7df0348adc2859f06d7b70ab2199e8a25f71d0f4b4577abd8066f3710e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6a8fbada5db3771dccddbd8df2550fa9a12bf64c45485937203941a5deac721f", + "proof": "7c5b3e5d28c5e8ccbb40e7dfd89d5db3c6df85b6ac1caf8a9f688c721f386e2a0455645d1da03f183e8a09983fb4daf74477f277833c13f9c81c83ffe124792b5e58a8d64e48a220e9b037c23b7a63c3fbfbae7e92cb1177e3c462e5c2432f173e1a433bbc94069c89c88a2074eb537b3c180f4e22f694e54e3b4cb218b24552ebc60760ad057c0283d70b98b7dd53562df5e9b3fdb47feaaa9cb6831c4d190aa1b44dab1537f4624c846cc1e84e2e78fde93461e3d2e389cdcf5a512d67360fcebd95a4f61b6c1f8f2f398f55bdf2ccdc7128759aec97dced9dbc5f269f6c0940339faeb0c5f9d59b003cba0cf59fa1939482fda7d103967a37be6111bcec2b48ac8a44fbb69f0b34dd6a995648e831f7d492a3d1e97f2a1f4d537141ac8c37049a29fe9f0b244833852ee1e1c3ff5755bdf66091736500c970a44d736051697809cb2a15c23517f237714a339d44179533400fcef973e5f42c7efe442e1d149e3df67f6a80f5022570acf9d53e4aebbbe6fd51165aacf3d94aa6c39589825db05f1fab6a7bf3f5cca3c89c5524969e80bc8d3839f2564c19d06ea75541231e56b13d951ab8dbef3528ad2cbaa4b8056bc909bef6729da8a36291cd2a9af965c8f677d7b2df706c601415d7c71dbfb2b0bab8fff305b5389043f0491a893f54a8eb4d2ecd0a17d85a4aa2cf81a096e38ac1835f95ac947d6fdbb550117daa4ef2b9300dfc23d1172f0868d3b93c5d1c705b8d723cea05aa2237959ad64d1e2f108045aa6ed89db91f586165254d7845c5738a8aa66aeeb991ac216f684c0b5bd45cab6c9f39c463070b6bf13e7137207ba3070bb088e1d27e3bceb72553c74a88ee84ec64b82e79aab4cfa25bac9db81517f7e252169caaa8c30afb61f10704d3b4947ca2934efd21867de69e69aa2717d8b8d532488ddc91dd56730ae2a809" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "722009ac3a6788bd645031cf8eec09f8bb7ae632ed1999a8d4f2316f48936b47", + "proof": "a2c320df2b966445fb81dd559a6946fe4cbf3275a949e18a45d999c8ec08de26b02830273b19187096c954a4422601f1ecfd3526fe5089fad851d29c09f3201eb08fd6a22324bc0eb0fd8b054aa98e9e028ba7f48a4f73dafd781602708ec875e6df529fe3d84dfab57dfb017646b17e9b82bffa6d0e6409ee8758a51df3ed655995e18796ddf38e78afda178a4ad4204b70c4076616e139f15cd5e66332240ab96a80288cc9615e4d11f6b8025ebbe80b71a1223b766385a7dd8eb94f29af0ae3f45d0031f4f25ef33afcc6d160d4f20a5a01fc17556d2174094f0967f7160422a36c8023293799c0ae5fe2f98cbaa2b3baefe9f06d9ca8fb8695d352ac2307029e898ae810ee67577f1e39e31c60b1afb7d9304910d167c2b54d145d0a34564ed8a94f87fda9df1ffa10922a0db99a6395a695a4589e310f8e8aee3e18902f30b0eb19ada831e10de2ed02453cf2c6280174c4c4d1181b879748badb6fd937223e44aa1b3844d8befc709a4d291d45072dab1122352df04cae464a8ada362af61966d88c72ba6991ee9b1b74eb78e857f8920254dee58ac556e747cd0eac34724dea9ebe079059d4808ca72f4be3aaefaefa6e0cca3082f02c7e5a5eab2e5d38e9d363efe950419ed04bfa7ff760ae8599f6342463604885fffbe171e3bd39eadebb255dbc1ec6ea8e853d366701a43080e3e1a2b7eabfe60762173ecfe42ee88d72508155d573f9fd533038c3641eaa11f0aef902bd0763a446275aea0f0ed6562ec7f29f174c23dbabb53a0b5cd3cb310e8d09cb7130376b1c57d19318295c51a7d6cee6b1b0d21237eda0f1e51f0d32fb2dd5af8d79c5f78d00c6019f7a9989910e8c1cb3805f3ebb700863e3f27abbab289e16ec21a5ca057e68794101bf5f5ca4cfb574fe180076b017487967c2c8d38cc2ed05b3ceb35f8d7dd09a05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b060989a4c557cb44ba18c2901314ebe1a5b4c21bb168121af9a86302e7c9432", + "proof": "5aa8ed1faec3b9f8973504f87120f731a23da665d450cebbaa8fffccfe3c84242e8a79b81e52487ce7ca45707700c3daeaefebb7bec3b092d3a77986474a66564c399df0478765270812adb21e1de439526baa8f1559627944692721af2f034fe2d1edcfe917f5b098ab415464d2cd9d1f67af2c51a59069925387021101610435690602c9c88f24c06139f3c26a5e348c555d866489de4413ebe0f2ef7dce01e9562daac1fa19619496e415c6bf01c17a1410e89003157fef287175be81b2072ec19b39f693728211ac9afd87605e993318da4205c606ddf35f67f7e58a5c0c5a4cc89d14c4e04a91f648d62cae4f81b541ae0ec0e996fd4a6635042ae5c9059059731b0e91d4e6c7ae87919c6019446971d3892e0c092e8a9088188d3b6a3e38a6011a8f6a344981d9139883193470347611b9f5f6456ab5e0e745c6a78c5b0cf0b83c8ac12d7471c33c5823e36a85ec52319b892291e259eb6503d42db91bee5c6373422876f3d03f844d29a193caaa6a67f56ec508b81a50d712427da24b7ed84a8a489718f2aa788526479da6ee91b0acc4ff7737ff7d85cb04317bfe29f445ef57921a626481fa25363f042de6eb4cab56127e916ce5eefc874e990546807095c3f5adf5b43eab0399075ab90006dcaa7e3838a8aec37548d6f8aa6830feccae6901fad811a890732605d21af7f55175ea0c8e43d2cd423aadccd495023cb30dad591b4d7a658625f0a737e47f528227f16a617ba82b801b0bac355f272c9572c6fdf0ff0a4e8952986d3f08c7f033bd20caf29769baa74d62d53a347f38a5d36fc1976320b50531cd672643bc5590a45bd745133501e28fc8e089262d473f8dbf53f06f57883c500ba10d8e237abe94b48bc487f2b328be6f798a810066c241b66f82e48a282ccfee5925e035d2aea4d180865cd6003f1b8710eb180b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b4bd8b0ffa8a8b77c8eda11fb2c63741cb8dfdc96c624682c18ad3e9e030d32c", + "proof": "c8fce1de3fc96623734793e61cd901416c5a5de906eca42c780fec03cfdb5816366533eeeee4bdc8d737039688a7732425cc9b20d6c2bb8fe70dd42e2a8e1b369e8614253ac831375bb9e54bc5cf6864d069ce0a80dd3f79b4f8602279e0dc25408382bab32d6c30a21dbba0de23c8373979e99c658d416d9104fda78355e120822da0af9dd2b5942328c5095e59afa97495def604d76ceea140b14a82960d01b29b7ec6d699953a6c35d5dfded59c2ae088d7acae0eb75354344afbfb8acf0ce409ff99da6b8c8eed5e663d5aa46c0e259b7b7acc74e547e4b4da6d23b6df0b18437f89d8386102afc51c94b5e30bedf15928f3202c71a40a1cd1d58a51144708a5829c81542964ff397e9cd1cb0343b907294bbc08cbd2236a7ae380bfea1936f858350ff517e9832f4777b334898619e540e20596e17fd282e2c10af3cd37a27dfc929185aa60bcc2aea75d90bbf3c52d18ab8fab9efdd80fa0393e004b75fee0e245b453d9e9fa558eb6ed8d7959f67f85de093cc7ff7caaa0e78989ba60b6b24536acef8b20b2a4131885f7ef3d98259da06fbcff36ae3a904e25d0b15d7c90fe9009c7e57e0500549f083f552a844aa7396eb02f5a56cf8ec9ab66e876f4637a22d5d93f02403bedb1cb5680422ca271970818ca61ada144ff031b9c37964e1b2c1b5c1e7209dff235b1c9970d526fde979f8c4c062d53ba9855674621da1df430dcc24b92526c024ee0913fbdf1bc363762ad05cdaed04f6f5b74be50588c79c2cacfef073b5172d911cd3fbe44b52ee2a7c807bb08ecdd0723044173ccdd8b16d6e6eab570cc88c2fe645f91f6e3ba2b689c565ed097c574b8ecde4ad8d4ada60991e1a218c97b91bf6a1305c6fbe9fcdb3eb8c566429a457a3eda08b38942c8df2481d9c22dd4c70a74b76c12be6c6e8071f97d7fae000fd8305c09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "be4c08c281b4ba5f637dc9c096ebf94d83de9310470530d055774fc220ef7057", + "proof": "ac88f25cb0156d405870b6c1291d4b8f6d0c1ff303edc9fcc617a54528077c218a9597e60c6e8c9bc32b9111abbe230986504dffa68d9626c02c12fe1923213a846f446ad7bcbd585acc424f28a6b6a226e37f274536407b69dcd5322ea8cb236029e664a8f9fa0762d7837b2e9f115786ec665bbe138876592ab583f4f997710f4eaa1a4b2741d99191aac65f7e268568294b22fc8a884d8e66dc07a32c520a48dc51b318abcc39297809787b45e4ea41f502c2dbc24698d3ed2d95eb1d55077429af6ee56cee3d9d25ad0ac7e4a9ddaef461c8e2b8059a21dea88e16253200967819387e564cc0fb0db974fcb8cdb3c108161dd9052e046813c864ac2fc37e3242d01e9c206639c25c518c3cc7fa5ff132764babc9b81067c384f24a1a953d60d46ff7c0f9e1a65b1dc1ec29852a66be38cf45aeca18271b045931d41dd066a2ad5902afb934d25b43a6691a41375139cbb36195aa9336ce55c84a508145068c0a94b34aafbb0d01c8a8872aeda8fa15b9bcafb50e6fd8b9c301bf8a38311fd0bf70135bcfee4111b30d1226eb53f4bf9fd532585eddc6ac7921c344916c7fe4b27f0a22c0486cefa42e5af3559ab174b3d08e7407441b35294b7fdacfe2086434bb9296aec875c5388178aef8d114a8e3ef0a26667f1f1c8c5907be9d21242490a70043637a6f58210051174ba3c7d6b3c189652cf7c46310e9366030e2149a4393774077cb2e6ad58c5a2432cf3b7224ec032fc5aef46b5161dd80e72f2e10dd4567fa69b11758edccebf47ec2479a62b24b1469e2e726aecc05111a1565222445921b4d4a0410b7ec26dc27f76558a7a299702608f042b888ded26b7a56388092ae0abf2852a2b327314ac11fa6122405bfb6033639a0fcd82d77c787001b562dee3a6688a248b04f1c671e83035ab8962365016ece8faa1a807ba1160d" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 45 + }, + "commitment": "82526d011ae9775cccc64b8d1d6bb30e270703781128977a854cc3747d802d3b", + "proof": "1a9686f480c81a4d343797eae526431a5bf457e9d4f909f9c6f5166b7ec1705120c96bec02a592c5a0ca5b398604b90e94759c1d8d3f08d8f8038a39ca7f16029ea97d85c85c7906488b7ea6915ad494b711f26addfc7f364e6a14931e3f46122c9c9e52a487b6e2bf866c123795e6a95b2a53815e041a96a1f6d984be08ab67948e40f7e6f140ab7e1b3e2eda505bb84d45644cd28673f32f56830e4598080b09873a3064f58c06c9cef3fca7c82b56e7cdfcf83a155c90e003fecc14f50e05e34c66c066e2543eb5a380848f6e4ee3e0a39bdc3de50db6022f694f7bbaef099203fd5dc503dfbf1d555663d3f9d3c105be8c8f4cbd65265f172640ed5fce705622439dacd6504c93747e4dff187ac32122bbe5731df4c13f666f9fb9a14f77a6897cdebbd6d1d1f20b3b3edea9dc32e47826e34e8d7c543e756d58a8343845b60f69cde08ce797f38361af08dc6d44ff4db70946f1d00e9dfa45e54ffd9a720a2f34121aa854a91974ca104c3d224821e8582d924f3e0cf2796b35dcdf0313b820c373d18c3e6eb5ea6f6ec690a8e6e89f038b25dd2db34cf31932b9250c447e09e16fb2d6fb0dd61be278c30ed19a32f38f6260d3cdcaa77afc6d1ecfcf28308fbc11156fd033ed2798607891c15b5930868f780e716160a651035338627caa613ee1bede05ff4fca98f197e5ad1353280df41e6675556376af9f0084524bee0acc2c596fd4004f7c0d1a10afaf51fc494a498e3a567b0e47d736f410692b44db20638e686f286c202b1ffe686b523cc0ebfd6b544a3faf8b8d4158bf61686a836d0be439a6708b2c1e356ff7490fb0e1c3e7cd7dc46468404400a6d17e4725276c749e6e503c4633205e2a1d4c99b2c91a44a6ce634652c15731a71d710493ce2cd2743ba314a2edb1e927d018c597f6a589e146d3e9d7b4d0ff61ace30f" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "48072c5548a1868cdad3cdc067599e8fc9cde4d21b89321904ef7c63ea41a517", + "excess_sig": { + "public_nonce": "e8bdb97853104d8be71793d37709040f0be3d0a0ea85723b5da6b91dd9143945", + "signature": "4b4cf7777c6bdad18643861435228af6da35df565c8dd7f0d09cbd375d337c09" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "86a4faf99ead62f7b7286b02f0bd572da1841ee4d83f2dc276d1e19a99ea4e75", + "excess_sig": { + "public_nonce": "b8dcf2a6732ce7b7ce07bcaf419c832c03ed2a568fd1aaaddb42bb7786140934", + "signature": "d367307f424f997071dc1868e3a3ea0196bbafb36385fe2210192fa610b0960f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "908ec93b9c412f14889a96c7640eb764b730192443daa2ea4beed127dabe1167", + "excess_sig": { + "public_nonce": "80b360fd5e61c993da30080a5c7971bb63a1c68f4150159b3629a570cf3edc63", + "signature": "e1d70c1ce05aa82446476156eafb8642f922ee5918a3d10321a62c74f0f0f50d" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "da250ed0145627dc396b39a2b36593a41969f0ca970323b06fc175c0634bfc31", + "excess_sig": { + "public_nonce": "ba799d35eb1ea4cf48d7c8d066567c0291f92bb1e18eebceb9b8b81a7dd1ad25", + "signature": "551df9dc08951b2498bd7e6fa2b81bf8108b8d5e42d55cdb454c61f86e79cc05" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "fc903eb24ab7b25224fb351bda3db9bb8faff4686081a80aeb8d05b33240452e", + "excess_sig": { + "public_nonce": "f80b7e758f6414de9df5d03f7e4bf8f075fc9a9e1ce9177909edfb8c3bb1ad2b", + "signature": "46550007a5aff40032b46c8797dcc038502e3c3a50869c34d15b393aa31c4004" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "62288db6095a977cb3c5881150b28f2efc1824728ede92f64f953f7a1949e533", + "excess_sig": { + "public_nonce": "a2dd71ad8c3972a9aa1d03f0fd2ff08177daf2e91f6f30a35c885b34fcc20a05", + "signature": "d05fb9ff8a135a6e1c5c7f298dd0c0d65489d32cf851d21fc7e10fe53332ac03" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 45, + "prev_hash": "b64cbbdf0278aea5d33be1ed264f464d2798b0f31a3575e2b747aff976f63457", + "timestamp": "2000-01-01T01:46:01Z", + "output_mr": "6fc48d093ef1cb335561ccf7faa56c04d14734e933577432c89423c41de41d78", + "range_proof_mr": "a5fe34d11ba2a7f346dc37d14869b079d99812b72c2f6e02bb67c85da9ecd35c", + "kernel_mr": "2b59267c111447ec16aa8ff8364a7dff5016fa045bfd4bb8eb898f4b28b0f7ad", + "total_kernel_offset": "2e99cad1317de7ab9894600b2c00e55ed3c899c3c536560512d5fb766b7b170c", + "pow": { + "work": 45 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "20ceb9591d812cc0deb1ddadafa1f75708be6d6774c36a63605369020d3b1664" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68ab970ccc08e32ed03302829631a63dbaab273b39bbd7d8b1a1c6c692a0f139" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6a8fbada5db3771dccddbd8df2550fa9a12bf64c45485937203941a5deac721f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b060989a4c557cb44ba18c2901314ebe1a5b4c21bb168121af9a86302e7c9432" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 23 + }, + "commitment": "5081312e47ef838bd52100e61a9307ba1c7272e3ab4c4905b9d5bb9b4d8e5674" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "04d648db58377ed65972d1e4fae3483993b5aa92823becb3140601871ed3927c", + "proof": "f2ac778bce248f9460d66978f5ed0fadabc83cd856a6609ea44785e3fe530237dcfdeaac427a13da830fe46131207231cfbf85d3af8b96f64e40fa4230acb03922f1447e89cb4c6bccfc5d021df65e8a8bf80a4e3bed46a316d4c3e701abfb7b561f96d910bf157da0045d003a6319947300962a939ce7846ece9ad329f7bb5f308c7aa2af6f469f905f7a74df762782aee695c31c433771b3507041254b10011478883140358fab80508f437fe700d30ef789583f623f9b5b3500dcdca3c20044a3e417d290be6c6b672a6a6b6b2030c674c33c2d261155e74d4fccda810202e8c8cf4b5b7a3dfda71a69d3bf1bcac214ee96f4e5a4b85a928b2a596d5a626c76f5a9f91ea54d4d76fc51f446e361cb0f5242bbfc30d434d6e82ec3d2db0c20046579c8fe63554ecc83fc9cad265cfe6a49c6f35ef25e1216e9c1a111d13134802db99399025aa04e77f39097a19cc2e884f74624b4d8cf09206fc6fd34af1cf66f797842a0ffe9b653d7bc6dbbd7b1848379e21791cfed9cee284381f92a4a96967f628560387dc3d4bbe30498ef4d5ad14ddad34b639eeeef66f68f5a932960c5fdbc9f401d94460e92bda206512512a5345a735e894dd511f607ba6e702b1a5001fb7e1000104c906b72698fc785a8394da05a27ef950b35ade0e4302523568174c06ac2ae3d770797730a412fcfb375fa28301013b6751193389ad4c84354c2fc7853eb53d17a95d605bbbcb7293261531194792c1e823b58258b14fc142a40b34bb1f1b7b25c42a127a173a5513239403831f258e9c9226e4b1450bb724820ebd85979effc4eb2bcf2cc1ce243870adf763f4cb4da0d92e7d1d669c014f3ce3f74045b8f87500e62054cebc749ac90b8bd51e6d73363238b326ab05c0288c80b0e0e60badb9cd14ce273953db5f220dd33b551fe8d5d9a14ea55c0aa0c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "20bfcf313274b946b5f93d0e5be8d3e1e2e0624d15ceb1bc47b9db1018bc3a63", + "proof": "ac212cf4c9882616b7025f52242b84c067765177dc45003ae8b9436c02f6d23056403f1a85fd38253286d63764b1862a5739832ed247ee144029edac6cf3874912b590bab55bb154162688d87b63962927f0ae92da7f8ccdd67004bbf6ade27968d7a5197a699b6e357344f640119725b932d0800fa374fe83fcd66ef31dfd27c9405261634a92da2c65a7019da2055da46b140d9734daebdb253522c65b3d020e736c45ef35760a95d58246e1844cf4b2762f456aa1dd3dcb35e3fddf0c3d0da5b3051bb2bd288988c15fc169304c7aee3b43fb2258b97f1fdfc4fe917df30dbc59f197097dd4ccb88d5678815b34e5de2cd0205a6c52317855f36e2e635d6e7a68a976e4150d8fb4fe587bb9996b1ce7bbff58a4bc987fb8bbb9ae8529b877609f483c71a943a6a002939bb261cad1d529f996934b7467ed17113f5bb8de26de189f97f4a4d7fe6706ce836d3720dd4aaf2f7992239366e975dfef5c7bec517400f0cce33358d1f356385c3662fa210034f69cd35b1bc18c617ccb97d67112147b56e697e8659d6d22c7cbf93ad257b67007933d4f4282dda41564a5bebb20c4d9746e5e78d4191d1cdea266282fdd962ade40557f7cc64eec855ae9404f660a0e90989e4adc48e93e49579eeb1f5b04038aaf6586835df3ede3c91b3c10759a5da79997c552b253e4408639173e2ea06f34f844900a6a4b992a2ad003e3241447dae79d3e9ad3b3f95ba78a22a040cd63dd1f3013b1435e44ed9253470a2c5c581ce0fd0952e13407653dd2e7cfb4ee09ecae6f74bc0b91c2a7b3d7a079469050b7fad691d764e4e0cbf2f3d4e888435d37ccbb40953dbd9e5e9ff4d1431dbca934353469ceeb1c4d254fcdcfa718645c2b3716e475c949dfd0fa5a420e09c8d2b3fef48234d804dd8a02a4215f78596da39c4401d483679dde488b39940a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "348f0bfecbe761c93898ba87ae5a9c74a53c07184df638849dae7c9b7291e570", + "proof": "f05f5c5715efd4bc79b8a4b3436af83d028071282ea04c5413c5de6a84ca385118bf02bd32ecd9a124628108dc6429e9b92749185694b7696cfdfe85369fbe354648066003bc8fd65c1fca6492cb57fa07007a2b04b6e44dcba38c7827726c08fe10e97cf41c365789dda2e17404d78737ca8436f14bb353e532f156f31bf358d202ada67a9ee2296f3d8b736df319fc6833a85a39330f18cbcbdd440223060560cf0ed034c840a32ae0dc89b389fd009a4895919f07a7cb3f51709bf4b62d05c78b830fe55af858d19f41b3089ee018a986e8292e691952fff9285bc55af30b403c2eb19447d88fab11d74f433872af2ffe52dea7b0671794ae7112cf032e09d0c4a9cac565a5234eff75f1475fd579f617063db505826295d156ba1cffdf7c9e8eae84071ebbbfa7f6210a15e591281e49c2bda4e100769df75ac5226169171ebf4681b9672cb4a4e8370d1f35a30dcfde1d3fff3e7ceec2835e96fa749b6c424cfbdf8180934be38ae3e98b3bd0abdb75f101691704d6c7eb7e7b2f43557b7cbbd3d05fc4ee5c74a4e0c4d752f9932bea455d1d85a3e470259fed4c316d636e6ba6450667a19243f6962c4aeaf7e294853fe281e92db8c92e00868413447134981c4d0f3fdc320ac98db47ebf7bb60c318782f3cffbe946e9d11cf1d1620ba4c7f435cb25752d310984ae45f233af7172e9569b7a23c04f810c7bd33cd51454bf43d831bf0c1160ec95700064e1952ee65dea169065553063d94a30eb9a31c285e24fd1c1254f3da4670cfb04a895e27ba4e421c93fccd935699c074b6346caae0092938f66a234d02da9dc05a1fb5ec12f4ef344fb282c57d9fa3d99343fd35527b4b28f066002d04066034099da0928728b544369ada325a3184095870b6caff746522b282a35f828465aca521221a08fac6a9d83b456ddc2b1e7d7ec00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7cc4f8337e4abf0661f28016426192d7d5dbb00d79f67a6da7f5711f9838f60c", + "proof": "44f9ee7ac6ceb1a7cb45208f979242d288f18e235aa31c1c1c99d5cabc976004f8266a67e09778f069699223fc65c6f644826f56a93460e3cd10c7dafbb87e14d2b523ceaaa4e127b79033ae08c6ea4a1bc4d4b2634cdb8021ea96185b1afa15de8098cfe8897c6e31d9a5a69b996deafc902377c9889b347e15fb2a163f02424b51779b6a8ab670cc29a750c131d8136c207d96a70c297867c2346a7ffac90500d021c38ff8f3493ec0ee59064807674f879e2a26ac2cd453ccaf8efd692801696d5043b4f320a7186b0061f92fc11a830bc1547b98e45e2533cb0101f1fc0c5a076c9b3ebae8954ca71c695bda505656b843873a7d4bc60764eab6f405d942505c3255784f3b7f7dae715ab81369b44f12fb0ab61f08547a9646e7882ce62b6c5f794818db075bf106acff9a8113ebe56a92f5b75359592aa382244ac31e4d0a00845ea09d4df700d314998014bd327da90d29da544fa9042922163563cd24029fdafde5c6e2e08d3809a3ab02f9f377a5f9c2291445c3dafafba6ec81d3728421fde5475b2ba143c9a6dbb23a6039e9671a9c049f86f36a96f40f4b7ead52723c2c1bfe3af17c466f61b16487941eeb22e3c6c3198b1a0f8496555a3cea794ee5cfa8b40ac348698e8f996d6b2735d526b463068e3602c5bc5f83dc07766fc2a76e02f5616b3271207da78cdf0c325c3e6de5d29fd75e6da4b6294ee51c6462b4a6caf8d8140cd0666f7d901884d59ed8a3f42b251d32e5f9604d4f92e646c0938640a29f8c7e87903dabc4f140ee8b290367daad6ebffb46473484810c15dcbe81adc1fe1aa2d648ad1152c85205570da056c62471a5ae33fb6155ad7a4a9b34102d61809f808ece4e9334da3d0d4c98e1fa8b12dbefd4f5102b8fed3906dd49172020108e211b9fd5e3487b954d32f3119729a21a87f06923b280cfbf09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9e6c74413b7594e8a61d915ae6739727c4a7ef733c5c6f93f4fbbba3b6b65849", + "proof": "c2cff296c0b4bd91c3422781d7ae7ba39e9f5d39f854904a023dbff454f26e143efdd96e6aa448917766063c9515fc6ef48b79f83309d61d86f3c5f14ada734f88d72f9343b6d3523242a4755618058473d4f1b917db905ab1fe2d35343c812cd26f3652721a90591a06212b839f9600d68be08b354568d23dd5c499520d1a716ebd0161e147889a86e7da33eae1498906be2d0c8bff8fb47173cc6550380102380c42409c74842f67ea03daea765f6bb248b36f026a88abc3dbe30f9c5e9e0bc54e9d0922efbfcd6d4e887193bf6f772d7fd37e483f2ba238ce8c2755d36b08aa75e1f6cb2a0df260b1336e2f803a069a95ba8951462431a9b5b06342298b2e8e9e6ace01c47ca3aea9f6fb8888d508c5c9ce95b6d4ca07daa5749b1de76c19727aff5503c4615beb25558550abc9d64adaa6424f310f717e79c4fb9a4be62c0631fba727b4c249a487393e93a5d0fa3daa75e894dbb6e59838c240e7fec7326056e46df9d0be9d883c768c79fef76345bab6db2d9b576206671d93cee34725f06e0c4ad8c7a44899c920bb8dad427603e2e588749cdc48b5eb64528ae72b1b3ab3c04f2fa6b5b6428b4bb6d4b9401e53fa06206d304e475777ac8b6f7e8c525a962da261dd4ee0830b12f591b0922253b52800ecee2821ca0113de08c6c30b909bbca4ccf18faa5233609ad3e07cec21b58378a9346904f169eb0af960807a868d1603d0fbc9b2216afca80b5fb61bfe0c2ae815249cfda595a6bf642c640402a0e5095010939d43879b0b55836cd2c9d77893f0f0d6e07865b8fec6d59c7ce8e1938e4a88494c54ff50be882702f43aabaa779f5bbe1bbd45047cce658253ab3ae456a3797237bdc7fafc49591e6dea3bd00daaff3a3d350b9e6e390c790e7d34574d808baff167aca0efd5b718f731677d250955efddcc2e894739540d02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a27d9572fe1829d8d56d341fb4bca4fed301d7001491232c2c165fcf14405951", + "proof": "aaa6a59144ed1228a2a40d0f47f7ef935a673b23c56355df2fad2d838c93a670d226ba03d6d86c2fe24ef92fa21765f497bdbd5969df3f2b29a30ef4eb96a75adad314d1320ad7c58adf1131739c18ed1d88d15f1445db10c5d762429e46aa2e986861ddfd4eadc1582dbbf3aad5cbc08362241754739c235192ed2a9b52ab3a4e631ba5e2c83c363e06c8871268cb2e4098b4010c04812cce3f08ed4fbaa50904e72f645d770c0b65915a0d5f20d5ef92b085b43ae3851995c222d70f22580f2d6ae48075e661b77021d6d505e24b30e7f093d5220ddfe810e1eb17f573130a6e7567f0d610781dc23d79d94f79a0b0aef476ccabbc5202f0913b08bedaf44b5ebc414d17fa0bf4b9a57e1ef77f354f1afbe387a25d8b1c73aed4cb6303db04a2c9b18d49c729a12f3503d404a43fde0e1ecf1e30bdc834d6e6f63cc202eb6d88a3ba7d5bf65f82dffc20326ad30414d1f420ae798c8ee3e89f9b7e4fbee6181aba92e921cbdb68627f2d48035867ec6a17fa5136ff9e4f3e00867d01a38b15705ccc4e589a6f013608127fb953de21770ef27c961346a3d2f6c5fafadc4e1e04462b9a28f63ef3a5bbb6da5a88de0126b500b4b7ab0c24b6f1c4e68f587740209b06013a4c5a4dad882d69953d35cd87dafd7775be9b9d140648b5192d7478f43e024fc42ce389b0f1a891844e14dc8cd98afc013e526c850776fee48f5070821312ab0cf422358d6cc0735e4cca3fd363eec8fa9dc18a9f1153e696a8690e26d90a07c94680eb1ced30115e02f45e12d4d926ca9f2768b2be507755721d74ac57347f78b2756f17ecd4cbab05e88586c13e2f41de36c76f5fbaf42c2eb233b8be1368e44bb1f9b615b3c579094f35e2ce070d14f68ace1ace63689cb991076df66b7f6c278124e98e5e25df9ce3c149be5fee49921d975f45d605bba89504" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a832d10edb4e5243fc7446ce898028664c7b64be12a47fa839e9112ca7a79953", + "proof": "90a6addbed349b3b3950dfaf991f31cf615078c53e7321a5406bd1f0e08bff7e76ead2570ae36b98ba3f8d1b089d7233f62bfe73f62913f7e2d75a605830db0fdc76395584904e95abbef63a047f1d9695a86f89e2ebcdfa6238d19d18b9395c7445622e2e8e49e32232502450d4bb15a58f90194795a796d8273663d14bc978b2243e9604f9fe4cf0a057285efcbdd907b64a85e77ee61fcde105757c255f0b30530529f3d6c195b8b5e8d351d5001577c86e736585c5909cd0dd9929f98f0e5de8735da019dc5952bbfefd8577760bfadbfa7178f3a4f3e3c49f3966597a0d4ccafd215ffba06f6bcb8fa2846f0cca97555ac6c39af270db7a5e391de48b09e00e796a3b04334372e4766fe3da0865d9aeded1d565d2a86f8b63c60abf457d941dbace2602ab37948c37f093654a19a4543cf3b218d5fa71c8af50a01406132a1e81f3057969f732dec87c2d29577584e9f17e9288bfd16c3ef90f95ec7633aa8cae22eda43793fc43812f36792ed80f98f2302c0790cee03aa3641d962d7a8cf24d199feabd7a816e3084737e99fe2b99fcdaf29e8809d776a8f8b9ab8c228286fd3317f1fb825694a855952f7a52dd1d59f56ec696579877e08ac7d41109c28996f80326fcdbd16f753e853b34fb1730448b7b8cfc32a01c7a18a1ce957556ef348ccdaa8b3cb4170fee482d0c71892a830f859aad5749459432cdb1ff1b6aabe970de157b24378a6d94bc604ffd84020e4f293f9e5a975355dfc6dbfa5284a57409b239272f0e83eb3b89a7cceb41254bbd2fa15ba1d2535b6aa8c05b2b0c090050948f0ba1edc11fcf2172cb153c861ad784e3e91e73c74dd4a9a15d2dea8e61d676ce7b9a5b2beb74153869e9c96b4876bb7af2a43bd8bf39e110500716c7b78d07d2ebe5db96c59dd8582261a43e7de3d4bc09be7d1e72abf144ec06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b6e2c15e467a8875f5edbf7dfce7b637c4d99d0c021ab897134360c03016cd4f", + "proof": "98e10849644aa8b1f488d584b6b5cd1963ac0c6c41a778ea3eae076d6afd35603657b4f95fbf2eee63c297f0df7f0d004f2e2d49cb79feb9f9d1859b16a0850910ac7395d2c238dd92b96d9060db7dbd6ec2b601b333d596971da1f328b3e279c89ce4cb80298e776446e298cc149798d72e46d99c47a695b841445cf08f8d4dd66770c3068a0f4e1a97d8b8bbc4f441c061135cbd684beae6f367f828bbaa09c22acddcc671121f771b777f98e5bf40dbfcd6eef647f9d66474a3597dd6db0e953df0b38d9b7b5fe003e003170e94a9c58ac04ac3e3e9c0c620127a0596100d0caf609e73b763cd2a408d199462384f8a526cbb6cb826885a992e34df8d966f120f353b017608ac6cf5b365e31a24cb3774d92155edfa172c81819a106e2a1328ba9a33a31e34b672d903034aaae743e803ff18a72deed7c8f9065f7d44db3baa91fb068135b8b5b2e190a365179a361fced29ac29e29d459549768eaffd73c1638f53e2ad9c9120b053309024e2d35be92dc33ff517234c30eab6e32546c4c505479389ee4ef8a0f38fc9ba3b2ca96009bd7457aa9e7d43ea45ac002fabb49de223eedc354e88efd7830ec4415b812f93068adadef031a1283b7c2017ee75d722ea043053539018d5fa2b8711b36fbe24d5fe866e263d69ccdcf0356930f7e7cd2dc2325ad01c2451502a1a8260eef1f6353ee73cfaaf4dcc04270adc726537887f65b3b2601165fa8639eda4dbbc597bd8460257fd21e041dbb4849b91a770c6beb723475f563fa2407c3dbf71ec23f3ad91baf678c586f471f54e8320e798400e6ff7daf8e9655cf88e2dde677e0a8c158c2ef94878f5ebbbcafc16864560163d13c830a919c0827e2766da28db8111d90c49ddfc578b94c79018af71302aed96b610726085d337c3125b3a37502bbd0f57ee92e5b048caba26ffa183e09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bc9807c078adaa274bbc24a5acde6f09793d78b5f7c3ebbecb064430aa691058", + "proof": "0a3449c2f6423392bdc884e869f3d9dd8b5e13925738cb93a2beb110ecf4375e6aeb15431d6717ddc865a539f7573a3f1ff53975db3f377a501528bdbfb5627666df682ab7f7c89528e6bad08e255b4adccd83ab9f7f8e48cd683f91c490f01d20231afb6b75f4a01ea7aef8f50b571aab6b97f51c4776d8c92c945bd6229b7d5eb896910350cb9cbc906b69278ef9a800b88f2062fce9e974aafb6e2bbdaa0cad57e6786e662a30b20de363343a3bfd4fc9a53f437662ac7ed58c5bb9f74706f87c4e3d656f39037fc8e8341b3f16097a15796235e991c6303f02906f9e5304ba0a8c5bb523f788b6e442e905b6e99d641e31d831ba29b247fa5f9cd29bed095aa07f5ee8598ab8c08d17fefebbabd52e06cc79a140494e3d891b05a8387401820ef2d269ac94eb048b2bee92016d1a1a9535dd90a3f92a6042acb1e795c545767c3eb76ef86fe15a7e6e7543861aaafbf87b3fcba3029b495cd5ee3a5168643a93cc7786ae685a2f347bb47a04a93c14343e2e1d7bbcf7e7de83e3fd51ed56e8648fadc14aa4518d7064c2a4384f6bb45d9a343af22f306957bd9ea14653098afa6ece977d5f4a0884b4ec5d1d7edae5ee0460bf861c7a63609205b9fa6a21889de13db02f55a280ab59dacc7e5d412bafb0b0f2b5e96f9d9118e7358f293f7cc82953b873c5fbe9e4a524137012126367de7f28d5934fc79cb6f21ba015448ee1e3ffff527ba7ef8699a6e6f1d4293cc07b345804c4e594713da05979dc260c7f8cee060d9e8c7533332b14d4cbefee821dffb2eda633339bc14cad95eb4ec2b2598717b0d2818815e1a3b95e71ecf8cd340629ecf3702f7d8075f20f677df611119dd2e808351cdf72adb615eafa31e613ac876c4bb89baa03f8798bcd0093991c20f76fa7b50e910191cde2c7ce53d7c26eb4478420ad96a931f4187a00" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cef1924c7b7c7e771837bf44c846f210a0098bdb99c30fc6076d994f88bca067", + "proof": "101cd0960d4843bc55068b3957b25564a474e33d6729dd830f5683e7a7be7240c6c228c1b3262a469fa124fa3f01acee84f2d90ada3f7c15755d8189f39e066ef865bfc5671a168db0d96c70064763ed0e6c5b42e3983b0aaddd4c1da9dda322daefced631a0907493861add08ce710058f3d4b77fd553fbb6ef7e3cf4009274185b6be2a245a31d30da7a3acd305e3f1d26926b01e0f747dc59981b7ee54a015fab212d25c6b820c176d2d93fbb36aea8615e58ae06178c74b486c21ae603087efefd7f72d672d443ed77e89dd52b46d5e815debe78026d596f0f0df25841030e64cc9dec94bee5ffb03aaea9c0872a07418fca6d2abb315b8c1a8bc92ed20efecdc3ba945f748e333b1a39a278fccf66c794d2fc76d6d1c25c4de7cdde9e063249cfbd5a9c74ef613c025faf90856647a56eea02f654b86de9fc44f0f8f859f053517f0f18fef7732aac53bf38fc9b81ff342704e0fc21c09d7ed7e108fb3cec8b861902b4793eb0992543e41df8e1e71cf015026d4c0f76d6302db19db43ba8647a1a2396f3d2323d67dcc56b708fb2eb82fff23af46dfd75b55f379d9e2482e25fb9afa5eec9a97853554729068cbb49c4aa00fd94da6d0d711ece466906c0cf5decee85e0d96794cee1caafa86274d78380ab0ba3c7f0e86cc05eb73d49e6e6360dd9e0bd4cb8233ba5d00708034c9aeec3898e3eb81e19b64fa870fe611c791e75fb6866f54eeecbb1da685aa80fa984ee6143bae95a4d4e522f4cf5313c82fcfc618d61a3c6f0bf1334e99cd391216e019e8acab748d5d4e700d01314a8d672929d474a5d36b1465561fc1accc1624370c6572e222cf0206722b69c631ed2896794fb379d82b5044d0d1f3ab7b01574edeb2bb343c263ab3c0824200a76caba13b70bfc916bdd9f099094edc39f3f663ef90fc4f5a42716c0530c3907" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 46 + }, + "commitment": "9a0770bcd3d744790180c871c53ddf02d195920fb147f43ea304f8879a3eb965", + "proof": "c48fe15c7a907b1c180505d60399e4750222725a61fb8566ef2415c2c8900d73469fde2520ecb16cda2874ee347c0f67f1c9068e2678c72548cf4257c088d27bc6947d52a01950442c839781ee941f70fa066fdb85fa3dbc45eecff6c8b3a259605c68e1206ca223e7b313485435a3a398d170f7d7ccef0e346dca6935f47b5a5b2d7f35538c796d175a3077155188b19c2963b358b91707664dbceb8932e009c919052a13ee617560e9053c4581d20e1738c90b3d9c1cba5fcad7ed4fac790fd55b5a6db3dab4f76a080f9c800deea054f7095c8afd0753ad26459842ea950dbc806b1273960a95cdf816ca368da576324db19985ad236df8ec6f8c8d73082ec6cdbe0400889e63e1245e6080c38d7f016ce51ae10e59813e627353387d6a7048b1b363094161a9000dff609ea828bf89d6f2b2902ede366b4f59836e375950e41e13de6069abb27b18ffc6ff3cd3fb73a36a64b1cda052a2bf81f39837b350aa29eecaf2430cd51208111b7fbd536405c00d68ffa02f62fa7b5d80d84af05a64f1dd53fceeba8c7f3d32663861e7bcab52b155b777b37583c7d7724cfdb54c700ee6dfa80393ed21d49aca368e2fab224118cee152b5b45088fe3fe83b975a826ad9a87ef959193f367e20c2172b4fa0d5068d257d393ee36af9d85f2c2613ae55fe81fe3fcfaedd3631f81e3d911878f19eeb271f1b1c9dc273575e59f14e4ef757177fc4e7a52341adf1397ff06169a5065bca056182bd8de335d3cbfc36761f6d4b400b62aaa8981c86a1b6ac526c9e627ba6e5b4c6dd1ab6b0e4b09a67fe660abb11ec2383c4986d0159c55aa6f1681277edb265b0426d5960bc8b2a43d54a981fdf79662729e7c86fe683ce5eda3aecd7397f99e5352a01f86f4e3503671d4b4a7effd9344727ae46fd8a33610339f0c975b20fed6913178db564610b" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1c0c849501b20e6334bf0570d02dd0f0ee34ba7d521c7c622b44c6972f28c82f", + "excess_sig": { + "public_nonce": "5a4c49bed6c12da0a56b4a67e44493ff9e0b2c067a6946ed505cb4069dd2e421", + "signature": "11a2ca4c78261b0bee62b6023c2cd9d9abe1b44680e23e6b6384e72967044d05" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "201e8c7323bb192eedad2fd4a1e7b4991033b426640a10dee815b90f91890631", + "excess_sig": { + "public_nonce": "22f9f16189ce185f8556564d5301a3510b874fcbaeb90336e7601100cfa5487e", + "signature": "06090b8be302043897a975a42c0c5e203793df31e2a56cbba8ffd5ed0da5e201" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "6a5e23c574502949bdd614ceb0b3ca5aca35f19145517bbaf387827e7979124c", + "excess_sig": { + "public_nonce": "022e49541cca20c3aa7f4e4d352c99122d7ec01ae3fc437eb3c6692ee041d222", + "signature": "8161df57e9bb5ff27b4c36c1a9330ad156aad7f120b6c05f41e7b35daf95da04" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7ea3ca73bf0d5e03c170022992f8320c0b152ab117dc3b17e88f3c724d661e79", + "excess_sig": { + "public_nonce": "5ea2b672eadbb40c8cf9e17a426b2f0e9571070995cedc82b5ad3d166aa75879", + "signature": "c75a08e02297debfc665659964426d018a7e32156dcbf23f66a05f9fa592e00b" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "9c4f95c017c2d678690f444c34d60b5c7dacb6d7a38b32f6bde12398ae55f37d", + "excess_sig": { + "public_nonce": "cadd033be879ce181f461067b8e71050cdad3852752fd25cf3a5e3c4ec28a636", + "signature": "41dd9369f0d05ca41c80bfe2cfcc5aadba9e01dd5e1831d219cb631c79fdde03" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "0c16ab76466ba8c404bd154978b5b045f8382a72c65a605e29514e068493bd2c", + "excess_sig": { + "public_nonce": "067ee1b50ac4b81695d96ed1f7964c858bd0932a76f970e93145715538a24275", + "signature": "96bd4a6110c2c5e744b7777bdcb5c0a37ae1eff55798369cce956a544f36c602" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 46, + "prev_hash": "00cd1f42f1db8c8b46417876b7152b2b9cdf77f564f07dadab2c7f86ffd418c2", + "timestamp": "2000-01-01T01:47:01Z", + "output_mr": "ca6e7b1ed3cbf26c7bb1bcc98e0e56793c15a20674ea2831486fc2cefd0e2532", + "range_proof_mr": "a0905ed2e6684c976459d5276caff02ac4c68f3d4f24ed699f7ac8406ab6a72f", + "kernel_mr": "15b2da6ccf921c7dda00b8c2fd57b922da9fc6c625416ede8fcda21e57155ba6", + "total_kernel_offset": "abd86af4b3469845c40757e2ed8fbefd8f9dcf6ae50e313fcc9041421d139408", + "pow": { + "work": 46 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7cc4f8337e4abf0661f28016426192d7d5dbb00d79f67a6da7f5711f9838f60c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9e6c74413b7594e8a61d915ae6739727c4a7ef733c5c6f93f4fbbba3b6b65849" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "a27d9572fe1829d8d56d341fb4bca4fed301d7001491232c2c165fcf14405951" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "bc9807c078adaa274bbc24a5acde6f09793d78b5f7c3ebbecb064430aa691058" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cef1924c7b7c7e771837bf44c846f210a0098bdb99c30fc6076d994f88bca067" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "26c34007da48149a9108affe534a0468b128bb97ee60e66be22ba01a8d079052", + "proof": "6e64500a154da8e05a9777a4b14667d5fe857247953ee7fbd299735f0ca7356006409b58fb724de1df1b1e4f990357da533195c990065d456e53380bdb1a104eb484b49779061ac5c900bf3bf00e012527eee1ee22e8dd3c90f78cb990bec10910bb62c84496a8b9363771f385a89aeaac8edc12fa34d56fce4c724f42934e2f9cbaa2bd51b5ee7c1a6755c1e496632ac7a026998d17176d5324c9160174e1027d8f2daa5ea0001be1b38ad2bfe7944459ca8598ff6083c5fc8fd30534ce5f094aa067c6af2406247371dab08b5bd2fe8fa5eac9141f2bd6e79d4cab9140400d325f662039eca424adc91155758ad662beaca7658570e730494098a715ad406288198dae6360f0044235752c895c48711d601f0414384ad47cb95ba57975325392939b2a05f4a6bcba8149422583c0747bd42af2483338a4e4ce86343b11ed7cae8db55e0fb1813a3f0c8c54c88ca22ca1a54bb6fb14d1f1ab783ef0e8587f4dc48bb06e4bc386ae1eefb2e3d9af560881d8b7bfb9a17e56b0400bedaab12d168461324a726fa5776d089737614308836fb2d86238aafe2a48b8d1b8f5aede7ed69e2f46406813944e52f2f0790c4ba3d56d9c3f240c74bbdff40f173680ce387cf9d744a7892f6bf5155a96b53f8f85670ab146ea2edbcfc5084eb0effb4e0b6ae565380b8a9e623c29c5ac5dc951a93bf3db1ee40e73c40a69f5b5e6f9335ee21fd70b6373d4c988d1b66bd34a0ab47b7da533ac3410542cf64613a8db7d19180a94b297c9ff40e5ad86d0ee6bc4ed4f836f4a4fde8fdf23d719eb40fde403687e6bc156081ac2311de1fed3959d70960b5f2a0a2e731d0885a6b9331c1e170a318b4970e7480f4a922110bcd651f1f1e55c22b29dc9380a98ec31e0996008db5eb473c836969004c8f46ddaefe377506f7fa21e427f13e3a04d8c6c7ca601" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6e5a92e29025984f284f627d064a8c7c815471ea399c870a8f69b59cd4f98265", + "proof": "e218e25adeb3e5c11f68eff9502a84d2e7808d0442ad334b0eadbfa757807664a21f637111ca5c0cc36efddf6d0a6b012aebd5c8f6c6e69a4fc8cab6250f3339868634b6982c9acddcb09a2bf9c2cac0b445612a5fd449e01f7a2dd2a9b1776eee976e28014d1fa77794e340ebb5e31bc5c902443b1213505df12785a0ce56499971c09aed0174f2a7bdad982fcdc86af59704133d327fc24254263fc4d27308115888a8a88c161f93c2522c89524ebc75e374867226a609293f0fd753a9ec07ce52019b9c2b9dd13b3907f6f39a14f875178cc5c3e6e84005db23727ca75a0ef67cc37b6ca1b4243395d2df144ab3c040a94b2bcb3884ea2f8a9761de5c2618606c6ade62520df8f09be5a479739ec4a0adb1f6d61244ae59128214b8ec5a58eec70e6c41b0de4270cb2c0dd5946f7b5ce0b317c690b71a26329d20c476f51ad4bd21c135e47a3e7f7e045c73e2fa9ec103e9ad3768627b7ea63f31a5d6e024a872f7f86724cb27f1c76c7fa363056d62f96ed795a6a43f04da8dbdda8b3362ec4eff2ced47ce82cb0b0c6f3b39e0f607f4aa6397a511705a8ca1977f825a1a7c8dba60a32a21ac4117753d90579434e2616aa793c88dc277092fd00a17ca0f108dfddfc9764d49ab0fff0eded84024fd3ea32d6c3d299fd96f4a993d0f706030333cca9eb581afe722e1a335b78227f30398400fb7998e8bbd59ef88936b2db4c0aa25baee77b3856883763a3f2d56d7d70bf3a99b1e63110ae38bcf1453006e9223f8f41d01c98766d6fa58ab7165e308843d878d12988625c70e1dadbf2cea7c15a4a9f799fc6bedf20e74adfaed9521588b68c42d9becf7ccc290eeac246972fe8294d53c9b8d4fec35c574f24a736faec582d8e8d7851e28cc7934670f52ad28ac486a658532288542dac887160796c52f88a5bf4e2f1a61d8d835520b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "82d13c5c8dd7faf0ec1f7e8b78a50384ef0424f5d37bbd5ad8461401ca3e566c", + "proof": "20d5de9363daa53a7589304e453aeb3ba88a86b17905aa6c41933044b0211849fa11deadc0ccd3baa358343e1db380229a86fc3ebdeee9f3dbf22c4cc1eaa94328a60ebbda9463d608cbd3875bedb747b52010d48e9e0172116ed178d601795cd03b65c6b2f7abaf1822cab50a60180f8b44c21fee5aa6d4dd8699897c710977b0e965a0b1e9a70be09586ff3ffb0c9159d27751b8c3eab6b2a91a948cb1f3053d910d2c11011f3d4ff9396672220c5c899c39f516b6e09cb8a713d66280af0b28dd1c339a933f9e51bcb68abe15ef151be045fa66749923a059d1e1a074ef03762fc23720ada30d3cec057b5a33c644430274df37c0de3a0d54d876cd4062052c1954418a9f7ee702a9f824e8909e8569c8aebe1edef397866111a137a5f312be33378f8317fbc83884e82a6c0c2fc6e8a2df8e0529247cde47f427c0e4023e8825ab4c111434ea7b9d14eeea614b1f6c7097a28919a997a850c649b911162528397fb68cd850594bcac30186c961da9c612fe5f08161ea7eaeb52a312f5e51023e059def3089f3cc3b92e6fcc09aa6d8ade795146b49c9c70401048c0b6d7fb63158c8a508af7a4e582ffc82af29cc0bb2a81373aa6b762856339185c4496dce972383cb7a781cc937bebf16fc55340861cd188c7ccc4c97b703ee0eb9f1512a2a0d46212845253325069ccb0e6cdd3662c526494150910b32ff9057e4e25ae0a1bc50264b79edfb4962de3f1a8bf63fc345984a97b545b951e7bcb56f8905321d06cdfc52f1660b2e87a1564c72f7514f127f0fa84d6ce0e280eb41c52737bc620a2bfdbbdb0784d1985e78de3c9fc721b5a09c72de25f771363e21707d1441a63f4e0bc58e871c3916512a5988bbc3e6a6df01d3be3496e9a460fda56702e0b068c90615482a88ef6b6fa271271c51e465946012802d7d127dc3fd26210d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "96ab2db982a30a5cd42e4078c84ecc4202e013b9a02270683dff84db0ff1e84e", + "proof": "2e0f6590c96ea30112c8ab31e3e9486618ba698e30bc957820e4ed03ed63b950621f6aebe8f76de191e4af42e6db8502916aa92646885111f1f260fc210a226614a4d8a02ea6a229f6059fd4bc52cbf7ebbc7a4ed06a6c92efd618afbe7fb33de438c7e6a65e4a2000a407f50ab33c446b06a79196a6fbb6a5a05316fe0db76fe24b4e6853fb3148b46f2f40ef78b30e6b61c6fe1dbeac3b7b2d31e2acc50202020af7d179dc03d6247ed6c672dfa36858d68bf9e2c3ae95280c20c3d9f6b702f64499340c4b3371be63faffeed4646c02b057e43c614d744db467f916ff890e880c33a0b487194314988b6fd72588c92cde774e1e42d9d3949bfc3d19d1372b301c13f96ff268b242eed8c25d71cabdb74a816d34d36c9271d048abd1ecd736287aba1b836ccb68d3f166a125fc14fdc0420b235d9979212a6cc61c4764f718726fe36322f63a1523aaa208caeba6b195f5f6205ec870ff24628e351955ca16a00e08e3b085e3ffe4243b1da4f30bb1d6e3806aa9adac8728652b229501b40106e0909ba7c56e7bde2c204f6add33d321d9ede754c1cf63cf3a52b5a96c54729a19087e063d7bf681b67b689b2e9256cb61dcee69490ed77170a9070035dd77f2ff76625a3ee3b079fffb5e7fde1c6f7d04eccec7afa901fdb7966946fc9e3d4a265a0fd4b043e4c8c17b9a9424b0399f07bd4e46529157b5c60cb972ffdc5e10ec61c10be4522de81a1d178c446771e64a5cf9026ac81cb6720520b087041964ff0482dbf40c98bc6b87e846855ce6f2e6c68d1f1add0f19e2aa5514b598718a919cf40b28c554864d6e9368cbcae6cc3347aff84d51bc7041f3523b5ad03eaf2a7230126f67d1f26d2c9c821f043dc78209b4d80dc4c969831e5c97d01a0f929a0b3b9e2a46cae7c390b5abf2590254c2398561531ce740bf3abb1bf2ba03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "9cc42b2a0522eab6db49fae1b4b3169f01d5ac69cba479928f58d4afe283bb3d", + "proof": "147079e616e8a9c0ed555a0aedab382164c48b7fc4c4e1c2f618b14a31a8bb3606f199858035403284e64806eb296dbb5e7631e38872e6530f2c7c2af3351875d4fbd66be2cb78a40edf6d7d6b21111360dc18d790bf470564e39e86d9996c773cfe1dce153be9165b83cbc4dc61e66fe068d6d7a16b42682d323e8c62627624038be4d0e261ca1d9052a6d834e2bf9fca84764de6b2130cf246f8ecdeba2e085b73b89ed8deb2cca536ecd826cc71cd30f11f6f2b56645669297e820b74d104dbe7781fb0153714b9cb26c7f82e1b8351d388d8e093b9a4e77e405a9a190404c407833c75321c904d48bb6bd1301df9a3c5b0bc224e072389e3067543e5e4225034c89183b3b30c7b28036b796ccff7d6eb163a6cc01f56742eb7b363bcb361060d5018e141ecabcfecb38b0ec81c4c1c0c34f97692802666048ae8897fc8267eb211c4021c9a2b1eed741a7b4c84102209e81be08d441900eb6b65cf8f3374b26bb881cc6d666a87437baa7452f526f4b722cc51bffdc0ca9bfa95b7e99a65000ea0d2ef68ba3d746f3da2aa1432471c843b53b5aa6945d5b9a00c9a9083403ed515eae96bc0ee37b077c9bc14151c0aaf7a82623b6dabbc5a1f4dd527b87b3638a9f804bdae6fe58c3b07c57aaad916c6f6c04b18691d4455966784d8f53e1022a765e6ed869dd7157c1e6f62e1ca3da0a4948c285de301e180bc17df570e34d11df65baf6fdcdfe6171405b0ea8c450d0e2a233bb92b307e41f1fb4d7e5378b1de3e5c8c3d2dc9e7953c0f202d3b11fc8795d16b668de24156482298b6183cb20046aae1d74baf5025e696e1db7996a24154ebc022773fb59044b6184518100b1a7dc0cb0c684954a989bbcd118d0cf75f59483443555d4aadd7bfc7540559aee74cbd072b2af6fd4d8138ac403b6345b92d10ef46e034c26c03a27e730f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b2970c3b140728aea806bfee52f644cb812e2f8ba657a656aeca5bc189917601", + "proof": "8017a7cdca1ffd311e8875528741b3ef43200e3c36270f581d18dc842ee07700160f0879529f09103f4b182327798584a81d4290db78768ab7682344ba44967aae9c857efa53a6fa5a7d80ed5b015782c7745292638c835a9805c3a07e2238673864719bbb2ad3895120eb64bc6667e7e93b309780cfb2a9629a2b5d0525f000f673821971985ad1ddca687700c0fab5f1aeb4ace0c28f1b020a6dc6cdb1e50e736533e4865acf361ff8ac527faf6166dc2db1e66ff19f115df68f7413d1580f2fe67cc5cc0b32c4b633553a779e96c18e209233a052d0b80ceb185c750c1004209b65d6de94d0c26965e10b907bb9abec1f4b9e49420f1c0eb6ca6662e99049d0e765ee549debb5addb031ab896a3914284da13b8ac50e252018cc8e6613228e2dae0d83981f7e1ee21c6bd3eb16d6dfdc40a07ae1efe7c943df8a4a300c15e90259310bc62a40857d41fdb40275a64f5b90a946c5bfb232b3c8ad086a4ca75061ff32717ac6e6084c83290da6556b18eb65e9beb013db6147fb318a5e78b453cb84aff8e23b4f1cc7dcd29f440ce7eeac53985c44c34270d98fa354ace575f4099b0444e73f71a3ec135f911ea3d497e6e5d5e27039d644461aaff74da5141e850aeb704cef1289d193b57a6a1bb5db19885c87fca5afca06bdc37b36f691b2edc065d95be1987be429af452de28f67b63c0f9583a121abbd547f94f814c61d23a8e2b09fffa5289057e83c29e104d9267dc4ee2b9f35af223d12207c0bc696853789c6b183e04d77af7cce775c3ec694b634e469ae0ef03f16d451351db775c06157f8e2d2f6ace950a8d41a2741a6c6b253431c62238e15ddd640bd4807532b5b49a37698e2992478f0916850c225ca211373060834de3d198b415ebdd0188b1d3a489a142d3964306c078fd2575105020bbec8c1c7d36e99e02e748ed06" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "dc71ec373ac098d4df9d9c03bc70a2444b150d035674e40a5039a078f6a82356", + "proof": "ea03295aa69f4688a6a29458cbb61725521c9509a222bff46a89678b30d93222283e97bfb398c71c6c28d7a842b2d633b16398b1855807f8e4ae89310fce8d60422f65c8ab2ef8e536c2dd3b937e4f4e4d3cc569807debcfd5be83c78e006276bcb718fa85653b383ff21a3388496247ff8adac413f0871cd7fd075f2a9b435dcce9e64f584b414a7fc381f261e0187666c56665259c36a9063633eed6dde80e5f260dce38a006a36e0c6b3b33eead6f0b36150ab760af15f815a69e4279810502d828bf17bc3b097ba3feafdf9488ef29b313fd94e0e88d71a2efb99526ce08eecca2e5f8ccf68619065d0a384a0af1dc0a4a31ef7980b806a6cbca3682861f1efcbfadd6b143d8d8e15c9e5e7d216b01112617ad2cb623abfafc795fbe916e10b0d2cb8405fba5b2700ad6d68314ec6ad22b18eb49e30ee3e59fa17ff76a439c1dbf3ff6ca1da6ea66065fa768a33de3e99f9d4a450738cef8de53e465665bb27a8feca6f0c9b280d0361a2aee5bbe822efc39a34c99e39edcbf2854b2e5297adf0ce551f780109f29af3a92c870b68d8befd5af576525b72b2112e0f12e6d8e8bcf21fc8b64746f84eec2fcef0eb338293884da48c72f7d95c4da85a3f1219ad78ef8bfd6f07985e296ea55a4f954acb524d83faa9a5f7410677267b5613d8a3054c0c8d633487a654b1051ec22d73c69a318399a4da2702c1d031384716a02774ad13a2db70b3976ee5cb280b96b21f06dd76883e9c8735c13d4d23889010293e25ba48a18e242833ab65934d93ab9c08ee6edb72cb39e82051c91c51839b0e18e0a68e17c5a82dadc664b105098b4910082c15235f15cd85395b95fbd3e847b4186711fe5bfd0c37fa4ffeb2913d2bd2fdb10a5aacbeb667f92a24e65027eff7e119405dc87c90396510e9be3cb25685556905f6f0d3bb7c830e174880d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f6f0fcea9108a1e395e19552f86445258dcb27db7aa10346d520c378d823cc3f", + "proof": "1072aee2f2c7d7f5b7840a6fad7a56f40b8c570038e1b6c5eff2544f8e97554742901af52e7a6fb35def137a98260271b17ad4c9cde07116ec2ee7eb9d9aeb7aae47517248d581ca695095f224accd09fc5a324730706e303f25bfbfdc8c1d3bd8a4a39557dee5ac2959fe35537fed01d104c2b6d2d7c06ee338d667d5eba316e408115a2225b0d4e40e90563da7bfca9eced9b9be17ff0fa60d7a26cb1b5c02f364074c47e54ca76eca5b2f8b6c46ee9199f04434075451e0b231da54f7040fb18729070c6405f20cc09eef6501296635ae8fa25083bfbcea01e5ec9ff1f60dd62ef572b555675fb80009c7c145a7ac5e3c5d8fad20b1994f191890c485f544ae29709bb8f1c32bff0120b70b1e9260a7b896315b46ed87870a4883ac7ab61e5e11c0849bb38c66f7b0f12060944e0cd28c53be4526c520a8db722801c1790ab44dbccab3e2d91bddad4365bddd27206b57ccf7b4b7c571dac80dab7fcab764427e3107f12765571b5bd149008d4d0ce2977e6117b235a2e3c90251d97991042077d8ade8756d19787d70a4057f5276007b8f510ffde3656287d92cf82d0f7fa0eb157c7c5774de5d6959062a5da5fd52511cc88a8787b5351896bd1672257994edadab1978e5834c10743395141277124b0c464eb20a812aa6f4ed7b6aa3309e0ef37e99e15245d22da56a2e82ab010c4a44864b6ef40f09b955fb09c3156a0497b2962321bc53fa75f8149ac6c2652e649decad98d07eef2b916dd50773279eea473cef2373d9e34cd0f6bc039187275d1807d620b7a722a53df9ba81e545ccf57de8861e0b8b75d4f4ffcea1afc8314c879706444ae100b021616b213d2fdee96b1b1d421f479f7c969e61ddf7fcd0c7526f7be7df113366ccd391e68b02a1c4c26664a42aa7e433030aace3664d7ea6686e19034e5fb58dbc5a56f1f309" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f8f7bc373250fbe934be56c084e6792e53fb0eb1a6eeed2adc3a8d9957a74545", + "proof": "5e3871ed697bd99fdd793cd548928ccbf4f6f14122a2a0d670a8bd6ca2cb8f42f47e78201f28545970601c842c8c2e992a6f72d9e783819946712713f943732366bdeb38712a846aa1e246c3e52e5a3efab2d5cf9385477eef8df703c1888d69f038a102b764687e8453436a5bcdfa6b826f7cfc02226ea9c70717f0185d5a113e9e121f737a33074631d63b900f334694af85d9a68c6586d771f442aef8390df26bebdeb5bb07f475c46976d893aad48cfaa3fb89f07cdd06ceaa9c7f829a0817fdd89a89576b5435e6ca048a027466248d4b8cf6f23df0c89661da2b5dc90a243a2642eda220195f620c5ce99b98063b11dd0a4f62b0a127c48194ff04864cb886edc6c753caf5befe16204e0ef55c46fbb08c0355622372a6c11b4044067a2cc33cc14b69ce023ef98ada058d95a309f231508c42d286309768840b7eff625271da8efe868816b6d3cfb61fd563a622095c163969b4a8b3f5d805954a9c3f3e78285552b0f23f19b0e2b03652582317065397697c6638f947280afb4f252dec025c56a94554bfd78e5de01148ae1cea3a6aece877802798657b56cc1e6b41cc71031e645b4905cc69186a4968116a3707bbb2991e9f2a2552176786402c7680943c4ea19258829d0c5c1cd51dbe8d3bdc5f239b644d1f97422c487dcd903ab095f83a0d435fb91abbbf42dc67902aca085322556ce4245d38407384cff51690e2682f6a912e3d3bcf6ac898b84ec8ae1525dd65cedbb8e5a255191b46cc50e2cd7b7ee46d3e1a976cda00d4cfab0a112b0a0aebc43230742dba0709764806f4eccaa61b0c48f0b78df47ce39a88c2e3bf10445bd19fc4e0e9e3dad056166c321ff7150c2365250fe9025a85da501723b855be064eb37cbf14668340b2e70a2c2c49aa2c2e3d32f4eccea548c7b088b2f2c305544ad85fb420c146a7119c0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fe8b57cc3080bdc9df4aec7c02e296e6c4734010f2416a5786f797d2e7db852c", + "proof": "40c6e23d1b099ad4126b6332808367601e923ebf8ea00decf38f13073119882f34119a074ba0092b5531ad6bcdb6110fdd85c90dbb40ac7eb0c00d1aae7114357cad93a25b2406cb14b6dcd8ce31245cbeaea2822893a5ad09c950c9c48ef045ce625d3a5cd629444d6a44c0814e889197b43609083965e3338cea18f5df2e75ae44193ce7b01be35d9c20823985b158bac0be3f3f66c95243db7d20921c1700d600b8d6995cb27fcf54dcf961ed25987c87ab691f97f339684f8016244ca20282f60f5cdb3d968e7198616d951f355693949e9fb4d27af6ad1d5d7f1cebd706cac19905d4226b0d0afc898a39ababc958427e7bfe8a44c3f02088ee3b76895ca6269ace3255142337ec7fa86583f8f77f4ffd8fe2551096647cce38dddca41af63fd5fb51f615f788569bc31765e1c44c5527630c380e0a2a1c26d8c788275fbc5e0b52a5887de1d1324f9cb171a0df418e16b3ddc3ec9b19ca9c598133d15ec281dc9e104fc38d3b6a980d2a3d141b09b2db0f3e50de101c739d32a7991348d609857bf5bb34c5958158a13bbf3c28d14306548b854f2f246763af651da04bf2171d9ae98b3464f7484b50ae695a17e67b221f0fc998ce680d8b0051356a3f4854a036351bcf9d8b63878977b0386650492113428039bb9b55dd97e02c8e3bdac274be70b94f86d0939d2244a012191836d683d894c6ef0349f85bea19b005868b0cc32441867c8623b4a394f68e54a8a367d1cfc2b8bd405421aab7050f0bcc59d372f2bba3e1fa019a27b9f824892cd79842f132c5903cd3c64da27d951df481c6817f542d85aaf7a13d4241bb6dfa7e2ad23ace457d3a8f512ca918c653268cafdee6e8f09872479c645a243934dc59aa49e103b561ad8ac4483e0ccb0f0dce6410116389faba0b4536d325c04a0f8cf738c9bbace0a96b0541cc3efb0f" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 47 + }, + "commitment": "1255acba2103ff4e9c31023e32a7a04369b20837678bd1c660de3a79d4c23a32", + "proof": "5a7ac0d90574b748f95e3a71a32f4a16f89b2f238f4f804e2299e5867ffe4e5904958aed42c3f350ad5c6a458987528886a84b77e501b41de7364f6ea96a6506de9a09c15f5ccf01a0f25555705390cca6934f1950e261eca9c4f6282af81063da706b02af01e6b88397d8222525b493f1235f453015d6e8a4905ed808d70869316d0c14678d1d1c3dad0e296f4a8dae6e3c048e34dd5993152b2c39c8e24d02ef2fd72141f31cc105ccfa05630634b111bf134eba9990250d48ced01135a5046bd0419fe16a00e3348973691484e3f23ed1bf8c79a23f697277a9937b034c03622889a15a4d2258837087cbd5124f3d72801a1269fe2bc75538bfd8561009034caef10ff59ece3faed39910cbefc5d29b0629a30580cbfaff07265dd1d17043760826771f8916e18cb18c3fa6aca066540dcc54c18baa55fcd4b97c343a257818f75379c91ebe5e01d6ec0c69f05eed27e47b17e5107ba728607490f1cc480dd6e4fbfcc76484e283c4b8ee0807b3030da87736df10f53e8994aa45a4cfc809c0d826441161aa14b75454d758db75486270edd66cabddfd5dbe469c100d783b16546ffbb944accfdd011141a67cd2a11e9d7d58a2a19c0e8919bbcb31de7441649629d0a4cd24fd0fbbaa649b784fe79e047b06245c60ae8070fce0ca29f8135e771b5c84115104585c5d0d8292fd8bd81f7f37397d6bad5835914afec5ec3254ca9fe9be388a1d8a1e2c5a54dc95e4a6b5c7cb091b88a1f168d0b11816b232322fe6b9045ca78405abcc5d0ac92916bc9a62c924580e12f11d86258bdc575b1857eb724651003d3d13ec4633b14e1ec874c0fb15e51565995ce3928d3103177009aecdb664a2c09b2dba1e6324b950e46e4316f08640eb8243630972948806709e27b203bd4292a67c26aee94e4e3e85fdf664e4233fb089b1d1b048fb6206" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1429e1a133ef3b7df9f3bad8585c85d6a949a24a9fed1c51fa94c3503edf1836", + "excess_sig": { + "public_nonce": "401703c44199dc5371669aee02ec9638deef404620b2deb5c6b66708d75c337a", + "signature": "b5730bd4ee953c8ecb4f154372e221b35120e9aaa5dee86038619a23fcd67301" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "20c6cf7a0f352539b2c03396761286c4895edfac0127ea80424e44baf19da208", + "excess_sig": { + "public_nonce": "6c8d139645c1971b27241418260e6bc152879f20b1399915bb0800f25273934d", + "signature": "13e3ee46d9b0dab9d5b796e25dfdc13c8b9c3746ac2daeff596635a05f33e108" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "46f459d4749a488e59196f714257b9619cf24ecba85cd82e6ab26cb0012c4262", + "excess_sig": { + "public_nonce": "eea9e21e993747a8c260043c2e0729b25445b543c34bec80cb4a69ba6cceed56", + "signature": "61151930152dd9b8aba043247f665ec7a43a36bea71dd6035754ff2352643a08" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "668347c10302420570d37e406a8afcd3c055fd305d1971770b04f7699a1c6039", + "excess_sig": { + "public_nonce": "de9848370d3efe397627814b021744aa08e3cb46f44a8851cab2264e7c1f5e6a", + "signature": "f07fbd81f7774d9f3e9924e2708aab2a001c937a32ee0b2c16eaf35faf7b3901" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "d2133ed6ab923ce31d78d1dac3d0aafa2272e1b8724dca43aff7287ea90f5e47", + "excess_sig": { + "public_nonce": "f4393107dca2d9d6795268796e627da43925b6ef6643284e3be8a2039f9a7e18", + "signature": "bb5b448abd55399290d568299073f878b83c31c376b4b6e964f92576dfd26b09" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "f49f6a18dece8e058a10614daf93040da9b0220304cda5f2fba347ba4ef11d6c", + "excess_sig": { + "public_nonce": "c83d1a477c40b003b939b6a5e713096fb738167eb7e4116af1b52ba218cb8f31", + "signature": "de3ad74b3362804ef33d52da5a886ab2011d654692f267a75ebfdbde610b960b" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 47, + "prev_hash": "bf9b4f35fac6d78482d4233cf7d7376cb1073aed3f4255e2c49a31fd110b729a", + "timestamp": "2000-01-01T01:48:01Z", + "output_mr": "7961b92954f289159e15f79a62c61fc0c2eb76fdbd0305ad26e11c585ed928cf", + "range_proof_mr": "878f1790feea107bc0049d096f027cac084c5a611cb8923e977980df9ba9d90d", + "kernel_mr": "03229a17a3a318cc9a20a1eafa660161ac45bbcbc2e1963426efff511e107001", + "total_kernel_offset": "7f010565e72c0cfde3f9d61abed0c2c23aaf844bf7af2c7ae86f64a647b1d405", + "pow": { + "work": 47 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6e5a92e29025984f284f627d064a8c7c815471ea399c870a8f69b59cd4f98265" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "82d13c5c8dd7faf0ec1f7e8b78a50384ef0424f5d37bbd5ad8461401ca3e566c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f6f0fcea9108a1e395e19552f86445258dcb27db7aa10346d520c378d823cc3f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "fe8b57cc3080bdc9df4aec7c02e296e6c4734010f2416a5786f797d2e7db852c" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 24 + }, + "commitment": "f481bf13c5263ddb2b2fd61fa0f884a3c5d646790791ef886d4aacae3d5e7f42" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "12b3398c7ddd8ba3cfd52d9b5b1f98922c9e063e55f3a992e4b8d3e15b067e02", + "proof": "66170b8d47a6e65f11ca4c93023263e47cbe62c2302ed07526ead8f416cedc1a343ef6aec25e1836a76887d58dab59cd123faeadd8a642d39c50fe73f34866196642ed6a3f05280a7ef86f35325c821e367434ccd3c774acae104698a9a59c51e2b65486041c73229a2f182c1b995480ee0037c7b73dbe24891d98163997e25574201a0f532996573088fa36303f6c565d92ecadeb6fe8d30c3abef125ddc70331ddc09426b0301e2aef84dd440b7434aa2224ac7fd6f1c498b47f7d7f7b8705c4e493ab14839c6588d758b96e9def522915b3257ef8fae9c345dfe276d6d207b8c7554838bfdf5c7c0687a5926d7f382032dbf62129e0fcd3d7ce3c93e56b638293afb01e5e98aa7ccf6b65fef457214e86354b1afabce6d84c252b363a89506044d25dc9fe46eec141cfc4d55299ca5c229978878630fc1a8d35111355a05ebccf783cccf3c15b1e4aea75a3c5ef4bb9a0fc649ecea8ba5737113b3afbf07ee8bdd842b07955720d5678bcb69193fcceabc9d2678c132f06b61a60f951fa186ee46a2ae34eb83a50d5004728350a13f74c73706ee01b12efd6ee1d1d977420fec404eb0ece5b22e704ac17306cc14f737f51a217b27e20c3e1e36fe6601d431eb4f2bf04372aa7cc82b789efb14da8db4bcf1532eb2468fd04323c021db259aa9e1bc4a76c7660e3b36e740f2d179d43f97ff7e3cc0325c9e1cb7360621f215c57af28730a7abd4f7d40d7d129330d5c7bcc98b1691f7f14b9dd8e13861342a03fffe5ce36d160eb5fad7abe2ea1a412bcd0428d502a06049ff1c23bee9945f8583935fac745d587cc1e6b37fc64be7c28d4c28b447f3ada12a644f95a972efbe0456c81427e7e30fc946ceee82c26f1cc22d247fb18cf8cc5bcbce79ef20f4924910a10a70925482d0c23926c20c7d38084c65a19af8791bc70b47c89490d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1685fd844f5b6629db0ca74d3d36ef80c484bded9babec29f91a85999340a055", + "proof": "3c2e9e8a00679a284625676c0678fcac96c58cca4809a9fc9286d445dc5256331ab78e5f5ca5d8a1ac7801d0b4227397f894281371424bdf6683e9aa2f5abd09a01884578c2cf6658256f8a3068946fa86bf40c506cc4c6b7ba0394f8c4bfc1770445681bfd439f60fb0c27c54036eaae0b2eebc67fd6ea70cbec8aa682ade5b8d0ce71f3c1f302ceeee75e3b715a6cbc863f906374ceeea9a562ecffff8b30c27315b0f3d2b5f992628e53dc6aee76e3b1438de85c98700a3c2f5e97e4d8d037adbdfb0b1660298e82633621eecdae5c2ca8e4de6570f36aec6c79bb328140f2a798b3a15dcc3319722a75dbaa9a9c699e3190897f72b90af31891f34333138ce7f11048e8ccfd62313e8eee7d84e890b82915bb4c81141518ea613b8213155b470f5b2064814c5116b8898fe38eaf89b2f0b39f6d2383bcb638d85064a53383ceb31425630a28dbfe2e06dd4666e5dd5a90944bafb5e848c9c7606d77c196538b6540b6bb64409533dd90f45fc609f032559d92a4f520ea791e4c6cfb432653e00bda832f3029c456949383f80417fd12ea1867a19bc795b3fff1611e11c00fad278b2a031deb6486d90312a1d65832a168679f291b83dbda022e146081959e4a0b44d5e3f14002fa06b2d67a8350cf4ceed5a6505ea15ce90fdbc0760ed6340c0b85baf05a967e335eb83d46fc37eb9f943b5ffb27da5620847523192132746d2859145af73b16c7cc5b7f67ebd811a432d8da865b581e379261fc0ccd97fde9e5c8bfbeea764354f55ac1dee6ac62ee93db142fb9f30e36951b81763157c185ba7799fe01d0d6005071edce616c21b6f9eb49f43d50fc7ec68dccc414470fba8e319caf579943236d259b83f73801cafe1094a83800632e6ca801eaf3e0b9d342aa2964df2c3146cdb04e134aaa2bf738294562b535d410f6dd490cae90f" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3800b958850c828c556e204605ca05eff4adf778c47ed42fd78918505c05f731", + "proof": "ac6a6987fa53615c23f37054717ec9b8058f5acd583024130061a446edefcf48763e794a283e4d5a73869e41121d7e11e9e63839796319b7bcddf2636b39d16f8a6b09d1946223596e1d524a2873ce08e1052f65b65f7added462cc52a638741240af2199c8aa5c5f3270689e22dd0ee5290d2f5cf94b24afe532f94c54133225787d6a59390db2350175b6758e29b79f177e864d8fe020448321e93f35ce80cc1c9dd594113f393522c09e507835ee847b1315354b10f4aa81df63733b098069d655c2e8e3cf186904f1f1adcb5d3fc7393e086f47c37c99593ba9598d01d0ef8f3985f68a1c036ab328b59ecf572ee48a81aacf55bc486dfb9decf93db4a2c863702b0905d8fd0d58dddd63a3f24fc7c7335e16c32847bacff41606b88cb5c6619a1faddde01bf38e885b965441f43ac71697c7a0e68b3a47883ced10c0677f8f15eb2cb9f5305dcaec82a00f602fd02ee1aa908bd660e0c615daf345a2c49d232ba4489fb01857b739e5d5d644ca3754af9c00c1b9387fc41f30565931971a2f5db753a097efda7e2e8a70d65098b05be3cfb468cc0f1623ea03eb123cc007405edfd6c6fe3ff04e4a20881a332bb60d71ded2991211f5d39f763a2528b411274789e3ddbd32d06ee5dbe42c35a4a3fa1facd1cbe0ee598f7bde63854991634b94c18de4210a0ed03156b1683ed7c58db6a47b8e11780a7a909b7d765a830d82972059e8539ca40aab8194d0c00ad7010c459dbabaac9ac67d5942404f70aa8988386fedc638609f79d862e7b2f2e2f65c4596efbcfc92003f7c03e416c41d04223361a59d2991c324b68e1e4ac7d91f1b4ef69c021e921e791277f477970e27ebb0d9b24a7e36f979e7196d4ee782f14d356a2603ed1227b4cecdf8024004a67cd550113a8c0f809e1e5abb3ac780595f7f2d55fd8bd841c2e21affc920e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3cfaef994e7d400de3bf2eb74952e62c093505571d19f6c93363f73d8169ec33", + "proof": "d040336039db685fd0660a43e3aad4d407b0a290b648ad7e25c21cb9b22101396092e60e85bdda4c759217ccc47a399296ed6f2811faa4ebf24a1b392f339250f2b0a648c6c92cfeac5f28d38d734efe3e07a0fb3ac2ae084f1845321a4fff3d3c5f377abe2133b2092d58159bb51cc995aea29211eaa42dc125d5e5d7e4e37a0a57ce21330a12d1f6a8ce2de0d2574a71dd2901689ab0fb62da396fbca20702553c7613114c992c6be6206f0aecc56af9dbcbb69350f12ca2019c746e62650b52ad010ead90b70a1a966925593ba7dad374399507f46da491969f5d0c7c6501e4d38e5affce45272a8edbfa2aeac63b52090ff62d8d58bebd3079e46e6c953f0e4a5885f33ab90c0507169ebe31a306cdbd7e43b5a03e2ba1e162fe07b4c24cc04f38ca9cf30b1755f3f1e54868dc079a217de8012c0befeab2d236be432c0b3cfd71eca5443fbda3da3d511bc1e65c39e9a1f62715d94d2649489b30972a46a25b725a659bfe287db39735d5471a8654c0852993dbc0334359288f1985bf25ba7daf437c5bccf60f99664308e6b94cd96dc4c9090990b9c69a74443c3a502d5669d251cac1578bb884f14e892b27868b2fe5bed7f2fe1f6c7b58fc94edd70a7e83cf57ebde46816b182bdcb4960011f522e37ae2617b60707774086143ff15120b57630da5daabcfb7cff5126e80921c0c8217aba2fed96b2d70782219db6418885df7ea5b785998147d47c8c9adc10658c2a50b0b311242396ed7c466606cfe71a052add2f96652305f9ad5c12e4f9a9b72e80a50904994d0f2c4bd12db6bc2e44eb5bc55280f850cae04fb5dc836c1110574af2ed69a4654e8bcc3d73267c1b19c0d8fcd4843fa078950f7193e7278a472f346ac696aa7e351afa4664104fd319ccf4a60d6ed2ce11df598993bfc0b58fd798bdc79dfe926794031c6d306" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "504d89a52e666cbf24e51de80e0eb2a2bd8ae0c531c948790353ca6f70b95a57", + "proof": "4c1a0e586d9b12f27390c93c1a03405554437104156dc806af7d93aaefa8173252b2fb340eb222a0299294b810178ebc2b8687a75ab521bbc54f466de97bfe6b50525f5cec6a0a424fe1019164fb117f3e1a2029c250af81de5c83bf98cafb685a9bd9c9b95aa0233b4532e9a829f18af84309521c8793ce583c994b8fd885588c47469a3169b425b799aa71d3827a9b71dd35675a507d2ff786f17b4dc3e20a4b8704f10adea4ccf64d6a51d436d1d4ac150e43d1d8433ee7579de5d8daee0840520159a46511f82e3d361308b022bfafbd3c2936b95f2ef08f5d5b98c73c04bc0628b9ebd3dd513c3c833b1b77c8ed9b9069870f4a20daf75377ddd0e7d218a0697bc122e64a727dcb92350c1817373e7eda9b887164f45690602751a93048c42c94709816e95ae064192e6e4a9e5390881021e291a6050c3c28595df9da460a4aef5077e1ee9101699b307ee12ddd4154100839186b1736f5ba2eafeba948e2713925c6a3f54ca781a03973be56e24077ffb061406b0fc088930e4fb5fd609eb915759ba613eaed0342a3b4373caa1f8ecb878c7372f3c821f47f9b03a44d34ac446db2c242157f5653c6f275a2d3dcbdfcc42bda80a2111cb3df3a2d945c2ca6ef01862b1d46579ccb8156dbbeb86b2506306afa4fca0d5a89afcb9c9938708dc55e2b41c34f2c4928475e30083e5f555fd21e949f46e16715061f26230642b5df6608cedafd827cc111bf2be5263e95d32a4c3264b764ca72ce88a30a744612d8c7c4bca4b4f9d75c2432e74f785e4569083b4d3336cd2e0a948a96f4378ee851b828541e3076bb80be86ed878c5f8efcdb03c4e5f612404330d29d4865ab01c2c71701bd71ac50146e2813bb6f38aad4a23d638a02d376b8090d2b72019c4ec3ce4d37dbda64ff35ee1f4588b071884b484d631b05d6afcfb43792f500" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b451c151fe80f03e5b274333e5c080fc98c333c09fdf2a5b5928ce304acf7b71", + "proof": "049830832f4ed9545f7ea5d62ea75a9b29ce05a0b8c784f7afcaac00b1ea631c5c3b194e242df605a666f93e332be3de65ec06119762c546a201ecc142f62e2db20987d513440a88a7aa5e8961f572be43458b27d1deee0c1282a9e632992069eed5e215338bc462e73028862cc5de80c1b958c92a0b38c2b7568a20090f535b52ceeb5ded9945c077279aa26d1b2dc6c1e5b1011ebadb90a2497e6c0b1ff50d1257f0c00489849797af589c616c17b5cf188a808634ccd27d6f652d356e1803b67286f026071fd3ced79a24a479114b5557f1101cffb693bb3f2759474a040e20cda450d2f2e25f4cd13aad71abc5bec8c01badb3df4271c90e9707d9ec2202dc2fd31a726c255f4e8e1a66bfec4b8d733216be046fe6448be0e7146a13bf7214444016492187f018fa64ae6f7a9b0002816c3eac605a04f6607f3571840004121a3b2923bf577d9eb2e24e9c68e11765db90910d770cf89ec4b7ab545164780a6bda477951e3183755183bf38544fa7d4aa41834dd428602cb234838bcd43fccdccd9f6ead6c26da250adc874af0ec4e563725c3b51714e0f21e553b926074383b2974ae63eb3da4bf3fa193979f2313cd98b93beb9b04199b0d6112de5071e62da1f3a63bc9aae7573dcbf5dbbab883c0ce5ba8822fde7137c6f58ea4a1789a4a9598e863cdcc2053c5cf48b8ec5aa5afeaddb50e3a134c9d75b0ac84b02ed280e6ba2bfbd643e220c9d70c18487039993de9447095aa87358101168b3812680936ade32d7f1598edb839fb443826d530eb0b832a00f8cb47d1728eef8d5f2e5f5e9454ef2a306ab5d8d6939c6252719ca33ec5f97c9f5f00478726029408408f5b7e86ac870a891e6418dc7317cf61f3dfd74146d597e6e59784cfc0cf044e35f0e5a0f81714241c7c425a3b0537d5be1ded2a359e5923fa6f40824e0d09" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b82d148ac27fbad72137f518a8929e005ea2d13575ffd4403dc88f9eeb31e125", + "proof": "92969e691a1ed1a6f4ffae493d1be5fb56c872ab56b001db16452cdf0919a36e7c238473a764b8e607c328bafe7792051b40546d6d4812e1d7ee16df5af8a16f8ccaa50fc1f177972c8a8a2956b8ffe215210ee257554c404d19da84d650966d6206e7c4e723717ade25c6016184e6f86e9b01dc5165d7abf555a256cab0ba24d89757af1a82e9f12d8ea368b1ff16fed1130744aa06d3c05db68145302e4c08c55d0bc56165a54656cf2e9b666cf683e0021686730472ae7d1bc2ce29781600a272b3dcc26140e773bd5039595294ed525f59b19d8b31d152000f5944d50f0e9af96e1376b4b81018e93cdbaa2193ca9bb6ad355fe3345e0f0e31d8b4c0826c1a6113af242d95a6888647adfd5b3b2f68f33e6b179727e9cefdc4023d99587226cc8beee56ce8d5ddf1fafc2d5ccdb9a56df18fba2010ffc94c61a29062750c9e9c4c65285dd17173386d42a32f5fa9105724d16441fe439324272ac9e239104ebd1159fb241a687d1e741d72241760263a4b6d59c0273592e76e3ae149dc259cbf63c606e9960210e4a9d70ce39d9b4450cb1c114aa55c8a9b93ff9f4014440a2fdff7afbb3e5f49dddeda38fa23b26b8cc02bad6a6753a0aed970200cf97198cf2c78db18436ee8c1e867f9891e2f3e366743bcfeb7abd9c2e13889ea620e445f53e8f0564b9da6da71300542e695b287083d84a7a8b4dc0f2ac179c25428663421de723ecdbc6c33ee89b2cf6127cb196788ac698993d1a675d03cb21b3ad050fedd9a7cad2b1e361f720a471c6c434f0697452b872f34b12fa87fda875dbaf8af56a65156acc7c041378c6b3983b2182236572afe0862ed8e72897b233c6309f05d92d180144b9daf142dd2c963d2f62f1a748ed81e93b3867ebfa52c01f4ad663a28c53d1445f8b74b8cf6b5cadbec7a147b3f4e2262ea8ce05c456008" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "befce921e1a030c0a9db0db6d9e27d2cd88a0f81f1d492c6b62ae5dfbea92728", + "proof": "420a4201d972dc1db9bb7ae923fce285f32a2b9a9e41752e2bcd10bb86e18b6db6feba3efeffe3572bbb79ecf67a868398ac74fe4b55f1f172acce58d0590735941dba7e1eeb59b0d7ab3c68ece11db11931e584b189790ed04ffae88a018c75a85575b6486e6e375345e9df40ab7a4a0229ba2b5c49dbdc97650e9b7177c7053110e329e627e503f05229c0a35e6cdd3f40d92e259bc11a4e8c89018b2a600d0c89a835b7c58ed2f8890423f0fb6e11c44c3d48c741f423318377b864d0120fe000041754b72ee24ca71e3b3f0d5869f4c6cece3e875df3ee4196ff5668f30bc622d140e1ca6f376c8085c412486bd10c7f4422f12ccd83b7713bfa530ced3b627ef7e9393c379715032089da18789da0b08607572a097bb3cc68f6bb4dd4403ccfe058c8cee182a24069474549ad913d05fb1968b5c7f53da8aee626fbb903daeb375c8a4afa3274c8f47b6393f2a030543228e41b492659ef7ab0e87fd06e82cebb842a7564d394b2914bffd57f6a74fae871936fd888c0a669133c76f06c0c730acf983e9cb8d2eff9c2d388c04c27c9e850235c299ff8f82486b6fd395df0489a6143f656a03237245c830c0d4c5d2a979735c69c06c42b5d53ac011c3eea0b4b967494d38146a74ac4e40116d19af6b8b96e67161c3226e5324d857168f2bac3dc90831e36b94223a11f5a471468bf852cb31781800ec855300a29803f66392d433d9bac638351e29fb29cd9db75e14e6eb0e97be59e7860884150310f0ce68b42054e40da4b30ecd653f51149aa538aa6e9937063a8f37cb553fbc2756cab490e9d71aa24df7ed481ceb6ddb274ddce8a4c104709dbb8abe58388fd7d2ca2e076b794cde2b5fd0620cc6543b638de51ae6f18f4eeea0b898d0b35b1024f25612c4355babf254ea01d03c5d16f3a2c7a1b6f108625148728fc4978d009" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cecadd74bfd5f874dafb00fe035c674e729f9f730e9542fed43600cebec6f343", + "proof": "2cb83c70a7ae8f7ee8cdccf13b22b25276055ee9c8f1bd5f497dde77ba6df85cb05cde678751e5bc7e945ef5dc6f30958a86d4bddc4437e6d82426d36653a12ee4f393a5c0a593121b6f72661b1e8a095b2882cc132d7fd7438deb48a1a2da48a8e4c7916a9cac1ea7093ed2d319620e5f4261bc94b2951f0f670fb9137d0d17c00d641f3dff5a2bad7bc4c30c5449ee75409b765ef4050fbf1ae8e768da6c03a97434eaf8126f37840a0875c876520876812adce0cd5fd0c7228e8316e0e50c6ccd7086b25e771c7f36fd9ba6b504cdeb9de77f682b718611b412887cc2a80ab6bd08bd7dcf840c7decaf0fa6ceb25b0ad745f115d3463c2e713e9f1ced9415ea575fb2c9e5812efaeb352d55188fa48f6cf54f13508c877a8fa975a6bc0f6b1a02ba7071ac3216c1593cde1dd83d8424c69d45cd06dc633fcc44c3bd7eb7287833bf868c354583dd832fd7427623e64d806828baa0a905a1df91f6d8814624903a0a3762fc6a90daa0120186a4792ac6967a602595ddc71c1434d8aac9f240f466ab812e4ec6b67aeb4724b1a81614e3863c4afaff047d9ffa60cfd35b93373ea67a4b46c220922798fcac23535dfea171d1846aefb0e565179faada250c2a805273bf87cf446d29b9bf972b5a35d40fd8ecf5972df4b2e865c65a8ed6cc6c7075aeb7e24a4d92bc5ccc07553a41962e41e739cd019c872920ba8a2ecd6242d2e4bfcf1879438d252a0051e41fc6c5c79f18d24413d23e6770b2b47677c658b6f222c62f2affdc2e9ad5d2b9428389315ebc1e638fb5a487aa87d7ea0ee320ae080efa2513685e041f1306e7ac89c8718a2a4ea40b389ec94fff3886d60e26951edd5d37b443b16075c96a765e9186b1e087ad6cacdab5d35ae6e1965c5c02591ab55109c05566357131a433373fad990627363ba1da426de090322c7e4505" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d24096421b53902ee736ce947e800726f8877a711f3b1c9bf38944ee24fde174", + "proof": "b8d80e99e0f84e991440387715f83a1f144e5aae8a9467e869c03e59104752631813ed1b75865293115fd4da7498efad9c43a6b73015f8f2b4ca5dc87fea4668ec6e9a54264ddedc468f52c57d80ada94c0cb0d3b9136ccc08f8b5507605260f60471bbf398f1fd93716329316c1b1b0dc6794f5c91280d90cb012a112dc2b7c53fa87d7fe03257c239e67b95b9b24b9ef04a6cd7fbc699151de63e4cd531e0e318d77914d97c583ff1591967a6fafa82331d8cd8c6fb12f01d89276fd27830b8011079f055781beaccd777e6b302872c6e137aeed586baec93a6e2867d7dc0030ac24a423b45a558aa851f7e3dd533e93b555f974b1638b0ea4be79efd7447eacc02bd3b03a7a82e95ebbb97b61d3ab752bac01fb93c7f6e86fd279d14b30105cd3483408c5627d62416f88d90ce6c9b278cbffa8967bfe5918da7bb780f4793c24d4d739992f7da5ca2a8d9513f4ef7722b0d1f9a0fcacc245275253d61c5bf2b101e2120b320498638bdd554866e2611a88107d09cf8324f04d92d608704d0c3882d642ab506e39c23c0a63546a2fa0c9733ca7a75c002db621d1d0688177a0e12ec259215f4587f7ead0d1cfd62a62f24308ca26cdd45e7cb584871af95b3a292f5f28c64084d70c52af398074b3e95b048f04c822f81f754fcdcc96a533c21d9aa3ec06a8a66d0f4b11e28527ee07c486cf58be59816e8f05d69c755064605765d463bae7139c7e76553edc1adc4d57dde21d8c7c8a3f7b09573b3df832649eab1c0a1656fc015442237056ae7cc7eecab95b043b5dede6ff7a4f15fd45681c74e964d832d86d5a2bfbef27d82bab176533e4d8ad956dc9f36a2f202a7d3734d3d1b5721c8944386acf0b3ab4ba908dfa67c1a0bf920a49990a5cf7af0b75d8ba9ca0acae9a2564e7abd9183fe4a47e9c0ed461e5b9eb0072adb3d4ef0d" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 48 + }, + "commitment": "3efe2afc88ef668c36b947e0122c0c98904e3235b633a59e9635424523864b63", + "proof": "743e259d3b99643c15a42f1f54ed224a1bd9cde82dc1b62713e3c205a91c316af007260b25a682e322b769630d2c27cb499931be2877824db957ceb51913997fb22618d1ea660d48e9ef1ee027ab2f0ba8c43d85e732d51743136694130a476a626e620e0cab08d8abdffe4e388346ea88f78c0f91aa6b7791307d90a2a1d75dd30af67be0a9687df3bcb8ddef32d0e8d78c0e7a6759cfd0bdb77c19537fe60ccdaf9c5dcbcf397eb898ff2551f46e4da4a7e11668a63c6b607bf16a6f78a80d3c73ea6f610b900b17c297ec77754e2e1bf13d1416c7b196b02d66a11a38b80ec4ebeee9769cc06308c22c221eeab5393a56411aae3521002a47807dd7ea442c949cea17164c5b5ca475732cac494f450083542226ccc3b8ef999b5654f257124cad444340c80bd142ef6b5703de89b0c15e4aef10d231ee84172aa8ce8afe4c14bfe51a48df8fe6e90725d13668a061f5456f64bea016827d6161d36a21134ee8286da760151b5bf5b376cf00ec5c20635ca3dc443d6a4b8ff93df8401b0b71c81764d2e9cd589ce8d096778152d76306114f2fa52460d38b3262cde114cd77567141591c88376bfd89c81aae7d937c1eb941234869c60d76b67ab98b8c7b50181981c1ad46c5e335cb5c405e8143835a8cae307d57b6084b5b78130f6e815436a55bc6c051f1fd324bfbeb5ee455be6b617a57ec04c1f54ff6f74f76f7000ef6f9251a62373e52aa77008e915173e4b433133664012972fdc4186a6816a820f099f352a59a2299efff0ca79d4bc05921a3912498a6528513a73ee0bab00f0fd805fb39c41132d09c8ec93b9d8c605a4a03c6d72f336fa69cbeee7339cc6f730ca64d4cbd6bda464397879ececd616c4bba41d077eb13fe3976e8fcad3bee071ecdde7578d466e9757acd2c94ee69bc967fdb5b8f9b22cb4191bdcbacabfd01" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "38512b9c003ff490cf713d8def4b587c58bfce6a4badc2610e83051204387274", + "excess_sig": { + "public_nonce": "9c5e61024ca47dae5dc524252a25234d245235f779415ee1aaaa2313a388b451", + "signature": "4f3d0f443607063d163402947a0ed41bad490880ec64253dc1de3cf0d2137201" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "3c1307f8cc2a6abedb7d706230d9cc28a7b2b58db114d831e2a9dd683f6d426d", + "excess_sig": { + "public_nonce": "c022f44568a6461c382850fc75ec10154699a455356e1a0f41de93153c89bb76", + "signature": "be37b29697120732aa00c9c5bddaa09cce7aea0ec9dc50851f409f6b48f2ef08" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "58c59a5a9c4a078b6fb86999b629a0637f87aee70dc243fdc307570bc384ee56", + "excess_sig": { + "public_nonce": "9819126044c7cecd737fcc8b8067a6cc5e274cdd0a0984301e8d020a6f24ca1f", + "signature": "d98ca093dfd8d013c96254aa594a8c8e4105b3a848f2a57185367df74e33f90f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "7c907204c46292bf436a712f48cfcce8b9ec8ff171a297ec439afe63665f021e", + "excess_sig": { + "public_nonce": "e245d709f64bb7218e6468660ef79881e65d040df3f5f2cf342f0d47e34b5e71", + "signature": "2352820e5f796111eb764cd8768e7d4368b39cfc05ffa394986c2dc4c6b0f50e" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "f45c9c75538541f304d5d6112b9dc380b84b48db1a56db6523042e5b93037a51", + "excess_sig": { + "public_nonce": "a68cbafe774365794975ae389ad7407849d385d88353af4c78bc8244feb45c26", + "signature": "dfb5ce9b035b97467a4d74489884e1ad2c049215c34def9ee8da02bfb8abdc02" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "263b2c2ffa985d60952acf08c341a8baebd31156759edb387317e3a423fc2315", + "excess_sig": { + "public_nonce": "d080384423e51158b78d6311424b1a6208f92349e52dc0feeca94132200b2d73", + "signature": "08332a8d354239b5b38d55fca93763a35df944d0fa6a8f92342ea8c7669b4701" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 48, + "prev_hash": "54f4720ecff8747de4959c5230747631bcc00e4a8dbfabdc001edd364aad1574", + "timestamp": "2000-01-01T01:49:01Z", + "output_mr": "3ced17079263406d531afc0717e1aea6802be1d3eb8c9004b9429226dcad3a3c", + "range_proof_mr": "74c27e16b227bb369ecbe022ee8afdd3149f6ce47e8c7ec292c752d9370daee7", + "kernel_mr": "2fec52e01c107728eacdf4124cc22cf3535f96b9731964ec3cbd21300e4a2fef", + "total_kernel_offset": "c4a040b6d159ca68b2f551bf856b88c99473b686f781c06ddac3be5b91d5fa08", + "pow": { + "work": 48 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "1685fd844f5b6629db0ca74d3d36ef80c484bded9babec29f91a85999340a055" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "3cfaef994e7d400de3bf2eb74952e62c093505571d19f6c93363f73d8169ec33" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b82d148ac27fbad72137f518a8929e005ea2d13575ffd4403dc88f9eeb31e125" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "befce921e1a030c0a9db0db6d9e27d2cd88a0f81f1d492c6b62ae5dfbea92728" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "cecadd74bfd5f874dafb00fe035c674e729f9f730e9542fed43600cebec6f343" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "0226064fb27a9b99637867c8d7e034d98bce9be170be4d538436f0443b5bbd22", + "proof": "5e4868f7e7cbed3f71479601308c9a87646df2c362ed149b085ae271d0e6cc43dec3e05f6c1696487770df46d0700e56aa8680605551238e4d366c8ea3fa6b1d505d4ca0f72de65c014e4958f99472bce609ba22b6ee4f3a77c8412b27278667420e46267375ca32e16ba41e4a86d0237bda4f1f2dd85fc9d914ab7cf8bf9506ee287435962cc3b3e67e6eb9c86ea70ee7e6206a8bca701b87a6319842bf8d025a44be846b0473de24fc4b75414389d9f143ee1d32de2354f3963af9c37f5502be40ad2a5bf8c6736417e1f794094a39cfc1b6118d0c27bfaa05c7b4ce7fd10bf4553cbd23381b79cac1f78169a92e807a6cfb98e8d429ff003168ae29b41121406349724c7530ae904c6b7f694d33da07d42cf02585ad46540b482a7856e901b8d4c0121752b4d0164b14747d73202908bd4437e6b0d046a74b75d3fa51ea2a36c875a9daae6124d6620c610051b0c4d0df62feb44f0d4ef09bb600b62a570f9ae99169f5ac00e701ef9d1b39de1ecdb1d3a20b6fd2d6d59d14ae851890de53d077bf8794d00f6126e224c13d9e47a6262f6a415e385cbf83ba3eee2fcd1e08eae44f20d3b7eb4168a7145da964940beccbb9929d86a9a0fb8942c328ea89580abd19978c3cd9f9eaad6c37b7438ec2ebcdf002b5279743bb7009219503da15c8cf8c079e89c807eacb2382ef5b55bf03c947c711f5614790d08ed12a9e75340e150e8459417bc17dfae7b5d919513b9dd4b4da83569d2949e84f4ac632863320624d7d38d18e500ce3519419e4272f04ba7c40509048cc3eb5a8cf42ed3d3720cc41d9e6a4a51677d6bddc8f60b6e1db70348cbf42658527ed8c4cfbd2976f8af1fe099b3efd9c22d2cb83a5876b3cf4112e360bcbd1fb2f2f0fb371f2be03c5625632795659cd7ad87b886af89b5134d859b7ec9c14a39864f7f45f1a3704" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "36aa9371e33da44baf5faeb65d5ef0b5f88c814663b5fee6061d492112efc76a", + "proof": "b4c5a58324c8112d719ade9e9424447376714ddad6901b23f34a673f6efd2a1f14075114a46fbfdcbc0094eeeef31b4228fa7dc225a927e95e4fd8fe3edcab0d04c0df050d6171fe35c80116d4e5c74dd0926ff5f3d22f3e183a4a1f07159732d88576c1d88592165d3734e42a327fff925e87a4a15d1041854ee55fb14c8a4923bb410a076fe02bb63448a0f3b3501bc9dc6411b5d089ab68cb08c0594f7d0b9abeaff5b9711cc7fcc9572c74fba973a4934aec93166faf612e174d876a2d0ea0394bace978e14e009e79ec0dc98040096df8e9e93d221437b75815208474078a49963dbd20fdd81bd29ab2c58b5c8fd7ff27ee9a7a48ec563cd018db5d2e6b0e8781ea9cb1a1c0069151c784541864db3d849ad827738a2b15ee6fc784117500389d8314dd5d4d1d85a68edac92db4d45b63caf43e5e94092243c9c185583e686fc66c016dc77a3f17e91256d4b856f6faf97b4c0ad05d55f06f04a676a9241e038b0820a33a768ff791f9277ba3a7f7ccd0ba2157af70ecd73aefffda980fee05006a22eb455ab2330b082dd0c8deb883f3017c223ad817db6f125e1672007af835849863ef16ac50673cd189af1e155757950a8cd01e1318374b90f417462ef01cd910944560efe6659410ed172af2da29f0bc717b2fa678523ebfede05984029c040dad60a07b50e6a87fb31da68340b6abf7561cc944437a56f75f2437d87441d12f9fbbba7410d57b2e49b33684386e166059698b846d4f0df592941156d9ab85dbc1f0fee66cdc38a76790e2d6075c5f38a10c6fcccc8074c098932c12df2db938f66df367506a0f9bfd53ed32406551b29cee670b744a45d80a160ca6ed5f7ed579ceff65cb520b8b1e7df7d431cf7ebf71a2c6f4500e30dfb7a40e1df50a408f136739c1b97e749ab992f3cc298a640dff04d27fa09183a001f804" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "52e7bc3a2431304e36313da59b9b2b7ccfbd537769435004691efd3a21396329", + "proof": "122b0f1ae43ef8fcbc6f8e4b56d006a9402e419fdc1e420d5c51e3eaf3fb61311a057a0caa12e4942c2e45bf3f6b345743584365f23e9d4a88ddf68140a82e0cd456d001dfc204839fa134725a5118f464f8bea48f8f13e86b1419667cf7ea19ea0238ab45738da97a70ba7a7851135356b68052ba9b869ae6e9e0257714fa56ce9f68aa1a6ef1021baebc2467df2b551db109166255795e41524eeb4db3c009b0cc94013d152de60ce42a81e197313818937fab99085b5db76b0562ed5e0c023b9b8f5e15e129e7f18976964556f3bc4efb1444dc1f955bdfc8150221d7790cf0bb5412e2b8eac66210420fbc1546e8221e754a73bf36b0b6128e47db1ef156b4185532a8e7373e81a034c15fb64e89eb4897d8e5f83bb81ffa15c244b0b76f0084d3c9f1e836f14622660938af53c44fa3f0bce904aece32ef93fb91e0874684f6f129ed44d5f3095b4bb304cd6e43b9c56f12d8850a348b578c0e6301ef4142e6be2f2ef57979b6858473f893431381f4137aacbc3f881121858a37a99e2068fa8bce54b27a724355b270b1eeb7c58e03cc070666fa02b5174c86a2c36e453280343391c7124e98d52ce2c7e6208b610dda807fedfdf13d540c030b422214807b43a310b29204827837b5beeee0f720ba7a449968054e5ae4909713644d094a616f8e5f6018d7edfe6c162da651a959136dd19e9644b74c5501d7687f1f191eae315363699a684b8337a9d533787bd7bdc0d2584b6ff9c6179d35d4531b0ecca17b5b1b83878db0b65af81e74550eb6d2edd9c25abed76c1d7a90d9b3e81a70514c063102f3679e3335d8480c594b9975a48d7a30b1a71f09cfec9f015935249ce699326786ca27717548c161416a185703089f21623a91119b7bf5f7fa0992ea7dffa1871a90dd629080495b1a6ca105e653393d7ea3700c678ba2f1da0d" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "56e24dfb24a61cea7754a853f2b6958f4e549f6d92fd57382ae51dc94e144553", + "proof": "d4e6b1bf83d396597719e850d120d817ccf88c9b1783477fb0ca48331c93b06f58ebab6fe71eee51a2feba6761f8927e66e2e01e6b2d4a160d46ab5cc8e39b48dec84923307b81d802124e6d2ec51d8110cdfde64809fe73f0e2fcce5769624a6eed4891d465fdd74cbdf2f9abe4dda9e32de830009dff41135707f46d3a981d9c53f1483e9ba81c8a57b91f2af52e35b19537d0e20fcba80bf6d9dbbb1937029c90e0a3a224fa455f203e9bebeab7618533e8fdc818d8d646c28f0e0385a000f54374c33cba65b056578ff23ace70299fc927b326ba5ee315c1c349715e3102dc87806e75ea3aa2a1ba91d8968503e9960f64bbdd6dc08dd6a75c4bba769c75d29b65d1851cf54efff325cc9b76dc7299a964a7d77ee9e0a100002404377057163a91717866560efc910c83087d75dbb7ee76167c1e0c7b74a9d9cca654b2680846ce58f68c1a1d4dacb06cbe01433d4713dd30639b8e6bd7c542c41a415c595611b3a40e6b2857e6f446bf52e079c7d8ed0a15dea4088f5c1be1a11d73f03d082a1e46646e3c60ff1b49b88d6d9762c5de26d37e1ba8afeefb22a8ea43c26fc67fae809a1ad49bb9cf57b6f8ab9e4aec4388931b72d9554f7b49d7c431c33cb2efc4cd629c7f051cd4d1cb5300c5a879187ebe58e321a18d44913df2177c5ff84f01182e94458f3bd50795453b0bc286321466e33ce025de3bbb5abc96a60a547ccaca2982338ed5b6c187cdbda8b9f06b9101b4dbaf8a3b41f62910b4863eee5aa847249220193647efd306fda24f81a143000050f9729b4c7a8391ff613d86ea39bb0688344651bfe9a8dc40556df2a1d7161c7de6de867efca462085f77d09a16b599a0b7d68ae25de99806ee36394c60256dbddf73334dacbc12b91406177141f3a2d42f5c7bba351f2d5ea543ece37910cd2a98ae3d83470c8038580b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "62bc13507f9c05e5e39a28f6b5a0d36c6ff0e19f1f0604015e8aab8d6411eb28", + "proof": "2028d8266f76ab78dc6af3855120c02aa7f29f8fc4563da3789f232c4445200490ec15cfffd73522157481d0c33aa47c0347f0210edb3fa0d3f88e27caf74e16fa22a39fc2a48074065a4137fa71ebe5da38cb9bc0213b78e6b25d3ee609606e8aa208b1bc5ef7036fb4a999814cf1c4e0e6fff1833206c8cffdc1fc2be4f33e3946879cc2a7c5118fa52f910b0e208f66b0ca35b58389a15a11b737ae85d101108d625be72a0988a08a7beeb1557b4758692b34cef5308e9f6336a2cb207909f4e7123ca2c80b3be3002f1e5f873e46e5cf1a4a18a28a9c60742a725e8e400af4ce310729d2354545a73185245661b1934218c6f519bf93ca6dcd6f88a0193f50512f9b7d1025b4249bf7ad4acec69dcfc33e7ef1addd6c8faedcd4c8429379ee0e0aa6e360310016f6e9239b76bee065aa93c4569eece480bba4a46f71b51340f3ed7873ebbd42de2ac77c4945eb14a01887b00c69f7cf4c88371666f51c5e662a599218b0dc139b84c0e6453ec0eb9014701a7e24562592b9bcfd7dabff520841538c42888dbb18a7ecc9c521429f1527535be38df440b43c3f327972cb26d605983ec6acf8f674e51a7adc7a8d6c019e59a8af2bd25d8bd023490fee490ada04f0481cfcefba557a278d01f425f5d996f3511c053ee0321c5fb89052aa0bd6bd6a943477a201b3e95e4f662479ac28c44a06248cc3fd407c2a5ed98a250c9ae0eef7d471f3d908fc29f17cbd342f46253deb6c08f23cd3771a22a092c73efaca7ab56cafa778febc1ad393d098886ebdb4e3d4516d9960561ae0ec39227aaca5c3efec639ff64969a3478db2aa8d95938e24c501d1aa3ba8e9511a93c044c436592c03a0bc68fb6b805d33122b66c3cfb2a573b6bfdb658922f1a062310fee745a41c6dced243aa6ca003b437198b404ea38810c3b60318f29ad0b0b2e02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "688590b9da63911958234da5a916495e44c21158b3087899bc339411d10c271c", + "proof": "e68e6e7344253801dbacac8cbe784514946beea79ab5eb3440053b8120bedf1208bc3581662523866beaf5c565e57b4a6efb79b619a48ef6a284aa444138fa0b9649657d13de61afa33ddfb8d4a95afa93c9ac6c5d68aeb46859bfec888582668cebfc64a5d499e5fb4557469867aac6f39355ea86fb1714aee68a0584173e3be0453a662b6b61ad0fadb24a8bd007453731504a8963257c8206f0b00c4e2c03d5de3eb5106f47f6e23e6222ae3c4f53f3869bef3eb56dd6a2bd7ac15d954107f3f82863e819065df66b01e0d83dcb09d451ed62d50c2eeca3a7ae5edf7699000e9d85c41f352fd97c5ed52a3cb289f6b0abd0209fb78cb06c4d9a30ff22903372a4cdd76640d3e675d43327543d2466c0d09c69b86168586641d5481dae002ac2460b4f74f98a6c896829212bd2af849e866c0b31bf2bc9d885f439bff9ff7e20a6383c5b86723bd2826a763f90f5b2f282b778c861977f512e5f6207074a083ae9ed90652378817c3046be8248f214d3b6e9e35ab72ef4c16eada8da97fc64005a912ef6acfe2398fc366144e84323d20b6050df2988d4dc887339a16eb66becb485043a1e721915b32052770a6ec70bcb12382bb8eae7f25381dcb78a1f0616bbd6c6313f2c5783a0753f6200546c2bee7aca1af66a9648ded8a6fb30b56422cc85b3863a10c88ce18f56b3d6025f7cd095b7799ae1fbb658d35075c2e0384af4d714597167c03f77f82f9fe47ec151152e609c1de575d24e888323cba7352c1389738eabc6ba9b81651799374ac038d668a57730c67c5d9dbd91b401ea1318c17d4474af23bb510b85420543d6edb3ff5b3cc420372b626bd80b3e5be858ae9d55190d2840c67f59232fe4c5830455a8c8380c63effe658129b76eae3b0ecc00947cf4012cf73b161788852a02306243ed7a5c9dd84c7cf9df310a71fb0b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6e1e0a522d99fefcfe3cabf834ed91d8f930ac349a09d88df4271f85cbd2d60f", + "proof": "ecbbcdfa1268456b2773217dc7337fb2d1489ceffb4698f888a260a4d992db345a2edd107c2fd757aebde1a4d83c4d3e9b6ee92dd7854de99ed9441d987edb45a03328034dfb31d2ab479ddb8a642a4fecc2e889c3ba6836dc34dbaa98aab02882ccb2aa187618db1c37b7e0c9e31252493472f6ae9a33dcab3366dc4463fd4fbebf9bf9c06a1348c8f0995ee11cb63bad03e07ff05e4e944acbbd986d28a4009c211d2219d09402c02ab323f1ee26504168c8c0d5486fb878e86f8b6776b702f262976ebdf1af8864df1a9c111cfc56111fe89cd4e7cb1484bf93c24410ba0e58f96083b777b81c0fb9de1074b4c2bb8e308924005f9039307d97720ef51c5d5645ffe23fb8aa4241b8e281018f1a0fa8638514be8cb3f6df4385ff5e08814520cf76e307ce9858d5c211c207cc56f2b9a04685e076337b17edfe23b6bcc74bcce089a3433ec2c9d55ace65b2c6648c03e1ce82da41e3c68a38d69f7ad89c2e56ce87f57052830b862e1790ff454410dbbb2c0d3234be7ec1b8cb4757162778a4ed3db81680d1fc061b59048e8ee4b4c3c8bc7f114f4fcd4cf0d29fddc78a5c5e25787a8a5cf00ac2af506885f38f3e0036dd83401427e12dcfddea5b03ce25b8277d7c9c4cdc4cd0e7271efac7d36e7759a4e5c5edb31cbaaa736745a09779929eb816247b8d96e9039c7f45eb0b122eb26da8e0df9b4dfbe945803be49271128b10fe859442497656ac6e8afdad3f82dd69b67132ad8d42b22f60f3245720da5e04ea254e21a604c370c2071f3c74232da0e641c7fa51db3ed65761da8e31dc8490a1004e9be1bb6243111dd1a7e9b87cbab84fc1a65afa035d665327cd6784cfc97dd30f3c244bfc98725b06f094dcb3fbd1b06572a34017148265c5c4041357bbf62ebf8a055bf2473a183ed41e18db76703bb0256d06afd30e7a124201" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "8e138968d35d720937819880b95f40e1f1ad27d1782518d1d0df9aeb4a4fa80e", + "proof": "44cf2808a449cc8600a3f5297c82aa552eded5ca93f1874021d6b9be530c757606bb5d3b5bd74703222aab4734e7d904bad911f65f8c6aee45a30192e7518b505e40fe0b8e67cac5f837d661fc2d862e8e67d8a3a3b3eef6bbc9d21554e9bc36c84012cdffca8e3d0af4eaff86c086e74d6aa3980d8d2e7fd46e695e4a17a84ba0d5aa9e80e2e903128e0fa3b6289ba477efec5f8b9c001b6fef57b9d3f2050cdbe1962bdf1fdcf8483848b312a57d9259d7a7b7275136ed74a8e6b20e50130d00406b533d5fb6b013886a026fc67c6ecee9ef79d4163f0e5d9b5ecbf39eca0730c478d0d256ed91b0b06c6c292e650f043f0c79dff492444d4976e91a07ce70de0b9b1ad39c00df67162d5b715f0dbb3f200e5c3003de1bfa96daa323153a32200f86e2275178c516bec9ebdb376a068586afe19a6482a590d36ada5e769b3d5ae01ff26af45c7d27eec2d737c5a954e8f68e3e03dc16ab47cfa928b250784d1a998239e8d356b7a5333a13f20102639a3379b673451efc02a55a2e45bb69581e877c0b2f7080a93860f0454621d9cdb2caab528f27e71c0b2f5926c1bc0750cee9acbb8bb9ec3a1e58b32ec628472279c5c5a001b0e0ed9e6531c77c8d3601d8447b6b4152570963422e8286fc86daf015b47458ad9be5c5ca27590453f211da8b96f5ecaf85714525ad97a290e440dfe3ac9ef501659a8b51b579184f4579b0873b68d2decf8d64ee8fea9da660d6fff3e157bc6fffbfcbd4d5a05b157238367b2fac5c2cdd8ab07e83dc59e28a4bd9ce9d7cb28beb617b225dda994ddb2ba6146d27eed30d7ed6e89606e32ea06b01204744d43b332978106ad0668065465236cd20d304466ce5d8dd26bea19e990576f5fc5e42dd1c6814e399b0028902d2ec619b15779c6c9f83b17f544c8e861932e88dbfffa1a3fbcc44ece3d05c07" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d4355fad7336e1e2a9ebf517be28246b864ced7de46a010d1f8c3ddf09a1ae4c", + "proof": "8e5f093d281019bd119e17476f506fb21ba10a1396fcf76f3321b97d62d81377beee0acb728023b541fde500fbd3195aa9097b1b6e9393bea7622aae47bad84f20ca133d768466ec8a294f4c7641cc311316ecffdb39730fb5c274e4304ed4710ad804c09d3c3b001f322d33229ab6a1ddc7609100e8238c70cc6fc24944bf662a704e829f379e7ff6beca9857130dfeab3704f54e93fdfb0ddfd5022c1b5805fe209f1dd36dc99b207cc22951b28a48c35d7d3fd47086d010162df01ab2790405952f5dad10d0e2f4231a0a4b39372cbf59837533469b76422177ee0d91d502626d09957eefbb7e9c23c2109b30d23924ae6ba2efe78c0693184b0716962743f2714fa0f6b6bc1219e2a5daba1dc095432f4d2a7c9ffdb6af6223762d32115746342b14ade20c1fada4e48ea3366b5aeb681f372376895661d67ad0d3aeef68a05f33461765525c06069a3455e5e4bc6bb8dd61c4d7ad988c5cdbd30923472c70ce85d41e872d3ccfa6bed1d9fe71fd9071012599fd46a405794ab83392c178ca3b568b7d49bc905be43c84fa5dc2fcf40a0bddcbf6d957a36a906e0cb4d860d82254b6120303379411d8d6b30b1f0e9eb611d7a157ebfe966c7c72486bba5f8cc2152f4483f2c88e3d1de7eb8c23e44edbabd2d8ccd83b0bfca8d4a8f5a84a50dfbe4218f6d3b391b5eaa6217ffc143c5a74cd494896a5fd9998b7d8ded018cc48a732cf5bdb23130e4d5f07fc27d65ea196745587677e362e13499fabb530fef9f0237e8cc4fa201c551b176f6a18204fff22408fd4933171b16f50dde13d8e572e8790b32fc56a27168b6e7f2a5892fc92e22b551035cf896c6500bd1c0e5aa6d68bede4f32245a945ea32674d5d03b252192c31a258b95f2633d48b2d08457f9b38f84627c3a84e0ae5acf81e59e9255ed2bde8ec8270a8f641345c7206" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e69b72629f9370c33a182bed61bba3e5b95dcb34c65f5f5f7ecfd6545b4ba079", + "proof": "48735924ee09ac808a806f8489d4b53202d876cecd332404b4b807e98c9f65668a27405986ba3e43f0a315eabd390be07702e740786f7cf3680b73f6b9014f5ad2900688b0c018b7793beffea50619979d669f7b2f3b5d5e4e48c72d748ef116ba56076889417f4de93517a91f3e67d22847d9df7ba0b0feb43aff2891825b17926ca64f7e8a21ae85fafb1c889b3b30b8f7d775e41d2c428b2afba46e9e370b4021d310893ef0b3e5dddfbb727650c673f94626a7cd91254caf24698ec6ec082ce4da8d7efc94fbc9f5f6723b74ff14ff18d53b21d3b9e7ea1ae7de21b8a004de047c7a82c18b61571dc3d64ae3c3000ae3c63cc6aa2cb733e1f5faf9d7d978025fbe5d4f354521b0b323cad68b35cb49136498df2febbb2a2aa3c1fb796b29ba9d8eb1dc14a42c4583e422739042c56cfd790dfcfbe5c5b2ee6bc2bd1a9e42e6bf028b22a588244fe8408e3d7f0aff4b4e80461696ccc841bac6a81b473e0e087c5eca6a2a6906b7936a286790fa01ffe3d604f0f75a440a31a46d0ee60f036aee196c36b092916b6a4084f2e1e523bd38a538c358d89906836d9601f12a31b04c49b9115e6a7d3e1f9426a7f13284954e3f3da689ed6299c86dc1b07e066af889218dcd92cd23881ac7ef5a2e55971ffbc7e034e999332f08f881396a477d7efbf7be331b056f974ab12d2927d9a4e1eafc1337e8eb4e468924175782d436fc7da21d19de7fc8702ed27f6f420bcadde8c76b8e17ece079807a65b7c91d4cc6c7280c8b18413faf4541a1a0a8a36ff5edca3f19c25f4450ddd1c1feea05685af983b4ad385cc054e647969dde374df3d6e63233fde069c0f45d7c9711c00751d6346eaa4d02792c120b13f3f798b1fa172c7545892beeb4b59a84b660500c152bc05ad5173ca0119837006348a9d4db40bda1ac97a23ade7cc2c85e599b01" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 49 + }, + "commitment": "8691a4e1d4ac439ece74d5a6002171b3cf220cac97dded3e4ab59ca3c82f4676", + "proof": "28dadb981f7ecf63148320c304aae990faf426b0fe0f2593ed7c922a75efbb36d6daf177f7b21d53e210b8c92634e548a93c3bf40347251b080036055a1fcd6afc6e742257ba7f643f8537833388048adada9f9060de8d036d54f5c9afda1732bc95a5cdb68dbbe95f760210f8679184216fc013b4bfc7f2cc36b1a697a98879a142f93f912aba904c4c879ed5304cae2a82d0baded3f436d19204c11def1800b4ee0728e92d2f8abcaba7f6513396f34aa66348c228b1a7683d5e37afd6f509d85d568ef8bd422fcaa0b38252f283f2bd1702688f29a3d3cd0ccbb7542862007c5798bde22fb8f70c9434ec201f0ba5869c2c7b7d43a85f8d84e31eb9a96f115261362161674a772e9f05ad5784702cbd21cf017d3ff5f23454ac703b7f5207e0698f845ba4349970e6866f2f37cb345f4cc098369b9beec179bdb9f9cd0c5d626bed25c4b741d7b4d64ecd46be56f52b34375c5d426f62c76646677d0e9f2bc6a1ad1ebf41778ef2e0ff6bf2152e3800b967fcd55517e2bc93dba7231aec6854b441bf1ec021eeb52863260245db5c4a231865de115ed83eb2ef43d2bf3d45badb5f151fadde104fbaf2c21facd07d2884535319c2612821453c7efa100b45966b4ae072bb91cbd730c9b60c632e70a92e2bcabd601b79cfd509a099a1094f187964c3218e8591876d302f52cb1c31db6e903af37492756e507716c1e2cc23024bf2b137657e2ba4bc5b38018f8bcf2a1d695695b77caf92be581d20f59715a8c407d6c49d53c4a14c435898ed028fc0c1c9b0a7f8260b1f1ac2cd90da9b0e10401e0f7eb6ae763f16ed5b86876e01d57ae0b9ca3ef9ddb7ed94a2c1c3cc172c0ffd3770c0efe8650e0bd2cb0a6d6fd033eef9b34f369089c3e47f73e66809a7ecf394fb8c0d6f5ec1da42aeaa978ec2f189774f30e62034b03ff541a46405" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "1cfe88883741c7f2b29424aef0283074c9076f0b30f8c57a3e9e3f82b36faf03", + "excess_sig": { + "public_nonce": "480bbbf6c5d6519cb6fe9e798161bd12981679b6c16331e692534122a9c8bf03", + "signature": "a1126556690d45e6f6ac52487679af1a31a407fc60b055a7cb69c25d5afdea0f" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "689c4beb163180d3688fd885d7fb137eee39c331935147b732e8fb71f84d7756", + "excess_sig": { + "public_nonce": "aa97bfe4dbfb0e2412e3719e583d9ce02ff3ae055f0b5c5a7cc1e84a125c7335", + "signature": "1d145b87118ec3f6708ab194c93c9e81455b24048829835a839974581129a605" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "8611eab8c05c5fd9300b6fbbc9999a9cd89d8ffce57278b2edd83c7b22ad2225", + "excess_sig": { + "public_nonce": "0e4dda86ba2c934db3b8c449e0c63fafcaf3dc6913fa8ef6581367d8d27e1c0e", + "signature": "9f729e1566216bdfe5c832a91c85bf3da7d261190f39ba476bae63fc37f7e103" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "9255c2dd501b8d0cce21b7dfaccb72ed078762ab03c8b4e4a37aa5a7e4785f18", + "excess_sig": { + "public_nonce": "5cb159e6c97161513f74a30cd9a6ed3831317cb6d0b871cb632c0dbd45094b7c", + "signature": "f95673e26148b5382c4a76d8fa76d9b4392503c43448e812788c336e97057101" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "dc8f29a5433971cfd78721bb3100a389d52b961f8b4e35d6b7264f52cddeb065", + "excess_sig": { + "public_nonce": "76a77dd9bad1ad62e9931065aebf2fa965dae5a2ce862662064d5f9fe7f6600d", + "signature": "815bdb3c42ad61d9bb1b558b186a80084309dfde8c21db8d28011df23f66ec08" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "de0c5e1846bf32e5563200a04112e395bc506e94c05e7c93b9ceba5284c84e69", + "excess_sig": { + "public_nonce": "22e7f4c392c93cf70e6a7b5b9d10f4b625cc26097e0ab78e17b693a3f6801463", + "signature": "99979fab99b1c286ee3677de2a95d08b4358c24567d4504c65d6d5193d90db0c" + } + } + ] + } + }, + { + "header": { + "version": 0, + "height": 49, + "prev_hash": "8ed0b72c735d7b2711c69f6d9a63d7c44ba99208a2f6c90e5ba3647717a7f89b", + "timestamp": "2000-01-01T01:50:01Z", + "output_mr": "e50d6bf1edcc47e9df4f9046ab507f6ca315ff26839ef4a1e3982a40dfc0c5b3", + "range_proof_mr": "ec10f6fa7f2e826def5fc0769ef5622972c3593ec1de0ccfddfdf02b3838ab11", + "kernel_mr": "fd0731fe5642f940318dd6073f18b09c9718d161dd3c239ca41f712a51c0868e", + "total_kernel_offset": "b9bbb0a74fb1edc56aec8e40a0f9c3e4e70c6f14f956919f72dcb82b5ccb080f", + "pow": { + "work": 49 + } + }, + "body": { + "sorted": true, + "inputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "36aa9371e33da44baf5faeb65d5ef0b5f88c814663b5fee6061d492112efc76a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "62bc13507f9c05e5e39a28f6b5a0d36c6ff0e19f1f0604015e8aab8d6411eb28" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "688590b9da63911958234da5a916495e44c21158b3087899bc339411d10c271c" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "d4355fad7336e1e2a9ebf517be28246b864ced7de46a010d1f8c3ddf09a1ae4c" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 25 + }, + "commitment": "0678728e31adc97eedf2840ba93bb4d7fb9a17c55a60eccf5048dc8dddbd5148" + } + ], + "outputs": [ + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "34c0c7664d9b8b660efcee5dfb225bec4b4e26c4bb429e041b27a4402b92574e", + "proof": "1464d2f266688e3c4986970f5f0375735e835cd67ecd07e85d5558eeaa8a1d2aa02bb5c3f240bab87ce859d28cda9142127c9e8a01f6df5a713a2acd5a12af3c4ce9dde1d4069cb36f19ab6be043ad65bcf41483261edd79cd7fadeaa0786f6880834d1e5ef74296328e1fc66f438af649ba539c5f7830a61178e5696b55200734eb8b0cb6432731977229c7932ab170d5fba3ea305f1ef92403bb8b7824d105150a46e5da3c1490f8d9e533b74111f70ef37ba6625a902069bd0dabd091190ee1c3b3d4b249e3e7d375d74ff0ac7669e273e8eed1828408ed6baa1348a246027e15caa8ebcffcdcaa832a1db7c02da6e80e00e3df5a7de5174233b8d90db11df676701d84834247e7537e863104a2f22479ef7d684446c282ba0316a6999762ea337aa6b9b9f27a77fd3072e453c3304511c87cfbb79c372dbf8af847898808a88d709443939ff7174c22eb7fc09dbf2746f80c196a1dab6690200bcd5fd03c1849a24a7ebb1df37f0cc03b3694055a6f8a83da794c8b5c0686b33dde474423b00867717560344768130d18e4f199c57f87eab42b5778089cbb49910d113154d82292c66528523a633d3820efc9527729ddffcada276d63819bef097d49542ea23cb950c4cf032868b2e7724543fc1e447f5cb49f0f0b95c294d8d75fd93766806450e979f0fb1b0a050f4f01de8f1103e1add9cf39449e94b88b652e9f7e1f24f335ef66e9b94b4b8ebdae692652675f34938a93f681e6ca263db18e35d07d3e869ea4dcc0caa59d1d4015ab42d405d463a5506dbe4c5b37536d8d37607e069cae94760032db714b394fa9cb03d6a00aed602c3469b3861319b1a503dbbf491ae5e1cc0bc8fe362f50ff2097e8ac01f0364ca9402eeac0709f3536cad1c4037dc11582dc27535b3cdebfed7859e5ea058e1f709026996b4e16016a3ef16404" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "5af246946c4714d9ccf9b2249ab3dc084da36f4b0d5b421a60e41dfc44600a75", + "proof": "74889c3ce4bce9f499b54851add9017951998d499cb3850ccee5a557ad6a515876a63530b585ca9aba921013a334a98f15f395f721fde0e6b68248cd89fac343beac95db6039e5099194bafbf72c1bc70e6506eb3c12c709871e673c25a29c14ce5d14e8bc9d384473dc587fcc23ea7630db439c5bc7e0f71b7d4d6579c0792f9e7de615b14483a946c4621e9917e78b396e8d13e55d53a3644088f02946fc073bdc03017a850e15721f4b75dcbd7449e486d90e4133f00cdc837014d46fbf04502ea4a5289095d304fbc0344d339ffc057da995b110fdd53b8b660f002c1404cea3ef5143d654e700855dac6220d55b4011db1704a3a33d7651498cc122ae749a9c475e8bd74d88faf7ab1edcb08dcddc25e7e356671c08af523a5ed631d53f3c48481dd898d6b0f2673aacb3dd012190860a66ca850f7a5e332afa67ecce77d475a2364d98749c7f2421eb50f01f7fb9e78fbb6441678aab57257b3821a706d0b361b8a19703baecbbcad4cbd2cbb57935e48c0bd8606749972a3e3e64ac2ae8c5ec236ef655ede8dc31840f0d240b412d2f5602bfb73422579cbebefe9d5eb02ebb2e2668d5b481cb40c64357c116ce9c8da8f7016878c44764f92e9a63291c808fe885a1ccb77a97cc6857343e0283e3ca465065abbe6f1e0bf0a0c39d4adcc3974404aff3de9723e1997455b37745d0ddb7053bf66656abe7670761a92a4453adaa7ae934b79b5ddfc13a56d963c0409aa4f8964a274a1dd0ffd1920a76c46593d35edb2faaa292c649b679d7e29004e7ba4e61c0e1dd4821df8fd1eb32fc179bbe63bf74b48e30cdf9ca20b07402715d6dc0b289c83805dac78a55b40b9011b9a88364240a276d78c5bfe55c430e715179ad8420173580de90b087c508a47ae4bb20f47481c06f0b11e5f6ed9927296b2030469e1d35c6bf7c30683a0e" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "66fa4bb270ab40b59def0871c938239d0b0a0e370f81301bb37436c016f20542", + "proof": "8e2d49f9516317c1cf2d9cd7a7077677d0763c2e77e9371956285d235839f9262e34e884af1323fa6f698f90dfd7f0e3714ddb35bfce6392f8fbea84e244e15c429487b2bb18fa42faa6673115b55cd5cc1f0e57de2e1e2964c86aadcd387805422b65e995bbceea812f6a181ea1cd5f375530e133bab070b05d175e71dfc478439599241934f91a8578261fd1bddb942cd4ec8fa08168ac6563402c3e432f00159ae4e2d84c1512aa86148499855d2fd8a3e216951ac2d1d18c7c2064dec406fbf6c72126fdfef168cc022e104a738ab82fef20a0786f9fbcc6b1b4dfc2b10280b06a29898c801259a22c62186742c44193e92e05f63d3c652acaeca4469a2eb8210f56d020ab2735ed1066ab988e906acd3e4a69ae438f7ff7aec6f85b48287a709f9a48a2390ebb43219ec6e2cc6f2f152b7aa4e2d60c4d446847bb25583deec4f6c3b719f4d5ae84d2733dafdb8516d6a4f27941dfab8388443a3e022a27f697e6ab0b4f0f57d9262211e063716213f27c4a0fea63d6cf65f961d93655368e9bd9c3be3be3a86a26e56615c2a0c6091d8f7134932beb63fd0b30fe4a7e3a32b26dc9838db7fe83c52a647121dadf7c96af97d5a0e5fd9e224ac0090fa569305ba50b0371e8f838eb605e89ecbb502a8d0956d7fed01b532797f8ef6e280310dd2b0ee113a7f167095b757dc884d424209439753289420924b3b323181e4bdeb6c76bc301edcc3f42a48c354d731069a2901e0bee38eff6137951e008fa024a93d9fb22596d77240fb4015cd6bb1009dbebb8249b7d0e32506ebd04eace0c625ca8d0b0b86ecd69e910aafd4c9c22101afb3a8f786d4d6da5535066f0772544dc3ab7f0cc5cd1646ac363f5ecdc12afb8b12f65d0253fe880164cb375f208855d8364be258e1b119cbeaf737e29f3fc930d383c4c4f9484a6b07d97efc400" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "68581436d456cdceb9907cc66d45942453dbaa6961e027062ffeb01c3e01247b", + "proof": "787d683e24183c679f72e47192ed353db43dea3c6570fe198077f33f72ec207b56e280aa1ed025328df1132408d6fe5003ac7a915617f795c238b3c1a771c51748df05c61b116ad41b6592236b0f6ed4bbc042f2459951b316e01451ade41e2370e16a3f5a7bd3c2f001849206c900a024de4725e268e32f5fefe58e1bd192197bfd4303738224df4027270add5ff1e94f16a704ca9dd94da946f0350eebf200fe5c9489d4d4c4c999f9d14077475ccd224803c8a76c39eceb8d4e78707cf90fd1a2e335824967aca24d3cdf06e4b437d9e4c49ed5eb5011db6452f7e504310e926e5fbaccc2ca0777149b90d09e4acdffd5b1d49bac67929f966b6e6e9ef3656e69e96d4312ec901d724e31d75065c03f0e82c7ff1848f2bc2707a5ae71be2fb067dcfca76dafebfa15c9b92244790db26310517eba6834f9ec9db8cde2394e3273e0a2696ef05b434cfe7c32580f255047cb272007e64c4f7b9ed9bc6fed48f80683ec0ed81c8befa9078c079e4cf8ae9ebae6cf18636e2e93cc042cea934fd67b36aa5a52f1b3a64236ca1df53a1f8c532a3ee5152665346887368bc1c349d2ad6e2551a345975c2cdd186cfc845dc52b6fb310c9712b6afd42cf48f9dc618c4a3df2958859054abd8f7ecd7ba6c2c9d78ab138cdb2e7ef40f6b4164e5c3faae1ff507f8489f09fbbb0ebfae97ea84de47a63ec08b2f32c3af442cfb0e87406812fe63f50aade895b37fc4f8a9be7b6c3c27b501ca4abcb949cda9f67901982fd75f2b0393f143cf2785a536372a502740aef04fc6482b129dbbb31fa2149ae6575df9b1ca819bd7b9511d36c8e80fdb5a2b177f904cc5b6a6e520fd06e5086a93f65109fc408460d4acb87403d1e66e5b9bd0fb2d138638374e03785c90d1a71500b0d7e42aa3f886d6ca4f691ee413a8d70029c5019bcf45abaf8ad6e02" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "6e543b4516f3fc0d69f0b41eac8749f94adf229df714a192c3fc3cdce943cd00", + "proof": "ec8ac78a3ce2cd85aef458691471c2ad1fd431adac2ddb7c834d3267b0d66e6dd8ce3b733f5aa1dc940ddbaf18d5dc3d5e6b9accef3f9ab6b8b5b6a9f72b94492837abcf927d00e04008d2f84251bf700a31440e801b1118650fcb678f2896761a94bdce92c1b094c48443945f268a1177d73f03127280eda607f728f16d4c7a0a08e6c84826e2b74667b44359e9bec497d57699e38b4968c8b5d92f3ab9340f225bb8818906f0341129031df8787d1d62638dd491d20b9d5b1569e5dbd9b5099dd5b07997f22396cfdd13628514b926022296c3e3f09fff859a3abad2d19202c0768653c03e66a4f16f3ed8724955d1c52a3316073e275fd2eecd5c7328896960db0ccd1f65d0b665f3d5d45ef6acec7b42645e7092af0034972d814370ca2f086ffed548988ea8dbf3d7cd77c852b416664d4e4bea90da80d57ae1457fed27f29033419d039ae78809f7b9fbc202a45befd644fbbc164ef49766115941091364188daf4b02c406664a8a02c0f4d67f0afccc7eb175e82a8e78680ee71062620a46e63bcf03d353a3d099273a972a6ad8220bc251020ffd1067c9f3c0ef8001deb203c587478af35b5b4fb18c47592a312afebd7839dba5794c1b307401780b04dc9a8517258f00513ca9767ba0a82adb5e0456ab3eb58c8a371c125ff1b47d06880fb5b8e1ccd69cce5d1175a63a0a7f597640e5ec91c894c9e0b0b862176d60352963a9d0c172f1c60d34d6a814959564d77f545d99de98f52ac4e8685f039cdfa1d5ee74916406ddf884ea1a2493ba5d53e3d44e0ac6dfe8ce7478845771d0357f93910243677c40c8804836b980fc884dde89a48e4e9731a8b5c7e18f1bfbd217d19d1139f67d0f9b150ed15a45057e31a73d973b4c978c0572af7bd80e4d7dc2450770eaf1e63757b340c0f7850ea337a9fa6f8ee76317fb3ff8214d05" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "7049ee5d4ded7b9c8cd7557215afcd812d7173821a34c14a2ecc0adb1f827308", + "proof": "9a2d598974fac648f2ac6a51ebf465d611b4a9732a6cf3a831ce3ebb97fd9060b8c8f4138fd8590d6ff8174b1aecdd8c039c3eac45422126af0fc39cee22986606a9ebd08a68c227787b4cc7b4bf86d0fee9389ea695bdc3255d98b0f4ae1402f06712c42678e4146e99f570a82d48d7292e688f3650c14d2f1469ad304f5c2aff7140dd3e3558b63a011321a14d3e9fcae238c85e7a549898da95a1b17fd702aabd8d6f24888b1d027bef80d3a65926adf25b50e84919082f440bbedd71010dd5ddc8ab723a00c428f1a5e5a958ef74b90a4cdd2d6e1952410347d9aebbc909a6fe692eec329a64a8e20d1c08f33876773a9304839441b64bff2b18785e2b66da3bccba6eee89ba22d25c6abb54c0ffcf12009556d4390a8cf31fb7fca12b7c78f18ea6782e95fba02c2b3ca46aa50d64ecb0ac55e2026de96d120c4ed8561b30bad3ec1923f1fca3320ff33bf92ad37d297ba972871bbca649614346ffbd7764d2ab25648674f4e51aa4b70eb5d0eaa2b45fcc649fbbb5c77ff5feeb6979799219b09e64a2e88e85b40530f60d8736f6636c821d4597dd93e3e7ccf97204664e3875baa8cbad824d8ed302a3e69d6cd1cae015b093fe8e255fdb57eb6bc51ae8ce93da31c6e16f26d04e990952c3289e177bf810215731163505152cb1360524ce05df9f1acda218445791589e0efe538c9aac90a488ff41b67a62e2c5a07082d7f4c35b41f1f6c86bd92d4e8d8e3afb1052213ab3a891c3855e3a143e8721b88b6dfe0bf9c755f33abbfd34ba5c05105e5d3e98caf94b0bba806f72204a2a6a5aec422a64aa51ea0008ba677e5d0439dc3852808a43802bfd50e2d9e9b9577af86eda8b4b45835023ec865d03d31653807c1c726cb742a2a9ddc7fdda7c0790a80d1941040f0e7f6e354e6e95949a4d434f9cb9d63d36c00b5255996ec703" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "b29bb8709d262ed0a773e4f34541046530fe2991683e5637e63b11f557a1997e", + "proof": "c0fc0996c659710849e51d31253c480bcc11ca6bfc61926face340f0a797160d7e71510a4a9b9ecf2420edb0dbb701ef01aead45e8a9a665482a4f4716ab51011ea7193ec3d4abf46ba03fd7edefc5d6ee72a17ccccffb4c20333d8b94953f5d5427eef04fd2e4dcb52b246777a52756e22667666a86e31510e20a339d15de23e0f55eab8e9e569230a8a2252bfde985d211940e5ed49e5081660d9cf67eb00334704638eb9e6c2ab4111aaa6f6252f4f9a7ae5bb6465668bb33b19c4167780e8f9f5329865aa0d676b2a1e435f4d4577cdf6291ab84211eb1602a7247967e09249d6f68157c68ba73aeb5e8f64ea8cdf0359eb8a496e0b0e339a65eecef75692c73494026a3866f1a4cceaa5efb65cdd4d68f237400aee0ce7a6cb0c6d8e327b42cacaae3627bddd775cf7fbb110b4faf6a5737813aa973b549a8f7a5657077188cac0e99f5a8238ba2497666aff6fd72abebeaf5ef4f153bff31057a9b377918e9b80f78e14c05150900c06cbbba9fa895b766e0c84d781554622d6c1b3c189ec80184a7696d5e252f4deecaed952cd30de37dbc6752e60b250bbbb1ff1b15b88511fb996ab036d2d22dbd9a24afa2395ece9be6cd9cd5600c50e6b0d0d65dda14e3651fa12888dde184baa8bada38189f38b22061c94b8bb70f872dab8619c476f5e0cfa5caf3d720099a2fa7d5713f022e28dd4f35ebc86574b619cb876b8acdf36f423e1986d4be5ecd11da14dff86a21612a1cba5c3d74699590b4ae72402e9b5c93c6905678eab1a81c8ed8cef25ef38685c30e41732ce83468354c228edbb8ddc689fed729083d219e8e39ae2309c92b93656e9788e2018e5f754311359c1e03436b161e8bff55eb2001103b3981074366118d2ee45cc0eca37e6f090426979d0e39f2dcce232510891c2e77d7e0a48f27531438c4f49a3baec79e03" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "de50737770cf36d6412301899a9427ad6743f09f87fc96b57048a66bfc72343a", + "proof": "80404b1121406d4a585ed6384dbf7ecceab83eaba60eb0e5c6f6fd4e7024d534aaf91cd07374f8d844246f802d9875a05ddfce968b38d12a6c440af5ade67d11881cadfb7c18e32538a90581871f36747bfe6f60f51390e0639a104ad310d86d38fbb21c99b9adf6f30fd95e2a9f78a5d212a75f34c0738156f8415f2abd4f61111e90bb6e1d254e202c3b119769fbe1256d78003f47fecc8d61add9a7cfa70bbf185225dc36d0a4b00cb4761f14640eedba908acdf8ece87500349aad59bd0e444fdfa2f4796eb66a971de3c860fd696c0b2bd1b74a6f1b6960a00a71c062078820292e46f63f629d1012ec9bddc4c1d454ff0d3fdae5824ec6dcf5383c5c33e489c4186a8ddb78fb3c206947264e88d5e9ee83a634cb24571e3be35de92b66b6a4cdda62944d18edde86535c7d09833cc6c4268d4be06b5dc89fb3102ab8636693d1345aedab87635e5e2ec9a0f047cb6e04a8abe873a68647ef481c0a4e5eee04c38935a4938bbd612a28bd842702f33ecf53cc00aa11033a04b081eae52ddee9914ad95133f0610f866640d60c362799c0c2ad09fdf3e308fe6948c5433452640e8b52c2ecf0b8601e3dffeefe1d2e63dddcb216e03e532f19a5da28df06302151e28c8e2d1a83c8d874269e0e6fef2dfb92aeb84c260fc59a0135130676ce0e22505cc5e3380d46f345aca92a2d20f801113e2754aa01ade05085160c304642cf6dd25515488dbe844997676189f1ddb45cbdd1456b4ec2e674abf3886f88c5c19dc2fbb0f6b1a81c085e2ada1730f0d0ff407f81ea75f12c7b80547b0830bbabdf68597c453594855054dfa587fd2cbbf327ae5ea137e5b033eca3f803fc95e1f9a07126768649e726464add9928ce2764bbbf962c7a137d61cd91490cf28024b5c28fc5dab83a8fcab402a7218ee6b3734cc1d8b23a0d3ce64032400b" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "e6201e1a7909d19be521114429f15787ce5a4de17d87f677a771139000d95b27", + "proof": "d6a008156b6269acb102cb3d2440ed14106a67f1feac64939b6d34f9f0b0c94ee29970b3b9e48dcd7bf49612b78fafa75dbd9a3e2bf51a88dae4a45f68d0204500c1119857f1a4456058aaffd0f0d4d3e76df670398dbb59bae35774c88f1442aa27b9cb45ab39f051889b47bf0faeb67bd380a83cba943151b12b93c193047ec63f8e81afb05eaee84f047179ef298a55b262e2a5dca0b3fb3db09ca3445e0df7580a465c943d7a53363e3490fe72e8307d0796596d1b7e8c14b5a9f53a2600a9ec9640451281b28cf36c77519ccdb7545871e703a83a8ca6e02502662fa30ff48bb3a70b558a922ea2494a05f1e626cf0bb4e1cf71697ace14152f7732507282d4125bf3bb87148c014b8d659edaedcbae46bdcd9fd0c90a9c563c123e2454f6312b22ce6a042d73611de235b1cc36549c87c0e655f66a48ab67bb0e9f496986972a6d55b2f8cb0a2a64bbae3e0b98e46f7e3b80d272ca052dfa431cd980736cd303514c2e3e75da91952ebfebedec0802125daae6706346e96b52f6845f18b85fd1d8fa6969bd53ac8d379e83a893502232c50849e899105a5395b2089932ced591918e07c8029e8b5738e847c3c645c4d56ff85ef59a9e362a592eea8a267a522d3922407f91623bae68737efcf592a41f8dc0d94c8ab50651899cc3ed2a9c360c12b7f3e243f91f6bd4875227766bc84b4e826fdd8a71ecaf027c7b320be89abb6ebe1929063a935167ece73710762e5e9abcbbb8f9ed5851bfd8f2d84c82ca125893a2721a10a57637ad26090161c4b1cc8cccab4252706df2db3c4c7c50e17e3d900550f1ae82a0535ec5e9f55d106d2e642e4648377bf6be93729c4f32604608cf6a86118edcf3383f6ef4f303594f32031c18ab87fbd32a819527094ddb19b87af2e973fc05f9fb62d5ed43e063a0e0fd68ac2a585bf387eb2b740a" + }, + { + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + }, + "commitment": "f0395085c7c4689eb39ce1788899941589820b297ef9410131e3bddb7bba3a27", + "proof": "ca811f3e583bd0e37b5d4be57d55d858a6d4f59c0019263827a1acd395c3ca3c6099053905be69e94ccb82c5ffe038b62354db6b263a4025c309344b4325125bfe05b2842636c9e3866a12a1b9556967df351ee3c5489448dfc6bea9bf5871487ec0669098e7f2b23d5ec2d2acaae30302d383d29a985a5a3306dddb04823a2c02932f07f3318932b4207d93dbd0f7fa4af33c2cdedc9a5e4bef8d6772f66005986685486ef1b2462e93a248641cd2599d980301a29697ac38214e5a42504a03cb6fa6ae14fa535d0e12fe5f62b2dba8a07ba6757a1fcb27ec59fb46b2c1150cf2b10542d49188c3721f5368baa32f9ab1c23ab534b129ce5674351cae10ff0750f6c968dabbc1293f5fa5d7e8650ab7ed83bcb42cdb2c1ccba39fd70d6bd07d681290a1ed9368bd0e8bb6d1c62526048ea933ff530f43e286296fcfa4c27d0e48374f07109391b57426664b679802addea4570a660313815254a808bfe3587d90060b3ae743d6766ec204a20a071ca74f3147841113b8ba27bb296fde734e468654434b0f990107c7c63f481c856acdda291f8ab4ddabc659e27b2ed2bd4163aa964daa11fdb94c31d25dc4c8ebb9f34af2b33dc02f4ea6ff33207f8813e1324ad3dff56fdb2a73dafeaf92aea54b1597ff3b98fae2d52abcf1f04e97fd712fa4104a01088a2d6e253ca6b22aa4b10052144674c3b5c791e7931e66e4d99270041194f2f8b660182154fe251ecb9047348986dbdcb5c20cc03f590f2465a070768f17b91c2907a371e23f4dd8d74c53bfebbc042bb02d5e4b41b01595b3ec4e34c6d6c30e8958a47b7bd50d01a513cf51a8084734d98c8e5cd4a2471d66cd43316eb316b914511d125039ea826d440b972932629a97dc4a3f314ceca51eaf0bc585bb3a90959b07ef8f8f11b523d1308467916649c7fd8330ba0a5948f9f605" + }, + { + "features": { + "flags": { + "bits": 1 + }, + "maturity": 50 + }, + "commitment": "e043d8f8881190847bbe7d295b2295cc3adfc2e9f26355863698dd1ef9806e6c", + "proof": "602626b3e6bf5b40dd6c6b7cd9d7ad9e9a4989a26e1f32ee248f7da71dbe1b4a4cb63beb6c020efeb4d2e9e67fdf3522b5e2dd67a0ea616eafacc32424065d7ecaf1d478a53acda412ac0ee0dfb1c9563af03e92ba9136149aa6cf0c73d9531426f97f7fac6b82a68ed0dfcfc63d09091186129bc7de2fe2d1c8240b9a3ba705eaa394df865eef6560eb40295fef6d032c10a803c42a343bdead10157d8be40194794706090d55bb524caf905ac172c5e3bdfd9e5507e29bddbea5b715bb380ceb02f260a4532068a2fcc85f54f712657741f406a24851eb8d68608c24fabd035cadc2d023c1a5f806aac4372ae103ed839e72cc3496713749d9fc48ae920b67d02d9fbeee5826db565679e4c4b578e95cb80eade02bc3a794547a737d363b4720d151b3be2a7a7e74cea724502f7f82ed42ee7862c8f4aeb88a6cc030be812284d038d56dde80c52b381518b8c84fab8ca06a909b2cfb69c504343646bb86514a56d6cfa8f68a6c7e7fe0988d311632006dd7d009aae2718cbcb9608837cc68809c664078f7c85858dc4bae7fb6e54df8b179fc18ef7451d16d81ba6f907843a4eb8975e8c8ab79d372bc35544bcee8ac01d297d1c1de771bf0d72aefacf25722b51755ea4b136a0eb0979255fa228c597b43b6614ecd15d744df8ffc7fc8693444adbab11a8a2b9c5223f15dc15c81d1ed7740fd5c86229ea95c7ed7b8d922766aefef39016cb206e9db91e9dbf71349076e27bc27dec8cdef66f15e0690098e3b533e444dc61a6d41352e70cbba889588d929d77b4d3f3a59617a1c563c335e178670734af05fc8bd71307f17d8f666a3736a5feaa69af7fc70c243d9e45d42f74f906dbc00d1849d0d710e2fcc07bd15002d0416bafe24d24ddfb6b66804537723424f1847208a161afb59f0a2c1c6ae4d1a7ebda6a32f0eaf0ced41c907" + } + ], + "kernels": [ + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "0a5aa40175cdc146d2b09719d85f6b4830f9c62645831f451975ae3541f87c0e", + "excess_sig": { + "public_nonce": "845e1d72172bd92137fcdda8889ad8e6eb82f5939e0fa19edf8c74cdf5437758", + "signature": "69533ba9a897f5a90256ec98dfe38f76045db0518047114f71a479f9ffcb4502" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "3a1c40117e90452082ad303937f65445f8d81f45a75ac6ebf9b20b4cc420be4e", + "excess_sig": { + "public_nonce": "82a4b02da53aa3c46dc92d2cacdd2470738e698291759c440c09fe8ebf9fae35", + "signature": "76fa18e9d4e52bb0f817e2e7bd07c0a634e57bd2b2fc973ddb5980903f7d9804" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "6eafd3677601df609089eddd5e15dd3df580296762497b889559261ca9160747", + "excess_sig": { + "public_nonce": "26f1b29e55646918de76412d6f72cbdc045deb8a40c6919c6e1024ce95f7700b", + "signature": "b65f4df9da8337f6d35a94ff98380fecf2811e31f1070580bb7eeaf43e34eb0c" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "cead0b51a61fba3acd676391bedbc2059bc5b18170a9d4a96fb8c54c0fd71306", + "excess_sig": { + "public_nonce": "94baa0ac78f37da3581b0a31e1c17843798a297f74ff96b9246b674c63a06b05", + "signature": "8eb2ba1bf42a0ff9dcbea30bd43a272fc86509384f48c2569a462ec4ecdfed0d" + } + }, + { + "features": { + "bits": 0 + }, + "fee": 181, + "lock_height": 0, + "excess": "f6d66930cb58f341b7fde8a8ccc9abd71aad2100458299ea89630c5b8c5be436", + "excess_sig": { + "public_nonce": "04d3865501e4a493eabdd0fa84dd5ea8c5da5fdc0c59f6b77e730a4c12f1b215", + "signature": "c62c2644f3761a2cdefb61ea1db1fdfd1fa50bb625fa84e0f1059cdf650be20d" + } + }, + { + "features": { + "bits": 1 + }, + "fee": 0, + "lock_height": 0, + "excess": "8a6d4ad863ac3f8f07ecd1d31a0b1d5d9e3540887fdec6534c0824ee128e7b6d", + "excess_sig": { + "public_nonce": "f8b188e5fc26b35309277e4b811b4a290287518358708c0c88908175dfc32b77", + "signature": "805ad61d0f98f8d978aa8cbf55b9af6371b0fa9795686729b1e1154394caac0b" + } + } + ] + } + } + ], + "spending_keys": [ + [ + { + "key": "b0bc8068b51f1cbf82a22e8c5299442b4b5b0566712511373cbec715e2a8da04", + "value": 10000100, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 1 + } + } + ], + [ + { + "key": "e3746cd3107293a70d787e67f6208d3ff02f016d382a03b3e4b39d5d598c1c01", + "value": 5000050, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "087b65c95c89329819fc21aa97f750225123a550450facc57418a19a8b06740d", + "value": 4999869, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "7227a8d8d14b86ac8a98bf4084d5bec46fa681d557c7a24cdc3e61563881330d", + "value": 9990281, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 2 + } + }, + { + "key": "a9205e2c9000f26865322d157f06089a234e1b3807d9b41b6d01442155c5b805", + "value": 2500025, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4fb4d66aeec22b34888de844884818e3d5066d53b6554d34e56afc5d75244c02", + "value": 2499844, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4103908accdb3d1db3d04d7c6c770d5c48ac318785a53bddfc06c3feaeb06a08", + "value": 2499934, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7e05f69b4e2f4101dc44687791550e7df787678b9135e7fc4979a9fa083def0d", + "value": 2499754, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "e392c8c8f9c9ff0ebd280eacc5e81029191ef3fee5357433cd704bced00e1a00", + "value": 4995140, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1899f78556d2e0d3a0f742ee20701d5d746cb457549b2e112dbfa7aecb721f07", + "value": 4994960, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "978d60473cd41beacdd95db532c2b7200c2ae86185f2ae6e6cd0e60bb53b9507", + "value": 1250012, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1930367e34ec7e18d758f8223f6b064948dbce48934165d6fa5e1410c66b3c05", + "value": 1249832, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3adf9c029b06d0a41b383a52b31d69ceb88c54d054ff6cb8bd2de18da51cc708", + "value": 1249922, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "bfcce60ff0a7a51c8779a05a04ad976e9764e2eb583ff64493d430510dbe030b", + "value": 1249741, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a5e269b2ef5827a5c90830bbbc1a195337a26b2f3e4a254d20b8fd4bacd0b00e", + "value": 1249967, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7ddb65c48111602fdf7a97b91e023b8d3009d780c47e8634dba42156e700e608", + "value": 1249786, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "57b07681c4f3eda8b1b72d164542575b570b47bebefc21104011b481c69e0b0b", + "value": 1249877, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d9c057a317439be0cf065066503ae44cc9a859196bc8c24616ccdbc4dee62b0f", + "value": 1249696, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "035a03bd70c5458b053003a89ace49b7fae107e8bbd39e543dcfbb1dcca6160a", + "value": 9980472, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 3 + } + }, + { + "key": "3ead8eea7941c8d9f024071617fcc49c3513f13f822f9c1b5fd47c7c2f470106", + "value": 2497570, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a776faf6d8190729bca41cac9b5163d8fd46d3a85c03f55ea3bbfaae59c38904", + "value": 2497389, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "edb0ac227a5cfb83bda1a6e8c4aa2b2a73692bd3c7eb8f7f5b92fcde983bba05", + "value": 2497480, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f2399d65a21f06e90df12faec87f83c71bf44eebc15186e980db876e46438d04", + "value": 2497299, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b890864c7b52cfe71fc039dbba17fd833c1878cec39692955cd93f96a9a6c10f", + "value": 625006, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b53676847a3dfcb83c0dac191854f09da0a33a454f46966c795ff2be89048f07", + "value": 624825, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "72d8d21b229819fb3e3332105a30d6c1a91ae49c84b1347b8ab5f262b30d4d04", + "value": 624916, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4eb89eb97faa91b4da510a1fdf3be28eb80b53f939a45f910978ecf2ee84ce02", + "value": 624735, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3a2428f40a0270ec2cf9c2ecdeda84697fb251d476cd2eead1c6d961d9d5f301", + "value": 624961, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "2ec13915a03a97e0b9fda4efc839700efec28728d07f0c57389501e45f57c008", + "value": 624780, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "e3f3e28d21dd9343ceb8157c1ed52872b7fe8154ba114dc45fbe544ce979610b", + "value": 4990236, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "65e87c7454a9a027514086beffb9cc8faed09c2cb6b514c509489f3ee1bf940b", + "value": 4990055, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e72d65303719223d145040f6f8289fb5bf8dfccddb5032b591eb71a281691e0e", + "value": 1248785, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "08766331efcc8d1220ad081fa743745f406022e5aa3319d3afc7a0a9d22bb101", + "value": 1248604, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ff7d0d1f0b2ec0e13b47a757ebdef2d03110d8851f391aa74b9e3bb3c2c64804", + "value": 1248694, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "26889b78f6bc856770f8f6a5572bd5965e8ba9965493a9ec8ff13d568860760c", + "value": 1248514, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "067ddf811cc5e8a6c805468dd98e911d7c360ac7000497d95688ebaaeb349a0d", + "value": 1248740, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "05c645c75562a6f53473845e7791abba3da5d6dbaa5eb638f6c902a602f3be07", + "value": 1248559, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b6bd45ade6a64d1d5bdfd4f7eaedc344fc243e3ff557f74fedfda411ca952f09", + "value": 1248649, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b23e8a994d23f8f86f23f9c5b7edbbff8a5b3a5ff6f6e99e598c2f615125870f", + "value": 1248469, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "ca776b6ac1bf81590df09df4844f209ca65f33af6f0874f019fbf29089a3d506", + "value": 9971034, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 4 + } + }, + { + "key": "5246990a86976a0e1d47feed1a508ca59f07a3bea747761673743c66f204420d", + "value": 2495118, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c84d9a529c7ebb950a43251a947ba9efe9d7c6970da2a847824ac9653b52280d", + "value": 2494937, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "18eb04bcc5ab9a7b7555a153a33c3adda132771212bdac1a1ff3fc01789dcd0a", + "value": 2495027, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "98e53533092932962167b877ce0c65cba3e9ed45c61b7c8c704e011da362fd00", + "value": 2494847, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "73776fa15dc3e8a55e2291b92f3223e5e3f8a0042ba590cf89991ec872675504", + "value": 624392, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1df4d59fbd63d03b5612c805a3a4073a899e110cb09e40fe23f8218d63a3b40c", + "value": 624212, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "54a0c7288ccc162852fc6cc563bec6b8b8a9ba6f8e6630cc90cb6c06dd2f1101", + "value": 624302, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "027bb8781e2289cf648bd9dae544b34f01757c16e3ba39c4298896cf59ed5508", + "value": 624121, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "297d1c523d031ec1ff44af1f43b9d68ae51f5523cff5b2a5352a11b0e98ca401", + "value": 624347, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "05ec2ea57de98fc096642300095587532f4c159a3dff73d934897590ed14aa07", + "value": 624166, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "655da97495bdcd839e60e0eb99aa604908e9ae568de8c6c620deaa1b7504e007", + "value": 4985517, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b9e4547bbb506f1d15f1950c7c80ef42a55d86e1979597f68437e776d8a99e05", + "value": 4985336, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "21bba80b67070293807a53c2f781b0a54d6ca74931101b7d02ced22922258905", + "value": 1247559, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b84d7c58641b3968f38d1b44012e84facc14573d5509a491ac3f86689d310f06", + "value": 1247378, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "2750aa1921b2372c5b3f508505e1d24c5ff3605b869ba8c6739d5cac12dad50a", + "value": 1247468, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "5563b33f7bedcb908e2eb0f27aeb934668212d4fddfd87022355c526ca1ad909", + "value": 1247288, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "2b2bb82834ec9a3236eaafb550145cf9ddf1f6b1bf053da9106672e8b8b9440b", + "value": 1247513, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1162d5043408e305f28c46048762cfc08b7e55c2860057025d34310112f8480a", + "value": 1247333, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "439012eca13f8a2d636a0dba2e9504a6a2defe533d35d8396d9becde0a92330d", + "value": 1247423, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1aaabd0c5d0e6792a05988b29ccd1a35d83ffb441bfc50dbf97062cef5dc3505", + "value": 1247243, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "d0e28be7f5b7a860f9784122286988e992b0b9947e85d281713729c920a4ff0a", + "value": 9961064, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 5 + } + }, + { + "key": "abeaa604a50a4a24ff5cb8e8dd1fb701d183b0c4259a5e82d4bc130a0c04700f", + "value": 2492758, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "023f151560589bc6baf11cf84c4b9934d6114f68f477b255ceead30266caf30b", + "value": 2492578, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "dea87df985a1cc846f275f0e369c27a1cfa0d0106b33590ca58cfe6500ed6f01", + "value": 2492668, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0de721c2b3cf524b9c5fd980d59eb2d23c646941515fc83d2ded91b4ee269f08", + "value": 2492487, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8a38caed795995b98f70126eae2f2357b29fb9c1ac739bb171f4578a30bc6708", + "value": 623779, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f901b3ab57255363e4dee80595f394f230e7fd1b4922c4c3e8ac4ac9ad28160b", + "value": 623599, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "bab40ada2738168f19f54506a79ad26004b2c1a37d58bdd9e9fce4a21708610f", + "value": 623689, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0dbdb525768f3d321f4060b5de8047815fccaa26be424a3efe9cbc6109649b08", + "value": 623508, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "5ee9391234a97d2fb7c46d66bb23634bd74bbace82f5b6625631be5df1b22609", + "value": 623734, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6ced749dc4edaa0289628a2207bfe4d158d8f776d544257a864816617481460f", + "value": 623553, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "83cc4554bb42092e29cb87c5cdcade7a7d64948b3312ba1be1cec9b96748180f", + "value": 4980532, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f1e23ca675044cf5578d06426641a5e1792208c8b0aaa638803dcf0f1b3a8b06", + "value": 4980351, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4952b1dbe8099600edaae947439372283edc9e29d5765409a87c994f6b960f01", + "value": 1246379, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1f843152d35a2f43778550fdafe965fd6abf5fee2a509557a9351c6a60d5570e", + "value": 1246198, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f16d4a74c3988d9cf5a171eaf53d77c5e3f73c7269c0dedb4d267fed056ff607", + "value": 1246289, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "babb054a4e7bf82b2728f9a7e40cc840827c5c2fd9dfc4e477d1d8c53bc6b905", + "value": 1246108, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "68f4c8d1dde03dea1de24b1fc630d0c7a10bb145a58c6872e17ef20257341104", + "value": 1246334, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "40159c4c887096f0c11424ea8c04ede2c1adf37575fecdc601cb855671eb150e", + "value": 1246153, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "342962de9efda18568a357f20768160aee80a6b884eeb96899bdb60151883108", + "value": 1246243, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "eb3813c45a901c4380cea9f7e0e2a10058b180564f1f01f2b2aa8fbe8c7d8405", + "value": 1246063, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "662f3ff8f752ed2e08476e6105a93d2ee5341f4b826a71d85719eec57f225b05", + "value": 9951104, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 6 + } + }, + { + "key": "d4fc6dd5fa7dcb57090413dfd5a136a090101def08a71ec0adb69b3e41893d03", + "value": 2490266, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d8f13675d661a8ca77e6fcf4058a3fdea48f096c66b210904b2c96a38b63de08", + "value": 2490085, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0692e36d7eb78dd345c3c820404960abb0e389f72cfdb69cdb30dc8b74bf7301", + "value": 2490175, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "87429420efc93a63ca2e3eadfd2d4dcab4bc531342a306afcd435dd9d560eb0e", + "value": 2489995, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8d7dfea4de8808152a22e3aa077d542217350cfd38717cae4df98425c5eeca05", + "value": 623189, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1734cca8ddab18a403bfc4be8f1b2cb5649a3c8cfd9b3295da8b3dd7f30b600d", + "value": 623009, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "607ea3ab8485987cf751760c545a6b556bb155a5038ddf0d943eb2d9c81e210c", + "value": 623099, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "411c519ade9abeaeb32b699e7aaebaa1a934204f7f43bc8d13990725b2d5d70b", + "value": 622918, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6fa1f10aae3d68a7b09a85a45935bbfed78623775c6db2a3f3f760cd28c57e0a", + "value": 623144, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c94bbf757194417c054eac82b7b1b4df375b3bc79a7be59b21e07fa448d5b701", + "value": 622964, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "6a7ff52aec05949d65f0803d1114dae6c7ec248c77ed9a18dc3664aad94a5006", + "value": 4975552, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "341e8d143c124c3fdd26219dcad6ed1aecc30c2601f16808dea46179d7859704", + "value": 4975371, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "65bf3866177612e58552a32e5584bf8918216e30232d2946d44b27f09027f605", + "value": 1245133, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "eb47960b4d2ffb54f5267f3fcb14c7c9f096751152af9fd3fa2cc8147fda0c04", + "value": 1244952, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "40e61f97fae867fbe66afe9a6560af1595605b046f88440718249f323316f300", + "value": 1245042, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e515a94dfc580197ea81f675eb0520c6a45eb1a72381450e09b27430268cde00", + "value": 1244862, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f3158e5752aac2866b46be01ee26c61d27c955e722062ecc8d6fd93b95727a07", + "value": 1245087, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9861584ad47a1c1284155e5d121ddb835abb7398704ea9971d9d1a29a1cf870e", + "value": 1244907, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "77381859dc2c60d4966cd5548ef1b34daeb212a966b04fadcde9205983b55603", + "value": 1244997, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "06446b21510e8d11541cab5e697dd85e01838eca7ebd812b6bec317488fda309", + "value": 1244817, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "d46df4e969e7f48f32e011828827ac086e41520f25cd0931320726180c58ec01", + "value": 9941154, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 7 + } + }, + { + "key": "936c1ef60020959a54b054f5965f5936ac6e16436b407d83bfa168b56d199a01", + "value": 2487776, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "762405ef8f36d3080ab0d0dcb2995ec70a22236ef949e1e4e4616d3bf075850a", + "value": 2487595, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f9f41eeff6c1e61f3eb3c64541122bc0c26ad0f1c8728b2a7c7d957bbee81c0e", + "value": 2487685, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e00b2053beb40633666c1f1037742d07fa6358f7a00a2a2db64ca5d21da0350a", + "value": 2487505, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7abf5adc31d1dd9ed0a6b10507ab5a71ac2f12cb55f31a6c4a7c59ecd0007207", + "value": 622566, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4e374b74466f865bf7a79ad777da1ec9268fe430d76b52b3050cf6ab515d7108", + "value": 622386, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9ce3a8ff602dd2c26d025ce25732ef216543b8dd1fc72581312c81f6547c6206", + "value": 622476, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b205a1e053506980c737363e311456791c56129fa7d9689ad63e5bd1b2e95402", + "value": 622295, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a44282fcd739e852136453b4d16b7cb6a1d1b9120769b5233824c287ff443a0a", + "value": 622521, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7761f570c788d97af57cf4cf84bbeda95e5094319fd538fad0920692667c3908", + "value": 622340, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "24c4ca5ab0a95fa632108cc77510270dbf35db519226d6d1d35b19c576179302", + "value": 4970577, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "45e7bf789c75bfb6b24f4df8ed85d2419fd67cb026d962bb2af34fa24bb33e03", + "value": 4970396, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "aacee969f71859fd8dcc189e0ffcd6dff25e4c51109c7696fd9aedbfa8fb460f", + "value": 1243888, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f69e1b6c70f05e382cbac377db7785c98dadf6248485e20a743c99b759cfed0f", + "value": 1243707, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "5099a410cbb3fbde888d84de6ee6cfa5c344b3ba492254cff61612f21cbaf904", + "value": 1243797, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "badc550661c1d07e0d5909f9417c66715fed17dddd28b977fa6d3aa53f02e70f", + "value": 1243617, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "366af02556d9ece29a906aa674d1a8854f3a6150a2b6772e4d2d043b72d66f00", + "value": 1243842, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "edf5716fc5cd40493c0daf19a69a52f46de726ad0e475a3d5aa148b359f43405", + "value": 1243662, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7bd53f71b71449fb3850106e2c5749d72c72953eddc019a5c6267bf0a2c0e10a", + "value": 1243752, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1eb4653237f80ff35132e11b9b3fb0649c07ebbe7a18c09216580fb904ea7803", + "value": 1243572, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "62020d1b252d82ea8ffe75e69076b12ad1c0123d79303bcbf83e92631d28160b", + "value": 9931214, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 8 + } + }, + { + "key": "bfcce96e4fdb8400123612e82df5d8833b967c063f1363d6d46216e6b7662d0f", + "value": 2485288, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "400d36d3f48e6039f2aa885eb2c40f8f15c2e0e9b548a6caee5ef1ce8ac8b602", + "value": 2485108, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d88ba19a77259d05a1d496931a1e3675eeb9d732ca3e5a995579c7060884e90f", + "value": 2485198, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "af378f9d03dfc1b6b3523373dcd16d8faa3a76a6cd40e3b8d5b5ac82cf7b4a0e", + "value": 2485017, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "16dcbca7a7038fed54a5be952c696b6f274787d888b94cb464c43b4b316dae06", + "value": 621944, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9c7c9682303b9c01c80ab2d0ac0af4215b597e6f0372eb3b11d48ddf2435610e", + "value": 621763, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "21b9706d6580e1a0be73ef98e14bc03fe2488ca845b12414c471136c22618c00", + "value": 621853, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a8926db8b997e474be6ae2a64854c047c6bcaa28b591590e0f223f8efb8b0905", + "value": 621673, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b926e15ed88971dfc86f9630c6597579a62b8954b06c81e7712bda8b0d77e102", + "value": 621898, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1943a853123f2588aa133a55e4b7e6125d94cd486bb83f030d484dcc07fe7308", + "value": 621718, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "06bf5a0543266cd8975495d0f4ee4d9102d82f4b709ae053c5bbb2907509f90a", + "value": 4965607, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f9b9fccf0f756cb7bd4534a68458a377fdc0c173028dfa25ad23dbdefca0fa0c", + "value": 4965426, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "cfaed525b7077881d96cb3a06186f3d3fbc5c457e1bd86d1fbc1ba7fcfd9300f", + "value": 1242644, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c66d4af74040bb2d169a6af86e6f73cf5b1de304978d8571c8abec424d418e09", + "value": 1242463, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "dd0832824838c6982f00fa1a5f9cf9536b67f62f0d6e3b6957647a777e234f09", + "value": 1242554, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "47049b046fafef56adbb27301cd3e5774ad9ac186a88e8c3cfd0d0d4b525c50f", + "value": 1242373, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "026154352f95d5d440b6386bba90de722e08eb11960bb3c7accd0018786f9a01", + "value": 1242599, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "db883f98ba7e17da6fef33bb9ac4ae63c646222e21262fa057d61878781ea30b", + "value": 1242418, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8a701e663bb20ccd3ade277e4bebfe8615ba990b1777061fcd6d3d9cf58e7d00", + "value": 1242508, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a225f66620c9c6d30ea0907d0844049ed9c6f6a18c608e52dc33b6d10e04e90b", + "value": 1242328, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "d490127de1f1568843980a665ba86fe416f01092caa64fd0bec037dc5e98dd0d", + "value": 9921284, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 9 + } + }, + { + "key": "84b6f9a0dd779eb9e2a471795be422d358ad82072ad9b6e9d7cbf0f335301f0e", + "value": 2482803, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "dcaf6c35e013dfe1361dd25cfb1ab9922fac9164178b209efb36dc837da72509", + "value": 2482623, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e2822ea40e5abe1db7e1c24e3dac7ee8571ee8181f881059a7666f75cd969407", + "value": 2482713, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a55c72ab033f9233ea636fa479173fffbe289e7367d17fb6377ec6869d02e808", + "value": 2482532, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c2d6273d33bc94d5bfdbf70305e6eacc225bc132298cdc75452019290b53be0e", + "value": 621322, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6cb11d7550df3e62ff9382189deb2a5340e696bfa757ef6a4455ae998e8f950f", + "value": 621141, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e0c4aaa99994f4f9bedd3c8d1eaa80d5fd0efecf21e081cff5860b3f6773ef07", + "value": 621231, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ee984d844349a17015ba4fafbdb1d381a6906bc5c5a0024ed2b058821244f20e", + "value": 621051, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a4f18e4e4f28b350a2718f4ad81509c0d3e121606085e6fa619e883e4402a402", + "value": 621277, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "078a669af4bfb645617962b5c2670a19e71bb136614029f7ba4a5aaae2701f0d", + "value": 621096, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "58ce09da6f1fce9e83eaa5ce25ef96de1a52e7fc3a6bc251ecdfba389921330f", + "value": 4960642, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "337cfdc47221ae98b90d757c82aa1b7a0cf4d224e4791d4c5e9dcb88f094fd07", + "value": 4960461, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "670707bdcff0e0404b66d59fd993c2f45c3c2ecef68180c15bdae5ed1be49c0e", + "value": 1241401, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b93f6a7016458db0134778964455794d6a55896ffe9519720963a7e2fc205401", + "value": 1241221, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "fe642beacb55063126f18bc74da5d6114026dcdcbaf18a29b23a3589057c9805", + "value": 1241311, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7c760fb109178dcc113830b123b7289067efc1495176055abd04f2064133e603", + "value": 1241131, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "00a0b16e58d31975909179b96609abc5d285756241651793ae3cdfe6059cac01", + "value": 1241356, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f2075b1342b5fcc342dc2849c19a283138020524d3b15c2dd5136426071eaa06", + "value": 1241176, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "30f02f4e3cfe5139cbfb81516e87c8a09470e916effb2547d063a95b006c430b", + "value": 1241266, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3b46f4870ce473e2b2ec6b1e6bfee7e3bd13286649680a1d9761632dc06c620c", + "value": 1241085, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "9bc54e20e97c07ff05b04729b5b5272c0bb216e2c6e8d1861d5163148def2904", + "value": 9911364, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 10 + } + }, + { + "key": "e7200857e62e156e35ee094cb5346f5e78e154bd308c39f7dc8c05cf9747070d", + "value": 2480321, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "01d751ada616e750fe3dd484eef8165723cd96a1cabe369e488a54e49622f50d", + "value": 2480140, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6811575c99d4f6699035bb132d09ecc3147ff72f4161569c66fc4a2cf0b5630c", + "value": 2480230, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1cfad7beaa60caa099d71868328f5eac9637ca41d7b81cfe6012c00781be240d", + "value": 2480050, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b6245c53bdb152096fdd7cd4a7dd2fcd9a3ca36856d421065e3d4558167b100c", + "value": 620700, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "644b09f94d7c8cbce5c2d129720d454e89776c63d52c0621e445925eb1f1d60b", + "value": 620520, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "cdd09a40cfd3f086121ffda98cae6c96e5772be32ee1a1be7f4cf01ad3da580c", + "value": 620610, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "314cc65d595dd3d90eca48628343def00c2aec8d8a0fd9eb55fa5dbdb0a2410b", + "value": 620430, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d28a0a224b8cfeefb99940882f3ee1d6f5e9108ec1b48f92ad7c9ba48d9b990d", + "value": 620655, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "992f0e20216510950dfe2cb6976625df8485495a79ecbf98e283057c4c636e03", + "value": 620475, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "62b48a910f5a2539a8880c7365028343aa39fa5c2067497117bd9e17b8e5c408", + "value": 4955682, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f4d187f7c08c4cf098bece4f1a66cd3f21eff05523c791bdbd07ca46ae12f402", + "value": 4955501, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "deda48c62de6b9767cdfa78599a8e635be95e9915f02f2f5de93168eeb2aa30b", + "value": 1240160, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f300994685db657a78984c14cb7e30015c94e6277d1ed09d219979f75959f600", + "value": 1239980, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ff1e6db38acd53318de0ce62c044b0f2e1f8daf08fdb3927e92043d3dd0e860f", + "value": 1240070, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "da0b72472cf8a97c7b8454accd9dc076a1a0440dc77ee859ab36437a5c2aa50a", + "value": 1239889, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "28928c0751f57fa91dd77c10dbbb8cd3d9495a864f9fc921f66204f40ad4970d", + "value": 1240115, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "2778fb5117d0497024b495d76f4c5874eafdd502f581d61af5329c9b719ea009", + "value": 1239934, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "077e759bd0573d5c2cf9bb6bbd6d3c1eb6d54bcf25297be0846b445ebb0a6502", + "value": 1240025, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "16215a640cc9b0ebc36aee8931af134dbdb7edaa683c4196fbb8c55993cbe10f", + "value": 1239844, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "bf2a4952f4e19554001d167ad9368bf9255cbea5aed334a4b054f87a6be9030d", + "value": 9901453, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 11 + } + }, + { + "key": "80f7b435cf41045bad1256044380add99dddf575911d59498633c713be63f50d", + "value": 2477841, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "60f466c48cb3bcacd6842ceb1aa0b5af94f052862c6a2f39fbbf0d4695e5d708", + "value": 2477660, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e081047ee7a42ae3f11bed3fe5ff3218537ebf25a1012d01e18fe8851e40f607", + "value": 2477750, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "83cee610a0705626b5f7fa7f45fecc568e2063040476e5378a38d3ffc37b2d09", + "value": 2477570, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1280b7f1912c972a5c2158132b9be121d2d9150f2ef9ce4a0a0e6b7f9ae08203", + "value": 620080, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "60a6ee3b43a19b45a18e15f3722684e1141e49dcd9b7ec37f189f8f1455b5502", + "value": 619899, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "39b834fb83bce68156b23858152ccf2c179abc75ba3edb177b0c5f15616bb605", + "value": 619990, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "27432c67e70696d094ee37f3a813eabc401c9669657140298739adf9d0ccac01", + "value": 619809, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e739ee16ecb219f17cb1447b29a8449c4a601df3eaf73ba6a70fc2a02953870a", + "value": 620035, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7804d3887f63f0ed8832f90f81331c3c90e6be5b110d699c525e0ed49241eb07", + "value": 619854, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "51025af1f29401f7dc0587a5cda030ea03b0558c5e6f49dbb257dc01b1b44800", + "value": 4950726, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c6f081ff4089610fe1b231dcc8b4cf8da518ff27954c83eba7c95f192baa8804", + "value": 4950546, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e405986f2f6a67d00edb699d4e2d558868bd56e0dbfd7737f916504115c88c01", + "value": 1238920, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4f9b2af87165ccc3aefee6561413fa0f1d7185ac66dd42928a1eb07d49855d0e", + "value": 1238740, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0fc2244e015f30abb8e0f262c7b2f19be7d41cb9aeb3f3849c281c1805579903", + "value": 1238830, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e7caf2d860afde9c67a36e14748701ce442daf035cf5bcbd766265f46db50e0e", + "value": 1238649, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "956f46d8fe08c9fa9babdbd3bf5f0a2173c32e6ef270c38c86a1862d68682209", + "value": 1238875, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "addccf048e72b9f65307af1bdb3a5bd727adf07ac0b2dd68418426dd01646205", + "value": 1238694, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a9d7080aefdddd4ebdf243da3b6493f8355aade1e17d20bcae63aec9a24ec702", + "value": 1238785, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3bfe5eafeb4bf514b275cf47474d40ecb971a34d70ff8aa0b77466cc202aff0f", + "value": 1238604, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "b4c893d29093d699f18e92966b20447a7e7934f00d3784673c0b03096da8ca01", + "value": 9891553, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 12 + } + }, + { + "key": "8a8c834516fdaf80364539ec0dc100bd0a6d7b1dd8f1a9c288efb201217d4702", + "value": 2475363, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6f6069a606576b010742f67d8f0e5eb8461957db37d5f147abf02768ffddf603", + "value": 2475182, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ee100e26d33632173c6052a57d8736ab5f54c87fcf8be2b1b08083359d3fc600", + "value": 2475273, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0c571931beb8655d65dc57f8ebcc2537c3c7abb5c366456dac3d0aa0a715cd00", + "value": 2475092, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7b37c48c30fde6adfc985ef8f74656109f410cd50c1a3d0df2287a5d4d5de503", + "value": 619460, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b8bf53ac725e4289f5cf60cd327fc9b3da788169f39fa8f718294decd7e46d00", + "value": 619279, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0317180e90e9c4ba8302d9e2aa01243d15c19588e7620b92a33e5e07bf08e00d", + "value": 619370, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "43e3b024aa73ca927007ebd4038d0916dec1522bf81f3eaed816f2f14b1b2a0d", + "value": 619189, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "163a586465a56cc7f7fb55689be153b5c859f98bf53c9fd218195c66dd81d60d", + "value": 619415, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "397def995f6017c6f813b027540d32d74f0361abdb4f696fca669e778d5c120d", + "value": 619234, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "1fb1cecdbb0735b5514631dbcaa2e1d8adda8be7b546bf2fa3d3b9c4a8d2250d", + "value": 4945776, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6d213971ebabaeb029698f0a34286cf5aadea0e6403c6dc2b6b4d2e0a163850a", + "value": 4945596, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "be0ecbbaaad41d8c6b07e89ff05a2a71b544fd362d8c7167f2681d05f7c86507", + "value": 1237681, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "2dea63d9051d1198f2c22a9b6f23e01846cf0cffd46644b1deaf115011dea802", + "value": 1237501, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7e44af149c13fc0b8ffdd99b2410466abc4179950896f7e7600642f8b7acbf08", + "value": 1237591, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a298365312dc71d60aac8569ab1b8c53261aecce51bf2c5d155828ddbec52006", + "value": 1237410, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3befd1204989cc8eb92ff90d7dc8ebfa63d65238a4d119c5cacba7a4614d3102", + "value": 1237636, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "2f4095473d17f80b0d4a0c13ca5c597a34c581a8298492d5fe27999eda3c7a0e", + "value": 1237456, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e172a42790caf39879b2d47b0a34ebfd0fe61b28522b34371af50605d09c6b03", + "value": 1237546, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "30bc7aeb73626c64faa1411450f3c491c3f4b81da180922ffa7448bdbf042506", + "value": 1237365, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "2ff1be02a7cb208ef5f9b56e200fcef534a61b3ac9bfe5b915f4d5332590b204", + "value": 9881662, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 13 + } + }, + { + "key": "efabfb9333fa526f7149aac29e41c046b1390b399efe74f2ac7f7df8c98dc608", + "value": 2472888, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "39856555403e09737f5060d20107c22302c4d0c004d642fe1de0feb57b015d0a", + "value": 2472707, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7a0ce064bfafaa45fd39726ca31f694dae387f74ceeb01d1fb988b175676070f", + "value": 2472798, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0cadc2b9f80f3fe2ed84ea5731442b50ae540642371860637082b2eb3ddae305", + "value": 2472617, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c27054e3ee4b2c8e87d96929870ce60390c831685e4e36216626bc421932ad06", + "value": 618840, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "006d8d97e21c7644ba4b9f71dfd510c618a4974cb3d85e1ac9db684eb61fdb05", + "value": 618660, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "cf06ec7ba2e210a78f309a21c574f0b45827ed52cf4da939008c212ce6812b0a", + "value": 618750, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "50736836ca2cc7557faf3b7e35974272ad8dac5040ad5181b930fad8239fbd04", + "value": 618570, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "30340fb83038c2c155f00a9116501f47352e39c47ea96364d95682c05b68c801", + "value": 618795, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "46b6d4ce545fbfbc9f016553fb9cf59803c3b76576d2a1827a81f1f7e9142704", + "value": 618615, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "bad9506d7a4524811b346c6f99307bb6c2a7e287e0914b9f8240552f1cb1040f", + "value": 4940831, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "33a8cd92651683ecf200ae234b320dcf1b612c7dd73946589298a5ac770edb07", + "value": 4940650, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b68cdd9a328f9a963145682cbe89a0a1ead85e30855e978c32e7823e07582702", + "value": 1236444, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8e37ebd5de43b5ca6da8d632d07962339dd783c0ece17f55dd74d605cbf0d208", + "value": 1236263, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "05233093a678a63821e9e66e74b30c8095e485319c50d626d2f5caab6be2e10e", + "value": 1236353, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b81908eb81aba037376a442c4b3fe43ab3ff6467c3d919052d861f40f8c8bc05", + "value": 1236173, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6922c6f80e7f819cc646e09df1dd40bbacbd53a98f4d23097ca89c9711ac1806", + "value": 1236399, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a3c02c00d41434ff04757cb7406cf4e044946706218afdcf9c7d6fe7bdd3a107", + "value": 1236218, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c49351383d53101dde2aeeb39c4a4839b6506493b03749c9077edb08fb09b004", + "value": 1236308, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8b7662fc54892be7f4e6bbb2dc1000c8143e409d6bfb6fb18df205cebc92e80e", + "value": 1236128, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "dcb5c289b63da4f6a97cc7fa66a8c2c7b81004e3a4d4458a499b23c9b8a8990f", + "value": 9871782, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 14 + } + }, + { + "key": "889ca832d6403275d8fb3c535bffbc40de69be780740b42a51d5a3949099be07", + "value": 2470415, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "84e8658789b118df91c3bce818af7d22a5999c4234c091335ddd5b3e6bb49401", + "value": 2470235, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4d60fd615105a9e1f5fdb2195213a0ff11b9ac71d61b50a2c7e98857ac4a7b0b", + "value": 2470325, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8e02b00fc3bbe6b60ab24b515da480146b8575154c8b3db0b4a432602b62ad05", + "value": 2470144, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "068f7606f4dcf7bffe30cf0d2f7484e54246b5068e0542d06055e8b8370c4c02", + "value": 618222, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9463d42eea36a07cd7075b732573439c50ce5172802ea0f07ec8a8a9d674360f", + "value": 618041, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d1770df4ae7a8b6a997901699418590c19e7105e1c0842eaa90d07b856cb5808", + "value": 618131, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7b1d8d042e78b4f9ab803ee8d25446001b18317556c61f412718584c3299c000", + "value": 617951, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f676b9203f68586437591e5d23ffd20ba3b95cc67793de126181a35d4cbf150b", + "value": 618176, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3e837f5dc9ac77776bde6ea3d347bc1cc1ce7984376f9c8d544b091c5c4c450c", + "value": 617996, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "4449487f1eb5231486428147c8d5555694627249f911380595bb91598defb30c", + "value": 4935891, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8728df3bd953c5de4b60e0eea9427f95b6c2ecbbd8bfd4d5dc85af16d8773908", + "value": 4935710, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ac70d707fc9b7ef537308492d949b3983b3c9ca4f15ea9cd11104563ca3ba90a", + "value": 1235207, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b3e35fe77990d166f16be4b0b422acd9fad28b632d04fb24cf817c187774f40b", + "value": 1235027, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "2196d5b2e9f0c53f0366df12142fa3d7dabeac08b7952ec7005d69edfe412c0a", + "value": 1235117, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "04b05473be19736dc253cc2832455b5855a6a63a411c32031187d196b9c3af06", + "value": 1234937, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "05e4a264e91a7b5adda3798a9d3073ea9fe8d1194ba9f9a1b8b6a66aa152b304", + "value": 1235162, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "035b8b76f98045607e53e7b50ad7dbaa2ccaee6441135e564376ade0b8c39503", + "value": 1234982, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1753268ad954d89ce5e46c645a558941ffad541712b0d5f7604d2cc394f11c04", + "value": 1235072, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6e0a8926761530db78979b951ab6eee40084ac6c0e61a90eed8c0ffb35151a0e", + "value": 1234891, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "bced2ba4c4dd5e1127adcfbc91d27301482348b55c8be367bed7570507eafd00", + "value": 9861911, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 15 + } + }, + { + "key": "458e2d9a4f8567d0d5c49d2606b256d3adb6cb2513621e40f8e9748d73c5940b", + "value": 2467945, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "37814ad433b6bb94e561aabc33be0cfd2286e955ed64600cbba3495e3a62c308", + "value": 2467765, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "fe1198c381b83f4450656bc79c9ce55b37fa8085ba3a04c88f075d3100e99c0e", + "value": 2467855, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "db95d6ac3effa0fd3967b13aec69f86b62ea621218cc661df6672485bb551d07", + "value": 2467674, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "fe1f013dd7e424ad05bb4242b49517492245db0ea6fc34398b1e64ec7327a302", + "value": 617603, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9900edbc1f2190569dd22ea60c7fc3e251fdf1d454171303772a193bb69b2c0c", + "value": 617423, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ba4fc3528b3a093e0ddc99a5bdf4e9188eb454f9a21bfd3ae45b373d66b67601", + "value": 617513, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f46bdf483f2e72ee2919da9d42e69f4cef4bc57886782e95c27cf9a41f918202", + "value": 617333, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b9427d5b414540d770cc7a8c898bd74c2fe9f7ae3b4f8a78a51dd3b8e93b2702", + "value": 617558, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c88f82392f9e716ac66bd6f9ad8df16eb5ddf9f57543a466bce4868d445ce30b", + "value": 617378, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "a209e8b8e0a8a82313fcc7e4dcd9a6d6853c6a782ac786ad24eb2fc83850ef0a", + "value": 4930955, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "aee223c45729660b2bf13848f9c209263cf7752c25f790776c9d761fcfbccd01", + "value": 4930775, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "721a6ee97816bf195289964bc0fd21e47e97f639b92a79404df81bfbe08f2c03", + "value": 1233972, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "db9801c7fe4ef8f772c2b3e8cdd9b15787678976e4772f65a7ce0f67206c970a", + "value": 1233792, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b867d93b5496cdbd64354acf7dac69f25eaa40491f16e971ddcaabcc54f6e00c", + "value": 1233882, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "67a68fe367ed5ddc02b0e8b3283b53277619a768827196517bfcd3b3e275e202", + "value": 1233702, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7f0fa83e14a2ab82dec73a2a263c07cf0bd2b2553244f938f4d6b260d5723604", + "value": 1233927, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4b46860aa4b2e215fcd45c0ebf49e67c24b89baf5287b3fcd660a7e9f9bafa08", + "value": 1233747, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "045fbfd55f21c5726b886947d9ac234b42f25ac4c32b21c632fde82064e6ce0b", + "value": 1233837, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "82c3f46ef8fa1d110411fc214fdc7cbc5be1acc2a15e20b2a0d28bef0aeb870e", + "value": 1233656, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "a95c01c86ea0328030f940a46c21a332b0bd3a8fdc308edbe148fe94d5b2330f", + "value": 9852050, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 16 + } + }, + { + "key": "08360decdc72989f859f8a365b8c87a83761b005aefdd26d3930aaa8b738320f", + "value": 2465477, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "396c8d8a9fe1855230834e19c52070f525e8ca9e9b2503533221f4201109230d", + "value": 2465297, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4696dc11dd1f39db87b79e14bac3423de9680d2887e553d6b78f3c7d2bc7b701", + "value": 2465387, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "38a83edb98fc757da4c4ec67539e5b11f5a434153959cb0b255316543b967d0f", + "value": 2465207, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c968bcc35d741e8ed87ff8a6b3f6df2dee3dc2f802df787fb6bfbe4a6eb34001", + "value": 616986, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4c4ea032ff444f8aec35fb0ffcad51dd5bb2827b274474a4956782dd6665c706", + "value": 616805, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "144b88f0630d3aedf7ddf38582a0d45dc550c129365fca25b75758897b298c0e", + "value": 616896, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "89f927f40ccccab140a43dbd11cb82d57e7ef8efc72411d0c3f25e3987ea2e0b", + "value": 616715, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "12591281d763d29a02188dbdf8e26f9194b13d9834f9f28b95b3d214136ab408", + "value": 616941, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9d2e907dce020133f045da77ced222baa3f74bbd6601e774b36a387bc05cc902", + "value": 616760, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "ad29381e1c11c4dd92321143540f737e2079306c68c440803c044e82dfa7ce0b", + "value": 4926025, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7fc69a7c3d14703c8c262ed77c55bc6fd5469354dea16eaffae2caa1d1b68503", + "value": 4925844, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "2fb52fd7845799f34f967124c8d52763a0233d46fe006ba13454aa6c2ac6e106", + "value": 1232738, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4adb5e468ed0b14abcd81083795881ea6c379b791af810ba1a5a9b80e5eeea0c", + "value": 1232558, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4ed493c52a892d0acde41cfde786e7b62c4f70f6502080d468c3526a6ccc620c", + "value": 1232648, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "164600af661edf992ceeb955535c2ff6c4d6a8fc74724988ba2482f4bf35dd0b", + "value": 1232468, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "983baafd1c1e06c690ce6887fd29fa04c06fa1bf060cd6195506738980837d01", + "value": 1232693, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "37da8066ac0d6b7f414a2141ad724fe3b4fe8953ba630cdb2a9e2ce962765b0a", + "value": 1232513, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "99559fbea709fa5a779edae4104b577fae9a21cb4dacd1568ccfce52861c4a0a", + "value": 1232603, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f443168a9ac04715a760005373002a3e05ae1fc73f7fae6cfa488eb0ff1a240b", + "value": 1232423, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "1a51cf7fb410963f20f58625c909fc1e3b3f7a973d4c0828ab2f52e3bf780308", + "value": 9842199, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 17 + } + }, + { + "key": "e816d45b606cfda0166a6cd62a68310cda51290686a254f1f7fa6af6049a590e", + "value": 2463012, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f15c32ae168ff4f5e96d55afb786b4168f5cc6bd65edc9a3285aa9b42a06a30c", + "value": 2462832, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e83a2af37055689526f85679d92a564c8cb6dc93ea5f0edc8f85b12eed34050c", + "value": 2462922, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b01cc5d36b87ad25afaa5166b0de60f99331c9a8d475cc0074a1e027b9b1810b", + "value": 2462741, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b8506482b3b8593aec70f0ed9b0b45c4e5c88ee95c9dcc7d669e207161b5f80f", + "value": 616369, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b93ce91cd1110e5710ef13d0503e391ef06d8df93ee1c70bdf2d39a3794df80e", + "value": 616188, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "51fbd98d1318c2569d6b6fedc0fe01516af3a10839a3f4aeac5532c6ae19eb0e", + "value": 616279, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8c52cf98bd54bfd8caea25983b05729df636d5be32732794552378668a5d070a", + "value": 616098, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e1a78c1e9ba5157286fc15e0552905e19026b08bffdad8f0f55f8f68cfdc0a0f", + "value": 616324, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4fdfb00c266bbda5ee389673286b07645e8753f16e80dfa6066b6b0912bc6105", + "value": 616143, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "67e17fa7823643ceee885458caaec22086c1a1795ca30aa7ee6a6b17128b9b08", + "value": 4921099, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8d67df0c745b37b797a431a317c35233bfe08614095b4eb9dae8d5e8b431b80f", + "value": 4920919, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6b6cf183eb0ca0b40fc91f1675450f9b8ea590b75d6bd78055b4b2d186756f0d", + "value": 1231506, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e99599964dfbba69b76d975e905ba16a50610e04cc8f36ea62b43904e2431705", + "value": 1231325, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0484efaf55a0e6c1ad5a3fee916c11c4d2e73578a6201defe102188ee2b45809", + "value": 1231416, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "044a733fbf58a35ee0573fd43899d20d31530c08127c521ce648b915fa28410e", + "value": 1231235, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b4bfdbdd4c69331cbd71a28d71b211afccb1f4be9ddde10cfa95cccde474f807", + "value": 1231461, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "bf5872a489c36925778da2484dd05f0fac404f36214c07122eb075f92b8fb401", + "value": 1231280, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "92268c55419d75900c888dfab5cb24700efcd9b05e9434a1a660adb653cc4c05", + "value": 1231370, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "834b8d8e5e30e5395d9f3a6fc284f7e47a33172b5c76fb1a88f68c9073d79901", + "value": 1231190, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "19610f14583a60ca7bbf604d877fba35a5568759918a5732256ee40130141303", + "value": 9832358, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 18 + } + }, + { + "key": "ee45d87e23189bb4f36cb024c4fe4dbb80e9376ba0c412ab94b4eb70eb37c306", + "value": 2460549, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3376e02708fe7e77afdc7df849c8c30749f746e5356d37f6f9013ae6573a2402", + "value": 2460369, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ebe2b90392766326dbc88cba0e386382b2190596847838bdee51b99bf4e04803", + "value": 2460459, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8e3e0fd7a61cc9ae26c6976cb21bdbca1772af770590bc7c52e826db0d419e0c", + "value": 2460279, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3f954862f104addcdf4c85b5cb8a6b3fa469d40c197ef1e00681ae04cb7b570e", + "value": 615753, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "acfbfaea32683583b739f6a383339a382884363894acb1fd6bf1158c3f757009", + "value": 615572, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e0a14895bbfc6b08fc5868c54e11d87a6c2a4592fc993f247a473963e16e180b", + "value": 615662, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3e1572bc287fbac145321dedaf2e155443a9e461b4d4250f94f7115408030c09", + "value": 615482, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a54a8158a6c2eb907fef274c466d655f87c2e9db76fe085d1509093d88384c02", + "value": 615708, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f8fac78727621bd2c9aa5707b5e0cda8e599441f66cf03f14d777ce9a884bd02", + "value": 615527, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "182acf52cd63f1a1dbff4b046a5d408ea979fbd698471d380017c3cf005e9d07", + "value": 4916179, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "aada92fc657bb44b071fb4ea8ac2c1fc9c7a8333adcf78b68cd7e579836bd103", + "value": 4915998, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "13fd0d7771f88b4958a1fb9ddfb662c3ba6b1a912cc2177193b5b986e565e605", + "value": 1230274, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4b58a48fe7c0f262c44cf3cfe896e7bf654e13d9bd29466a9f65e21c06a66b03", + "value": 1230094, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e3969ead29e82460a860a12ff80a8cbbc6c0abed9df1961ed00ce037be3aed0e", + "value": 1230184, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "17d7361fec911553e0252044125785d31b0d033b4a97e6c3568260712d2c7a0b", + "value": 1230004, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a35b57ae229eea14cc7faf351ad1a44f55f3ee085b16308f177177dd0ad41800", + "value": 1230229, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ed41123010b33b9e3c856923d646ba79590710f2465aabf02c986a9349c63e02", + "value": 1230049, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "de9a7aeaaf3879f14d909e53fe7ceb27a784e90518f7abbed66c6f710b6c3a06", + "value": 1230139, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3577435a1e66f985212148b589e17c0f9b83d66f987f243555898ab1eb670307", + "value": 1229959, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "7a33a7daec92b902ac193081e5f4ed1708b72f9b9319a23b884821b4b93c3205", + "value": 9822526, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 19 + } + }, + { + "key": "6d8c46d2da26101d7bc5b2bdab88232a8df03d4db83eb5253a6100cf01a3110c", + "value": 2458089, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "072abfb04607f1cf7766fa18fcf2e0d41f48efbd2198394eab080b036996ab06", + "value": 2457909, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3c4dbe36850824108e26aa422c2a114074840a300dd2f85d75b43c6a9e34a406", + "value": 2457999, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "16b27394b2b6dfa38c55ceedfd68009f04ad07a790f509477b20ded2dd949802", + "value": 2457818, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0113ec0384a70baba6248ddb1d15119c37df86d1261c11236c36b1a829885507", + "value": 615137, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "71f04dade26af18b623531e800f6d088d4aa0628b0a55f52c5dcb61e577b200e", + "value": 614956, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "828a7e4dbd7f57c820afc7ef7600f2438135c74146181134deee670acc712e02", + "value": 615047, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "103efe8d4ab54b5409ccbc23276fac33c0e981b5603ebec79db2e4dd917d0004", + "value": 614866, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e472a2f141f8c9c4da1a63fa13f712940108c4867af62aa1ac7dfef9f6b8b40f", + "value": 615092, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d5bc867679b7852160f85ad225330a9db0326cecab9bcb7ce5689b013a0bf90b", + "value": 614911, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "320b75f57fd304d8bb752eebb77e154797906be99abf629a8e50cc22abd7ab03", + "value": 4911263, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a263b0e2c725f245d4b7646323d779ccefb35f51fc17bcbdf0da54e75b701f0f", + "value": 4911082, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8554e0c2681bda84e3ccb48af462831da3f098bd3ef58ea7c965a9d95c94120d", + "value": 1229044, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "bdeba4238c12e8b287a497c1bae79d8c4b03f0d6a5fe71787a2862d532084e09", + "value": 1228864, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "41372c665bd03f8b097d37bc19a546b1f899b2c3dd0df5c19b36fc3e39afae0b", + "value": 1228954, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1a0abc752c8214e30beb7a2ec972436aba4e7cbe11befb9357871b4260074206", + "value": 1228774, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "31a9b116866d28455e2fed748f7f604236e90f013a3be78078f9593ccced2e0d", + "value": 1228999, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "68d080279032c14a6ef73711c9763e15524833dadba1e4fc5cd2a0c452f7ae08", + "value": 1228819, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "72431d537fc4c1e1b4eb2cc9b2d9f9e78f3cb1cfe8038c4d69e344f6d0d8df01", + "value": 1228909, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "314d3ef443c5f528b4df6f5154b64a1a79fd952321cd14466132516cf5c6e703", + "value": 1228728, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "c8facc6e5a11f2a63d750f8d41a008d3ee1ae2f6d74c4a5dd5d1a3758b1a9202", + "value": 9812705, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 20 + } + }, + { + "key": "ea050c91d9733c9cd7c10f5af26f25eb8cf93df7ab155e9ec5f583cc0d905502", + "value": 2455631, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7d1d92301855cc6afbf3c25429905d015a296c2abe1b5ba2219f163b4bbd8004", + "value": 2455451, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1b53a39bcd19286652445e50acb312ef10a901a0b421850daae8b793ad96e006", + "value": 2455541, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4148ae29456045c8de5a0c25415630d4defbc92b001d9489b6f9fa9acf5f110f", + "value": 2455360, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4ffc481f78412c8ca5a245034d118147f4c0b6dfe1bc84608cedd23c0440900a", + "value": 614522, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e4c4a484d68226136f8fd5015ad33325e814dc835a66ecf7899c9d5988c4360c", + "value": 614341, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e4c813bfc0bc0e53918ca4c62b8a985965e438b7a0b4dd6e732353a7ca07420f", + "value": 614432, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e307ac829a3ae11a20e17955f743fe4f1299077161a7e4312ef408a2e3a5ae05", + "value": 614251, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "56e2e20bee10bb3e913c2d79c78c622039b5fcfd0b50a99000b3bb0edd47ef0d", + "value": 614477, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d6f1bbe37b0f2c2859558f5f2f12db4b51a8fe68df2b79376661c401840a5307", + "value": 614296, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "2718c23bcd94424325f4f73acc63f93774289342218d1045755c6e109cc3f300", + "value": 4906352, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b0aa3c2c137701406f753b54a0978c6795d19700f76de40638f1d95f992ea108", + "value": 4906172, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "cdaf6817cf474c2ece34ef66399adecc8670adea3dada14264fed009ee6ae70c", + "value": 1227815, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6d3ba16d86bfe64789db58a3a5d09f71bd4f98d7f7f08db0539420854ce5e603", + "value": 1227635, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1bc6d6ffab3cd82c4e8af42b939dbff94a7d22e34758746a8bee9b64a3483407", + "value": 1227725, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "555e18155cbebf294ab52f2000680fe19b6c8c03c2a3271551d4d373a308fe0e", + "value": 1227545, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4520329bf60c3cfaabd3c246dd0081668c510b6b966e65973a19e0fe3c5b7a07", + "value": 1227770, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "15d295f683e612f97f6b7c5d3c788c080193eface863aafe9a31b5c48809a304", + "value": 1227590, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "852d808743b27b4408c7c42ef9d30a09fc9edea90b27538dcc586084ac067a00", + "value": 1227680, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0ab8cb89a7d37a813e03809d2ac98aeb875378b218527241c9e1567a1bfcd20b", + "value": 1227499, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "d7d63961685a89a87be7c86583ce7c935d94004e354f04a7609bf5016146e109", + "value": 9802893, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 21 + } + }, + { + "key": "44c3a2a57de3fc45e409dd052d522df4b9fd9fe67a42dfc895ee2f08dc107004", + "value": 2453176, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "499bf3fe78f1edc2c5965faaf4ed4422cb40c31a51375db63eb6a97d6f39540f", + "value": 2452995, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "952dcc886e115e7c81f521c7705ecdfeadd6865f60b5e49857d3490ef3afef0c", + "value": 2453086, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4ae7c45a73057b45f59884bbfe11d6cfae5eb84cff5ace1cd467fc965a80bd05", + "value": 2452905, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "25178947edb495b97d7b866eda22845077e71b33ee82b63f284ce6fc76e27408", + "value": 613907, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9597cd332e09a435adb62fddf50eed8b07b23d2780f0a168b467785fa834730a", + "value": 613727, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1803c1136113c07f0f1b7d249f78f8c8c97b8f537eb3712bd207ead946eb6203", + "value": 613817, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "941b4f492f516ed5442e4218292c8860dd8d268aa5d84883fbb0255c22ead80d", + "value": 613637, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "158bedb110c7bc937e6252ef6b0ca5b0a2e9cb2295422fef409bf17743c89706", + "value": 613862, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "92312848ecccadc543210f92e0b3f2033a1eb503b9293d4d5ddd939d685fce0e", + "value": 613682, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "43a3c12ecd76d4a277c49a59a200ea26ebb20940696b50a3d75166184aec7e02", + "value": 4901446, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f81620a56eb69709a3d797794a1150f4957037b82f890130159ca448cf49d301", + "value": 4901266, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3f88a5805a8841b5d6a6e72b5002e8e0882837f88e36332a1f42de6f0e0f170d", + "value": 1226588, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ea2ebe3572e04c35592c4c33840232f78e0f97a22b677098d99b81b92eeea409", + "value": 1226407, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7851a866cb45e4829ed5f9372eac5c0c29c4211963ff5d8ea97203b002dcbd04", + "value": 1226497, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "5a9699af46e6b2a4bf8fdf60187626a6b28db0c54a1cff4163b12864810e8c0b", + "value": 1226317, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "bf54de7f74bea1089217061660c85e0a9c3e94f68ae3c1c51f84d41291427b08", + "value": 1226543, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9d2ad3d1fdb1c7473752096b7a79ce9d24ec20768f0203b56e32be02d997aa04", + "value": 1226362, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1816109b81f7a66aeaa2f671acbef989583c2d60fced78d01517966b138f750a", + "value": 1226452, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "96232c540aaf2ef8656b683ee81db77652cbe781c56b84bc00a5d68524842208", + "value": 1226272, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "cd16f31cddf8bef5b155e16869945a4564e175af927580038dceadb923ceed0c", + "value": 9793091, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 22 + } + }, + { + "key": "b6239c4961dc93256d90b10439664b6872eb1a3fab616faf939da26ad924170b", + "value": 2450723, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a64d9127c2804f16808d99bca6fee8497dedc6fc37b7310ae87f1e3984d36508", + "value": 2450542, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6637c4d18b20037aa0ef5006b139d97390e29721dc1f8b32adf35e4217e26905", + "value": 2450633, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "024477e14291fa39da6419f1e3250296d74e4ba67c9b5e0e7b2c288e07fd5109", + "value": 2450452, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "069517b55d675b59dd4689368e75fbf412e44fa9f551a725c857b89e98c5b606", + "value": 613294, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e5ec779cb8433af6b7d9a41e68c5ffe67383675fdf1e3e354b5f4a4b941bed09", + "value": 613113, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d0bd80fc87dd272b4a9a032149fd830defb312cbcfc7ab15a73ae637d75b2602", + "value": 613203, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0a8ea5578c1eb922698e8051460d9472ee407570b112f88f37f2dc1de4c47701", + "value": 613023, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "5406268311ba139024d107cfff7614e29a8f0b25d438977151166645bfbecb0a", + "value": 613248, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e905def83229fcda9ffa2718d327f4267453e4d0e1daf1138a8c8cd9f14c0604", + "value": 613068, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "ecf6308c2e1d473206e2ad40302dab39453352a4154b74b8155343c1de07b304", + "value": 4896545, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3c1e9a6b32afefc2bb4fbf1220e6ff13c6be3ee137003dba421ba53e10820e0b", + "value": 4896365, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d432d85c3a0ac312627652684de38b3d2eafd86211df33483edf0452c1d5a209", + "value": 1225361, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "fa27fca1e990f64ac498456f9d545e4a8b99eab42cf5844346c1570ca9fad104", + "value": 1225181, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "3763c0e53fd201f828629d96157471d7d03f6fc4818c67563f7fda4b50c8a809", + "value": 1225271, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "582d5c0d8480e4771d7f47998a13dc09858da1ad7786626b17f7f838469c650f", + "value": 1225090, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c15e30d7a12f7cc72628d013d2f7c6c870b51febcdde08eb5bcb6fcb86720a0e", + "value": 1225316, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "fc669c704795fed18016b221d6bb28545a4aa787e8d19b8d62b5a727adb6a20f", + "value": 1225136, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6daad60059d175513c6fd8dd178fb3ffd96c40442ee45c234becc86e39e04900", + "value": 1225226, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "25cfa5cf9d1227552500f4454acaaaf88f9b0e057125d8567114b9f50d57680a", + "value": 1225045, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "70198dc404c50ec97e63375408be059d7134f420bdc19776948b759854e83d0f", + "value": 9783299, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 23 + } + }, + { + "key": "a45a79f3ab6c7f80d8a257d4be622eb8068bf441e7ce2a262e059c36575d9d03", + "value": 2448272, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7d6d003aad5cd2b7fd12c3aebdac5c5e77e8bcb359aab42255fb0cd4378af807", + "value": 2448092, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "987e24445bfd70188cf4d37d9b02202a3629a9d0ed4bec70236e64b911d4cf07", + "value": 2448182, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8879706c16bce19329a4209fadf6950c30fbad50f7f3a9daf0575b0f6c6b9b06", + "value": 2448002, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "22e9aac8f429fe71a375f09ff2cab06d42f5d3f46ffe90111537de7475921509", + "value": 612680, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d849935d96e0b8f4b21d112fc72e85b5bd9d0d98a79c647b35638940b578fd08", + "value": 612500, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "87b6a04718d77783f87c14b08bfcc4317f664dcf38ec0a891119c4798fd7db08", + "value": 612590, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1b062144c3d917a25a913c53d483e6115b83630ee086315c4832667a84777b08", + "value": 612410, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4bd0a5bea92d70f9946085fb68c83fa37333afd4411c10ee56f151dfff18fb0c", + "value": 612635, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f5ecdd430fb3ff4694969cc38912397d2908e05ef4bf0647470e0ef724baa302", + "value": 612455, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "4a60363ccd7abfa109432dc91037d0bd19f979ebe705f8df4d711f40f8894600", + "value": 4891649, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "4f2a28b37718aeb03e13f90186aa2626629d0efefb2563dc7d57b43a9ead8001", + "value": 4891469, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e7175a859c52f60aa6ddf651f46736d9d41bc45b69b56c35be34d2fb1e7c8c04", + "value": 1224136, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "73524b9a96a441fa71ed112647e0f15ca97fe82b2adf52b39619124e6533ff02", + "value": 1223955, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "28162073d59cc4a97b41e6f14077d6c72d9233b750b75310df26fdc546f14b08", + "value": 1224046, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "719fdf90ffe22ff8161559347d689e592a78eecc5576222d166afd9482d02c07", + "value": 1223865, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "12ca4d5c9bd3c1b7a98d16813d14926ffdf3728ea5fddc0751038207f530a809", + "value": 1224091, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "0a1cf49400dc0b98a8be9739dc59a3cef4513cf0b185d2d660c2041d3399000a", + "value": 1223910, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ac69b113937223a40fa1c87b85bd4f75b431bd89184ca1d7eb7be4351a55b60f", + "value": 1224001, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8f917c787fccc2a443cf995221f910eb06a5312efda1c96017f733887b056806", + "value": 1223820, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "5690f33ee672fbe295dc1630c57bfe5486ecbe593f3c1b037708315bf0191909", + "value": 9773517, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 24 + } + }, + { + "key": "9c72428291633c2c9e5a912f55dcaab8d9a28afb60a354a69cea1dc010897d05", + "value": 2445824, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "516a829728d830678c539cbdecc9c24417a53cd54ae8edc18fbe00ad34320609", + "value": 2445644, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "cfc7bf1d0c1c6442570d15a2f6d302466d11108cc1947c28670596bfad0a7208", + "value": 2445734, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1c601c797d33cd57cd2811145871766b3416024c77992d71a08a65e32acdba00", + "value": 2445554, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "be53897c8eb8504bc50f29b473e2e2c6ae76ed8b1b9bbbe19559ce285bc4f401", + "value": 612068, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "a85ac9365660f36e91fbb63c1bb0ac80dfee0a5e5602fd5d14562705aaeed203", + "value": 611887, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e366fa7b28b878b8883848270932f58c6d290fd8cb0358e0b2d60069e6ae7304", + "value": 611977, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "ee5b56d7a744b1fbac8b3585347dc71e341e588e6ee29883fbcb0727a7901f01", + "value": 611797, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "2a3f1fa9b860e05163311c393d0f80a9331eba71e92bed5a9e301af88ffa3803", + "value": 612023, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "be976899377fe797d19f3b9218b163dc24eb49507c4db61a1a938dec7b0e8506", + "value": 611842, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "cbfe99fad9b48f7928da661860b25d41bdea57b28367f498f5ca5670d302dd07", + "value": 4886758, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9c030be99cdab7199c081a8815a89eb748523480c05af983fe8a2fa159d95509", + "value": 4886578, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "63fb76e0debcafbdf626058e931548934070814b620a183447ea7f496a9bfa03", + "value": 1222912, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7da7fa74c53c4146651557f0a558f1af356e0a01464ec62bc9d491e532b2cb0f", + "value": 1222731, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "fdb9cc6cb379e6e2873afb3f5a5ec37cd357e498b74de2585bb88eb5df283a02", + "value": 1222822, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "d2707d0958520231b4c282f93002e09d637f2d9eddc715afa94a3b5e62d39004", + "value": 1222641, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "8013c181d03dab8fe92dbd0500686b99bf7b1b6d342404ebecd089b6b0dce905", + "value": 1222867, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b56153afb125f7cb957c2971e89a90328ed8959b03521fd7bf6ea436a0087e01", + "value": 1222686, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "15e556421e0b5c5acb4da969dd6ccd4a6656f3ab077dfb8cbca9cd4359c3ad0f", + "value": 1222777, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "5c8951ad1b0c67449314469ece6aaa2541c02efa985a5bfd7cd45c8cdd1e8e0b", + "value": 1222596, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "714be9c37e00db407f0aa795eaa4527c21f7c312a801dcd009049c4015ce5400", + "value": 9763744, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 25 + } + }, + { + "key": "8b5cb83f12f21d5ccba5268baad1ec9492610bdff226cedffe4c6fb3b962fb09", + "value": 2443379, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "9f2ed776b8787a39e234a974ea5714099cb891d45bf2b0ddfe28dda3bfd77109", + "value": 2443198, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b5549ca6230d996cc9a569476e307393db2d903673c58c8f5b51b00fd66fbe06", + "value": 2443289, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "64501a94d536e825f2aadcf02ffdd75b081b02aaa3b374d2f71468e711386c02", + "value": 2443108, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "06ef70eb15bf638d65d86d2cf797b544ea2900a1f0c305c22c700075a65bb504", + "value": 611456, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "94ec548922289e9f935057a312b6cfc7e6711bb25916905403d09dc185f9a80e", + "value": 611275, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "702f3c053e99ab5281bc25b83da5aa7773c0b380e2c85d6df3afa21ffef6020e", + "value": 611365, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "09b55f66f0913d06e2436d665946999d097ceb69889af42a196e14b524a5ff0a", + "value": 611185, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "905b9432f653c461dc9c15bccbc1c3bd7f1c6d47d04064fc452ce9330ae9060b", + "value": 611411, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "1b3185858007271f65a3b1f70054fbb67eb870dcadc190efdd7018e3de71440b", + "value": 611230, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "7dad8cff1ed9ccac75aaf472ae93dec313a7347f8a9bd1fde8fd48f99dab220e", + "value": 4881872, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "bb1aa846d4c55a67003b01f7d7cf538d347bba6023e8dec63cca519342e8ea08", + "value": 4881691, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "54199500391497745538792cc55b02aa488aad9bfba2e2e2b2424a28cb30e601", + "value": 1221689, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "e44569b98103dfc3fd25159700f3592f026bc8ca26e1f6b26434cadcecc6290a", + "value": 1221509, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "09d7136ffc57cd6d223494e4159a0806d0b3a7565bbba5d737ef60f1a85caf00", + "value": 1221599, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "f6d251d6f491b555584d822ae05c256afa9e3ea2ddee3176c9fa89e41e233601", + "value": 1221418, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "6771f486079b7ddfe8f91ee41b372295179e82648062362a3af930c88914df05", + "value": 1221644, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "c9f65eac21b02865799a1fa030df86a876a4b266df36ba1d7dfda3ee81fd6300", + "value": 1221464, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "b58558edc584f460d96735f2408749fefaf16fb5104beaa834e7dd990c4d5300", + "value": 1221554, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + }, + { + "key": "7528f8d7ed1c17bce9a433e969dc657576310a29f3e64166a8321388017ed003", + "value": 1221373, + "features": { + "flags": { + "bits": 0 + }, + "maturity": 0 + } + } + ], + [ + { + "key": "951ce530cb974b2d051e5a5fb32fd1306f7bec8ffcb57077a5538b73460ebc01", + "value": 9753982, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 26 + } + } + ], + [], + [ + { + "key": "6174b249e770d127fccad72ec28ec19d3e9fc4445bf55285977b980d48d78f0f", + "value": 9744229, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 27 + } + } + ], + [], + [ + { + "key": "79433f4b9fd7b68a9c8c5e8e220e0b41885ea48f9b9c0e82bdeb0be6eface802", + "value": 9734485, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 28 + } + } + ], + [], + [ + { + "key": "833f68f37ff0fa759f96464b8d343dccef28bf8fdd1365f52ea3552b0081ef0b", + "value": 9724752, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 29 + } + } + ], + [], + [ + { + "key": "2e3ffa6757cfedcb1c8ef7a58d93b3918966a878b42e0c9d63a68b6bbb1f9705", + "value": 9715028, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 30 + } + } + ], + [], + [ + { + "key": "d56e829768413a00133d8ee9f321e28fe799b612f914f92e62b3a7907ce8320e", + "value": 9705314, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 31 + } + } + ], + [], + [ + { + "key": "f901288d280f86e7b69121c289a78bec8afd37c553b1e101de08764c7c037a07", + "value": 9695610, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 32 + } + } + ], + [], + [ + { + "key": "b2956cd360d863cdcd7caf77c066a2b7c627ba3cd1297bb677f8c1d1b432de00", + "value": 9685915, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 33 + } + } + ], + [], + [ + { + "key": "e1013d9c55ba074406e11289c8635939a4234acc44a732b626715a430b8ef307", + "value": 9676230, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 34 + } + } + ], + [], + [ + { + "key": "ff2bcb5c99ce3b4590678a595d3766223028bf2a48c93b99922c767d61736100", + "value": 9666555, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 35 + } + } + ], + [], + [ + { + "key": "b65382dc07088006288cb51d1cb1a546b77d1830b362176e3e21e4a2b741f204", + "value": 9656890, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 36 + } + } + ], + [], + [ + { + "key": "c2c78eddddc379376fa963c4093b415db8108ce22f20d56aa020373864407f08", + "value": 9647234, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 37 + } + } + ], + [], + [ + { + "key": "d5859554b255448e4b99dd5be6b36a5db1d710f2e55550c76d0621819c2fe200", + "value": 9637587, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 38 + } + } + ], + [], + [ + { + "key": "7af006e6bad761b78d5e7710542a4bba4cfd677c57ea6f8c29f5ebc55a231d0c", + "value": 9627951, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 39 + } + } + ], + [], + [ + { + "key": "8a093395ed853e979fe04f8348de019890cb2ab0759a83570ef4b19b3e8e140a", + "value": 9618324, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 40 + } + } + ], + [], + [ + { + "key": "e791e37b4f364522c7e4023e57cadb0734b3c4bf9b8fba379c393391e279110e", + "value": 9608707, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 41 + } + } + ], + [], + [ + { + "key": "6445d8e237bce2c3736c3efa819d075a40322e3a5752a96dc7e10b5a7a3b5209", + "value": 9599099, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 42 + } + } + ], + [], + [ + { + "key": "5232048c5c84f2c54ec2ad9741333f2e42859afec243b27230bc3e619b5e7d0c", + "value": 9589501, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 43 + } + } + ], + [], + [ + { + "key": "3cdd7adc0f21af72e59e47e8234c6642b3b00a132a345d576d4c51365a44dd0a", + "value": 9579912, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 44 + } + } + ], + [], + [ + { + "key": "6b25574604b9e442058bebf73d6e612a0e0706a7b07bbf561307d8ebc800f102", + "value": 9570333, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 45 + } + } + ], + [], + [ + { + "key": "45be520a451e9af0b284eba925d81bc69f58743c5ab949b21c64b14efb6f7801", + "value": 9560764, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 46 + } + } + ], + [], + [ + { + "key": "d0cc2c148987762d8695de17558c981b15ba1ca388ae75e2636b1e95e94f1904", + "value": 9551204, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 47 + } + } + ], + [], + [ + { + "key": "c6e10ecb14ff6017fbdc6a7d3d0ca80b7750542415d6e41631aedf045191ff04", + "value": 9541654, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 48 + } + } + ], + [], + [ + { + "key": "c0af1c4a0a215be40a55bd8aae96ceab959535a28941b31de92d51315c58c00c", + "value": 9532113, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 49 + } + } + ], + [], + [ + { + "key": "c0be49720b4c598e31f42295dafb292c4adce44d59ce16ffb283027a96e8b306", + "value": 9522582, + "features": { + "flags": { + "bits": 1 + }, + "maturity": 50 + } + } + ] + ] +} \ No newline at end of file diff --git a/base_layer/core/tests/mod.rs b/base_layer/core/tests/mod.rs new file mode 100644 index 0000000000..83b0045954 --- /dev/null +++ b/base_layer/core/tests/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//#[macro_use] +// extern crate arrayref; +// pub mod support; +// pub mod tests; diff --git a/infrastructure/merklemountainrange/tests/support/mod.rs b/base_layer/core/tests/support/mod.rs similarity index 97% rename from infrastructure/merklemountainrange/tests/support/mod.rs rename to base_layer/core/tests/support/mod.rs index 4baebf2ba1..1aed6de008 100644 --- a/infrastructure/merklemountainrange/tests/support/mod.rs +++ b/base_layer/core/tests/support/mod.rs @@ -20,5 +20,4 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub mod hashvalues; -pub mod testobject; +pub mod simple_block_chain; diff --git a/base_layer/core/tests/support/simple_block_chain.rs b/base_layer/core/tests/support/simple_block_chain.rs new file mode 100644 index 0000000000..65c13433ef --- /dev/null +++ b/base_layer/core/tests/support/simple_block_chain.rs @@ -0,0 +1,480 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use chrono::Duration; + +use digest::Digest; +use merklemountainrange::mmr::*; +use rand::{CryptoRng, OsRng, Rng}; +use serde::{Deserialize, Serialize}; +use std::{fs::File, io::prelude::*}; +use tari_core::{ + blocks::{block::*, blockheader::*}, + consensus::ConsensusRules, + fee::Fee, + tari_amount::MicroTari, + transaction::*, + transaction_protocol::{ + build_challenge, + sender::*, + single_receiver::SingleReceiverTransactionProtocol, + TransactionMetadata, + }, + types::*, +}; + +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + common::Blake256, + keys::{PublicKey, SecretKey}, + range_proof::RangeProofService, + ristretto::*, +}; +use tari_utilities::{hash::Hashable, ByteArray}; + +/// This struct is used to keep track of what the value and private key of a UTXO is. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct SpendInfo { + pub key: PrivateKey, + pub value: MicroTari, + pub features: OutputFeatures, +} + +impl SpendInfo { + pub fn new(key: PrivateKey, value: MicroTari, features: OutputFeatures) -> SpendInfo { + SpendInfo { key, value, features } + } +} + +/// This is used to represent a block chain in memory for testing purposes +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct SimpleBlockChain { + pub blocks: Vec, + pub spending_keys: Vec>, +} + +/// This is used to represent a block chain in memory for testing purposes +pub struct SimpleBlockChainBuilder { + blockchain: SimpleBlockChain, + headers: MerkleMountainRange, + utxos: MerkleMountainRange, + kernels: MerkleMountainRange, + rangeproofs: MerkleMountainRange, + rules: ConsensusRules, +} + +impl SimpleBlockChainBuilder { + /// This will create a new test block_chain with a Genesis block + pub fn new() -> SimpleBlockChainBuilder { + let mut chain = SimpleBlockChainBuilder { + blockchain: Default::default(), + headers: Default::default(), + utxos: Default::default(), + kernels: Default::default(), + rangeproofs: Default::default(), + rules: ConsensusRules::current(), + }; + let mut rng = OsRng::new().unwrap(); + // create Genesis block + chain.add_block(&mut rng, Vec::new()); + chain + } + + /// This will create a new test block_chain with random txs spending all the utxo's at the spend height + pub fn new_with_spending(block_amount: u64, spending_height: u64) -> SimpleBlockChainBuilder { + let mut chain = SimpleBlockChainBuilder::new(); + + let mut rng = OsRng::new().unwrap(); + // create gen block + let priv_key = PrivateKey::random(&mut rng); + chain.blockchain.spending_keys.push(vec![SpendInfo::new( + priv_key.clone(), + chain.rules.emission_schedule().block_reward(0), + OutputFeatures::create_coinbase(0, &chain.rules), + )]); + let (cb_utxo, cb_kernel) = create_coinbase(priv_key, 0, 0.into(), &chain.rules); + let block = BlockBuilder::new().with_coinbase_utxo(cb_utxo, cb_kernel).build(); + chain.processes_new_block(block); + + // lets mine some empty blocks + if spending_height > 1 { + chain.add_empty_blocks(&mut rng, spending_height - 1); + } + + // lets mine some more blocks, but spending the utxo's in the older blocks + for i in spending_height..(block_amount) { + chain.blockchain.spending_keys.push(Vec::new()); + let (tx, mut spends) = chain.spend_block_utxos((i - spending_height) as usize); + chain.add_block(&mut rng, tx); + chain.blockchain.spending_keys[i as usize].append(&mut spends); + } + chain + } + + /// This will add empty blocks to the chain + pub fn add_empty_blocks(&mut self, rng: &mut R, count: u64) { + for _ in 0..count { + self.add_block(rng, Vec::new()) + } + } + + /// Add a block to the chain with the given metadata + fn add_block(&mut self, rng: &mut R, tx: Vec) { + let priv_key = PrivateKey::random(rng); + let height = self.blockchain.blocks.len() as u64; + let header = if height > 0 { + self.generate_new_header() + } else { + self.generate_genesis_block_header() + }; + let total_fee = tx + .iter() + .fold(MicroTari::default(), |tot, tx| tot + tx.get_body().get_total_fee()); + let (cb_utxo, cb_kernel) = create_coinbase(priv_key.clone(), header.height, total_fee, &self.rules); + self.blockchain.spending_keys.push(vec![SpendInfo::new( + priv_key, + self.rules.emission_schedule().block_reward(height) + total_fee, + OutputFeatures::create_coinbase(height, &self.rules), + )]); + let block = BlockBuilder::new() + .with_header(header) + .with_coinbase_utxo(cb_utxo, cb_kernel) + .with_transactions(tx) + .build(); + self.processes_new_block(block); + } + + /// This will blocks to the chain with random txs spending all the utxo's at the spend height + pub fn add_with_spending(&mut self, block_amount: u64, spending_height: u64) { + let mut rng = OsRng::new().unwrap(); + let len = self.blockchain.blocks.len() as u64; + let mut blocks_added = 0; + if len < spending_height { + self.add_empty_blocks(&mut rng, spending_height - len); + blocks_added += 1; + }; + // lets mine some more blocks, but spending the utxo's in the older blocks + let len = self.blockchain.blocks.len() as u64; + for i in len..(len + block_amount - blocks_added) { + self.blockchain.spending_keys.push(Vec::new()); + let (tx, mut spends) = self.spend_block_utxos((i - spending_height) as usize); + self.add_block(&mut rng, tx); + self.blockchain.spending_keys[i as usize].append(&mut spends); + } + } + + /// This function will just add the content of the block to the MMR's + fn processes_new_block(&mut self, block: Block) { + println!("Proc block nr: {:?}", self.blockchain.blocks.len()); + self.headers + .push(block.header.clone()) + .expect("failed to add header to test chain"); + self.kernels + .append(block.body.kernels.clone()) + .expect("failed to add kernels to test chain"); + + for input in &block.body.inputs { + let hash = input.clone().hash(); + self.utxos + .prune_object_hash(&hash) + .expect("failed to add pruned inputs"); + } + for output in &block.body.outputs { + self.rangeproofs + .push(output.clone().proof) + .expect("failed to add proofs to test chain"); + self.utxos + .push(output.clone().into()) + .expect("failed to add outputs to test chain"); + } + self.blockchain.blocks.push(block); + } + + fn generate_genesis_block_header(&self) -> BlockHeader { + BlockHeader::new(self.rules.blockchain_version()) + } + + /// This function will generate a new header, assuming it will follow on the last created block. + fn generate_new_header(&self) -> BlockHeader { + let counter = self.blockchain.blocks.len() - 1; + let mut hash = [0; 32]; + hash.copy_from_slice(&self.blockchain.blocks[counter].header.hash()); + let mut hasher = SignatureHasher::new(); + hasher.input(&self.utxos.get_merkle_root()[..]); + hasher.input(&self.utxos.get_unpruned_hash()); + let output_mr = hasher.result().to_vec(); + let kernal_mmr = self.kernels.get_merkle_root(); + let rr_mmr = self.rangeproofs.get_merkle_root(); + BlockHeader { + version: self.rules.blockchain_version(), + height: self.blockchain.blocks[counter].header.height + 1, + prev_hash: hash, + timestamp: self.blockchain.blocks[counter] + .header + .timestamp + .clone() + .checked_add_signed(Duration::minutes(1)) + .unwrap(), + output_mr: array_ref!(output_mr, 0, 32).clone(), + range_proof_mr: array_ref!(rr_mmr, 0, 32).clone(), + kernel_mr: array_ref!(kernal_mmr, 0, 32).clone(), + total_kernel_offset: RistrettoSecretKey::from(0), + pow: ProofOfWork { + work: self.blockchain.blocks[counter].header.pow.work + 1, + }, + } + } + + /// This function will spend the utxo's in the mentioned block + fn spend_block_utxos(&mut self, block_index: usize) -> (Vec, Vec) { + let utxo_count = self.blockchain.spending_keys[block_index as usize].len(); + let mut txs = Vec::new(); + let mut spends = Vec::new(); + let mut counter = 0; + for i in 0..utxo_count { + let result = self.create_tx(block_index, i, &mut counter); + if result.is_some() { + let (tx, mut spending_info) = result.unwrap(); + txs.push(tx); + spends.append(&mut spending_info) + } + } + (txs, spends) + } + + /// This function will create a new transaction, spending the utxo specified by the block and utxo index + fn create_tx( + &self, + block_index: usize, + utxo_index: usize, + counter: &mut usize, + ) -> Option<(Transaction, Vec)> + { + let mut rng = OsRng::new().unwrap(); + let mut spend_info = Vec::new(); + + // create keys + let old_spend_key = self.blockchain.spending_keys[block_index][utxo_index].key.clone(); + let new_spend_key = PrivateKey::random(&mut rng); + let new_spend_key2 = PrivateKey::random(&mut rng); + // create values + let old_value = self.blockchain.spending_keys[block_index][utxo_index].value; + if old_value <= MicroTari(100) || *counter > 4 { + // we dont want to keep dividing for ever on a single utxo, or create very large blocks + return None; + } + let new_value = self.blockchain.spending_keys[block_index][utxo_index].value / 2; + let fee = Fee::calculate(20.into(), 1, 2); + if (new_value + fee + MicroTari(100)) >= (old_value) { + // we dont want values smaller than 100 micro tari + return None; + } + let new_value2 = old_value - new_value - fee; + + // save spend info + spend_info.push(SpendInfo::new( + new_spend_key.clone(), + new_value, + OutputFeatures::default(), + )); + spend_info.push(SpendInfo::new( + new_spend_key2.clone(), + new_value2, + OutputFeatures::default(), + )); + + // recreate input commitment + let v_input = PrivateKey::from(old_value); + let commitment_in = COMMITMENT_FACTORY.commit(&old_spend_key, &v_input); + let input = TransactionInput::new( + self.blockchain.spending_keys[block_index][utxo_index].features.clone(), + commitment_in, + ); + // create unblinded value + let old_value = UnblindedOutput::new(old_value, old_spend_key, None); + + // generate kernel stuff + let sender_offset = PrivateKey::random(&mut rng); + let sender_r = PrivateKey::random(&mut rng); + let receiver_r = PrivateKey::random(&mut rng); + let mut builder = SenderTransactionProtocol::builder(1); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(sender_offset) + .with_private_nonce(sender_r) + .with_change_secret(new_spend_key.clone()) + .with_input(input, old_value) + .with_amount(0, new_value2); + let mut sender = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + + let msg = sender.build_single_round_message().unwrap(); + let receiver_info = SingleReceiverTransactionProtocol::create( + &msg, + receiver_r, + new_spend_key2, + OutputFeatures::default(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + + sender + .add_single_recipient_info(receiver_info.clone(), &PROVER) + .unwrap(); + match sender.finalize(KernelFeatures::empty(), &PROVER, &COMMITMENT_FACTORY) { + Ok(true) => (), + _ => { + return None; + }, + }; + + let tx = sender.get_transaction().unwrap().clone(); + *counter += 1; + Some((tx, spend_info)) + } +} + +impl Default for SimpleBlockChain { + fn default() -> Self { + SimpleBlockChain { + blocks: Vec::new(), + spending_keys: Vec::new(), + } + } +} + +/// This function will create a coinbase from the provided secret key. The coinbase will be added to the outputs and +/// kernels. +fn create_coinbase( + key: PrivateKey, + height: u64, + total_fee: MicroTari, + rules: &ConsensusRules, +) -> (TransactionOutput, TransactionKernel) +{ + let mut rng = rand::OsRng::new().unwrap(); + // build output + let amount = total_fee + rules.emission_schedule().block_reward(height); + let v = PrivateKey::from(u64::from(amount)); + let commitment = COMMITMENT_FACTORY.commit(&key, &v); + let rr = PROVER.construct_proof(&key, amount.into()).unwrap(); + let output = TransactionOutput::new( + OutputFeatures::create_coinbase(height, rules), + commitment, + RangeProof::from_bytes(&rr).unwrap(), + ); + + // create kernel + let tx_meta = TransactionMetadata { + fee: 0.into(), + lock_height: 0, + }; + let r = PrivateKey::random(&mut rng); + let e = build_challenge(&PublicKey::from_secret_key(&r), &tx_meta); + let s = Signature::sign(key.clone(), r, &e).unwrap(); + let excess = COMMITMENT_FACTORY.commit_value(&key, 0); + let kernel = KernelBuilder::new() + .with_features(KernelFeatures::COINBASE_KERNEL) + .with_fee(0.into()) + .with_lock_height(0) + .with_excess(&excess) + .with_signature(&s) + .build() + .unwrap(); + (output, kernel) +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs; + + //#[test] + fn create_simple_block_chain() { + let mut rng = rand::OsRng::new().unwrap(); + let mut chain = SimpleBlockChainBuilder::new(); + assert_eq!(chain.blockchain.blocks.len(), 1); + chain.add_empty_blocks(&mut rng, 5); + assert_eq!(chain.blockchain.blocks.len(), 6); + + // Check that the blocks form a chain + assert_eq!(chain.blockchain.blocks[0].header.height, 0); + for i in 1..chain.blockchain.blocks.len() { + let mut hash = [0; 32]; + hash.copy_from_slice(&chain.blockchain.blocks[i - 1].header.hash()); + assert_eq!(chain.blockchain.blocks[i].header.prev_hash, hash); + assert_eq!(chain.blockchain.blocks[i].header.height, i as u64); + } + } + + // we dont want to run this function function every time as it basically tests, test code and it runs slow. + // #[test] + #[allow(dead_code)] + fn create_simple_block_chain_with_spend() { + let mut chain = SimpleBlockChainBuilder::new_with_spending(5, 1); + assert_eq!(chain.blockchain.blocks.len(), 5); + chain.add_with_spending(5, 1); + assert_eq!(chain.blockchain.blocks.len(), 10); + + assert_eq!(chain.blockchain.blocks[0].header.height, 0); + for i in 1..10 { + let mut hash = [0; 32]; + hash.copy_from_slice(&chain.blockchain.blocks[i - 1].header.hash()); + assert_eq!(chain.blockchain.blocks[i].header.prev_hash, hash); + assert_eq!(chain.blockchain.blocks[i].header.height, i as u64); + for input in &chain.blockchain.blocks[i].body.inputs { + assert!(chain.blockchain.blocks[i - 1] + .body + .outputs + .iter() + .any(|x| x.commitment == input.commitment)); + } + } + } + + // we dont want to run this function function every time as it basically tests, test code and it runs slow. + //#[test] + #[allow(dead_code)] + fn test_json_file() { + let mut chain = SimpleBlockChainBuilder::new_with_spending(5, 1); + chain.add_with_spending(5, 1); + let mut file = File::create("tests/chain/test_chain.json").unwrap(); + let json = serde_json::to_string_pretty(&chain.blockchain).unwrap(); + file.write_all(json.as_bytes()).unwrap(); + let read_json = fs::read_to_string("tests/chain/test_chain.json").unwrap(); + let blockchain: SimpleBlockChain = serde_json::from_str(&read_json).unwrap(); + assert_eq!(blockchain, chain.blockchain); + fs::remove_file("tests/chain/test_chain.json").unwrap(); + } + + // we dont want to run this function function every time as it create a test file for use in testing + //#[test] + #[allow(dead_code)] + fn create_json_file() { + let mut chain = SimpleBlockChainBuilder::new_with_spending(5, 1); + chain.add_with_spending(45, 1); + let mut file = File::create("tests/chain/chain.json").unwrap(); + let json = serde_json::to_string_pretty(&chain.blockchain).unwrap(); + file.write_all(json.as_bytes()).unwrap(); + } +} diff --git a/base_layer/core/tests/tests/block_validation.rs b/base_layer/core/tests/tests/block_validation.rs new file mode 100644 index 0000000000..e624e19274 --- /dev/null +++ b/base_layer/core/tests/tests/block_validation.rs @@ -0,0 +1,41 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support::simple_block_chain::*; +use std::fs; +use tari_core::consensus::ConsensusRules; + +fn load_test_block_chain_from_file() -> SimpleBlockChain { + let read_json = fs::read_to_string("tests/chain/chain.json").unwrap(); + let blockchain: SimpleBlockChain = serde_json::from_str(&read_json).unwrap(); + blockchain +} +//#[test] +fn test_valid_blocks() { + let rules = ConsensusRules::current(); + let chain = load_test_block_chain_from_file(); + for i in 0..chain.blocks.len() { + chain.blocks[i] + .check_internal_consistency(&rules) + .expect("Block validation failed") + } +} diff --git a/infrastructure/merklemountainrange/tests/mod.rs b/base_layer/core/tests/tests/mod.rs similarity index 98% rename from infrastructure/merklemountainrange/tests/mod.rs rename to base_layer/core/tests/tests/mod.rs index 009e40efdc..f86a94703f 100644 --- a/infrastructure/merklemountainrange/tests/mod.rs +++ b/base_layer/core/tests/tests/mod.rs @@ -20,5 +20,4 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -mod support; -mod unit; +mod block_validation; diff --git a/base_layer/keymanager/Cargo.toml b/base_layer/keymanager/Cargo.toml index 51de991fd3..31bd954356 100644 --- a/base_layer/keymanager/Cargo.toml +++ b/base_layer/keymanager/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "keymanager" -version = "0.0.1" +name = "tari_key_manager" +version = "0.0.5" edition = "2018" [dependencies] -tari_crypto = { version = "0.0.1" } -tari_utilities = { version = "0.0.1" } +tari_crypto = { path = "../../infrastructure/crypto"} +tari_utilities = { path = "../../infrastructure/tari_util"} rand = "0.5.5" digest = "0.8.0" sha2 = "0.8.0" diff --git a/base_layer/keymanager/src/diacritics.rs b/base_layer/keymanager/src/diacritics.rs index 1798ca902c..26e5a03379 100644 --- a/base_layer/keymanager/src/diacritics.rs +++ b/base_layer/keymanager/src/diacritics.rs @@ -1,5 +1,5 @@ /// Remove diacritic marks, points and accents on lowercase characters -pub fn remove_diacritics(word: &String) -> String { +pub fn remove_diacritics(word: &str) -> String { // Replace diacritics accents let clean_string: String = word.to_lowercase() diff --git a/base_layer/keymanager/src/file_backup.rs b/base_layer/keymanager/src/file_backup.rs index 83286f9dcc..0b42fa2716 100644 --- a/base_layer/keymanager/src/file_backup.rs +++ b/base_layer/keymanager/src/file_backup.rs @@ -43,15 +43,15 @@ pub enum FileError { } pub trait FileBackup { - fn from_file(filename: &String) -> Result; - fn to_file(&self, filename: &String) -> Result<(), FileError>; + fn from_file(filename: &str) -> Result; + fn to_file(&self, filename: &str) -> Result<(), FileError>; } impl FileBackup for T where T: serde::Serialize + DeserializeOwned { /// Load struct state from backup file - fn from_file(filename: &String) -> Result { + fn from_file(filename: &str) -> Result { let mut file_handle = match File::open(&filename) { Ok(file) => file, Err(_e) => return Err(FileError::FileOpen), @@ -67,7 +67,7 @@ where T: serde::Serialize + DeserializeOwned } /// Backup struct state in file specified by filename - fn to_file(&self, filename: &String) -> Result<(), FileError> { + fn to_file(&self, filename: &str) -> Result<(), FileError> { match File::create(filename) { Ok(mut file_handle) => match serde_json::to_string(&self) { Ok(json_data) => match file_handle.write_all(json_data.as_bytes()) { diff --git a/base_layer/keymanager/src/keymanager.rs b/base_layer/keymanager/src/keymanager.rs index 41a1124d9c..31679e74d0 100644 --- a/base_layer/keymanager/src/keymanager.rs +++ b/base_layer/keymanager/src/keymanager.rs @@ -28,7 +28,7 @@ use serde::de::DeserializeOwned; use serde_derive::{Deserialize, Serialize}; use std::marker::PhantomData; use tari_crypto::keys::SecretKey; -use tari_utilities::byte_array::ByteArrayError; +use tari_utilities::{byte_array::ByteArrayError, hex::Hex}; #[derive(Debug, Error)] pub enum KeyManagerError { diff --git a/base_layer/keymanager/src/mnemonic.rs b/base_layer/keymanager/src/mnemonic.rs index efb0e412d8..51fc156afb 100644 --- a/base_layer/keymanager/src/mnemonic.rs +++ b/base_layer/keymanager/src/mnemonic.rs @@ -58,13 +58,13 @@ pub enum MnemonicLanguage { impl MnemonicLanguage { /// Detects the mnemonic language of a specific word by searching all defined mnemonic word lists - pub fn from(mnemonic_word: &String) -> Result { + pub fn from(mnemonic_word: &str) -> Result { for language in MnemonicLanguage::iterator() { if find_mnemonic_index_from_word(mnemonic_word, &language).is_ok() { return Ok((*language).clone()); } } - return Err(MnemonicError::UnknownLanguage); + Err(MnemonicError::UnknownLanguage) } /// Returns an iterator for the MnemonicLanguage enum group to allow iteration over all defined languages @@ -78,12 +78,12 @@ impl MnemonicLanguage { MnemonicLanguage::Korean, MnemonicLanguage::Spanish, ]; - (MNEMONIC_LANGUAGES.into_iter()) + (MNEMONIC_LANGUAGES.iter()) } } /// Finds and returns the index of a specific word in a mnemonic word list defined by the specified language -fn find_mnemonic_index_from_word(word: &String, language: &MnemonicLanguage) -> Result { +fn find_mnemonic_index_from_word(word: &str, language: &MnemonicLanguage) -> Result { let search_result: Result; let lowercase_word = word.to_lowercase(); match language { @@ -180,7 +180,7 @@ pub fn to_bytes_with_language( match find_mnemonic_index_from_word(curr_word, &language) { Ok(index) => { let curr_bits = uint_to_bits(index, 11); - bits.extend(curr_bits.iter().map(|&i| i)); + bits.extend(curr_bits.iter().cloned()); }, Err(err) => return Err(err), } diff --git a/base_layer/keymanager/src/mnemonic_wordlists.rs b/base_layer/keymanager/src/mnemonic_wordlists.rs index 71a75530e0..49793e09ea 100644 --- a/base_layer/keymanager/src/mnemonic_wordlists.rs +++ b/base_layer/keymanager/src/mnemonic_wordlists.rs @@ -5,7 +5,7 @@ /// A sorted mnemonic word list of 2048 characters for the Chinese Simplified language #[rustfmt::skip] -pub const MNEMONIC_CHINESE_SIMPLIFIED_WORDS: [&'static str; 2048] = [ +pub const MNEMONIC_CHINESE_SIMPLIFIED_WORDS: [&str; 2048] = [ "一", "丁", "七", "万", "丈", "三", "上", "下", "不", "与", "专", "且", "世", "丘", "丙", "业", "丛", "东", "丝", "丢", "两", "严", "丧", "个", "中", "丰", "串", "临", "丹", "为", "主", "丽", "举", "乃", "久", "么", "义", "之", "乌", "乎", "乏", "乐", "乔", "乘", "乙", "九", "也", "习", @@ -138,7 +138,7 @@ pub const MNEMONIC_CHINESE_SIMPLIFIED_WORDS: [&'static str; 2048] = [ /// A sorted mnemonic word list of 2048 words from the English language #[rustfmt::skip] -pub const MNEMONIC_ENGLISH_WORDS: [&'static str; 2048] = [ +pub const MNEMONIC_ENGLISH_WORDS: [&str; 2048] = [ "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", @@ -271,7 +271,7 @@ pub const MNEMONIC_ENGLISH_WORDS: [&'static str; 2048] = [ /// A sorted mnemonic word list of 2048 words from the French language #[rustfmt::skip] -pub const MNEMONIC_FRENCH_WORDS: [&'static str; 2048] = [ +pub const MNEMONIC_FRENCH_WORDS: [&str; 2048] = [ "abaisser", "abandon", "abdiquer", "abeille", "abolir", "aborder", "aboutir", "aboyer", "abrasif", "abreuver", "abriter", "abroger", "abrupt", "absence", "absolu", "absurde", "abusif", "abyssal", "academie", "acajou", "acarien", "accabler", "accepter", "acclamer", "accolade", "accroche", "accuser", "acerbe", "achat", "acheter", "aciduler", "acier", "acompte", "acquerir", "acronyme", "acteur", "actif", "actuel", "adepte", "adequat", "adhesif", "adjectif", "adjuger", "admettre", "admirer", "adopter", "adorer", "adoucir", @@ -404,7 +404,7 @@ pub const MNEMONIC_FRENCH_WORDS: [&'static str; 2048] = [ /// A sorted mnemonic word list of 2048 words from the Italian language #[rustfmt::skip] -pub const MNEMONIC_ITALIAN_WORDS: [&'static str; 2048] = [ +pub const MNEMONIC_ITALIAN_WORDS: [&str; 2048] = [ "abaco", "abbaglio", "abbinato", "abete", "abisso", "abolire", "abrasivo", "abrogato", "accadere", "accenno", "accusato", "acetone", "achille", "acido", "acqua", "acre", "acrilico", "acrobata", "acuto", "adagio", "addebito", "addome", "adeguato", "aderire", "adipe", "adottare", "adulare", "affabile", "affetto", "affisso", "affranto", "aforisma", "afoso", "africano", "agave", "agente", "agevole", "aggancio", "agire", "agitare", "agonismo", "agricolo", "agrumeto", "aguzzo", "alabarda", "alato", "albatro", "alberato", @@ -537,7 +537,7 @@ pub const MNEMONIC_ITALIAN_WORDS: [&'static str; 2048] = [ /// A sorted mnemonic word list of 2048 words from the Japanese language #[rustfmt::skip] -pub const MNEMONIC_JAPANESE_WORDS: [&'static str; 2048] = [ +pub const MNEMONIC_JAPANESE_WORDS: [&str; 2048] = [ "あいこくしん", "あいさつ", "あいだ", "あおぞら", "あかちゃん", "あきる", "あけがた", "あける", "あこがれる", "あさい", "あさひ", "あしあと", "あじわう", "あずかる", "あずき", "あそぶ", "あたえる", "あたためる", "あたりまえ", "あたる", "あっしゅく", "あつい", "あつかう", "あつまり", "あつめる", "あてな", "あてはまる", "あひる", "あふれる", "あぶら", "あぶる", "あまい", "あまど", "あまやかす", "あまり", "あみもの", "あめりか", "あやまる", "あゆむ", "あらいぐま", "あらし", "あらすじ", "あらためる", "あらゆる", "あらわす", "ありがとう", "あわせる", "あわてる", @@ -670,7 +670,7 @@ pub const MNEMONIC_JAPANESE_WORDS: [&'static str; 2048] = [ /// A sorted mnemonic word list of 2048 words from the Korean language #[rustfmt::skip] -pub const MNEMONIC_KOREAN_WORDS: [&'static str; 2048] = [ +pub const MNEMONIC_KOREAN_WORDS: [&str; 2048] = [ "가격", "가끔", "가난", "가능", "가득", "가르침", "가뭄", "가방", "가상", "가슴", "가운데", "가을", "가이드", "가입", "가장", "가정", "가족", "가죽", "각오", "각자", "간격", "간부", "간섭", "간장", "간접", "간판", "갈등", "갈비", "갈색", "갈증", "감각", "감기", "감소", "감수성", "감자", "감정", "갑자기", "강남", "강당", "강도", "강력히", "강변", "강북", "강사", "강수량", "강아지", "강원도", "강의", @@ -803,7 +803,7 @@ pub const MNEMONIC_KOREAN_WORDS: [&'static str; 2048] = [ /// A sorted mnemonic word list of 2048 words from the Spanish language #[rustfmt::skip] -pub const MNEMONIC_SPANISH_WORDS: [&'static str; 2048] = [ +pub const MNEMONIC_SPANISH_WORDS: [&str; 2048] = [ "abaco", "abdomen", "abeja", "abierto", "abogado", "abono", "aborto", "abrazo", "abrir", "abuelo", "abuso", "acabar", "academia", "acceso", "accion", "aceite", "acelga", "acento", "aceptar", "acido", "aclarar", "acne", "acoger", "acoso", "activo", "acto", "actriz", "actuar", "acudir", "acuerdo", "acusar", "adicto", "admitir", "adoptar", "adorno", "aduana", "adulto", "aereo", "afectar", "aficion", "afinar", "afirmar", "agil", "agitar", "agonia", "agosto", "agotar", "agregar", diff --git a/base_layer/mempool/Cargo.toml b/base_layer/mempool/Cargo.toml deleted file mode 100644 index e9ce8fb7e2..0000000000 --- a/base_layer/mempool/Cargo.toml +++ /dev/null @@ -1,5 +0,0 @@ -[package] -name = "mempool" -version = "0.0.1" - -[dependencies] diff --git a/base_layer/mempool/src/lib.rs b/base_layer/mempool/src/lib.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/base_layer/mempool/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/base_layer/mining/Cargo.toml b/base_layer/mining/Cargo.toml index c9a6998b14..892e9252fd 100644 --- a/base_layer/mining/Cargo.toml +++ b/base_layer/mining/Cargo.toml @@ -1,5 +1,5 @@ [package] name = "mining" -version = "0.0.1" +version = "0.0.5" [dependencies] diff --git a/base_layer/mmr/Cargo.toml b/base_layer/mmr/Cargo.toml new file mode 100644 index 0000000000..a3383476a9 --- /dev/null +++ b/base_layer/mmr/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "tari_mmr" +version = "0.0.5" +edition = "2018" + +[dependencies] +tari_utilities = { path = "../../infrastructure/tari_util", version = "^0.0" } +derive-error = "0.0.4" +digest = "0.8.0" +log = "0.4" +serde = { version = "1.0.97", features = ["derive"] } +croaring = "0.4.0" +tari_storage = { path = "../../infrastructure/storage", version = "^0.0" } + +[dev-dependencies] +criterion = "0.2" +rand="0.7.0" +blake2 = "0.8.0" +tari_infra_derive= {path = "../../infrastructure/derive"} +tari_crypto = {path = "../../infrastructure/crypto"} +serde_json = "1.0" +bincode = "1.1" +[lib] +# Disable libtest from intercepting Criterion bench arguments +bench = false + +[[bench]] +name = "bench" +harness = false diff --git a/infrastructure/merklemountainrange/README.md b/base_layer/mmr/README.md similarity index 100% rename from infrastructure/merklemountainrange/README.md rename to base_layer/mmr/README.md diff --git a/base_layer/mmr/benches/bench.rs b/base_layer/mmr/benches/bench.rs new file mode 100644 index 0000000000..fec253ed9d --- /dev/null +++ b/base_layer/mmr/benches/bench.rs @@ -0,0 +1,57 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +use blake2::Blake2b; +use criterion::{criterion_group, criterion_main, Criterion}; +use digest::Digest; +use std::{time::Duration, u64}; +use tari_mmr::{MerkleMountainRange, VectorBackend}; + +fn get_hashes(n: usize) -> Vec> { + let mut result = Vec::with_capacity(n); + for i in 0..n { + let h = Blake2b::digest(&i.to_le_bytes()).to_vec(); + result.push(h); + } + result +} + +fn build_mmr(c: &mut Criterion) { + c.bench_function("Build MMR", move |b| { + let hashes = get_hashes(1000); + let mut mmr = MerkleMountainRange::::new(VectorBackend::default()); + b.iter(|| { + for i in 0..1000 { + let _ = mmr.push(&hashes[i]); + } + }); + }); +} + +criterion_group!( + name = mmr; + config= Criterion::default().warm_up_time(Duration::from_millis(500)).sample_size(10); + targets= build_mmr +); + +criterion_main!(mmr); diff --git a/base_layer/mmr/src/backend.rs b/base_layer/mmr/src/backend.rs new file mode 100644 index 0000000000..23375c3de1 --- /dev/null +++ b/base_layer/mmr/src/backend.rs @@ -0,0 +1,90 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::error::MerkleMountainRangeError; + +/// A trait describing generic array-like behaviour, without imposing any specific details on how this is actually done. +pub trait ArrayLike { + type Value; + type Error: std::error::Error; + + /// Returns the number of hashes stored in the backend + fn len(&self) -> usize; + + /// Store a new item and return the index of the stored item + fn push(&mut self, item: Self::Value) -> Result; + + /// Return the item at the given index + fn get(&self, index: usize) -> Option<&Self::Value>; + + /// Return the item at the given index. Use this if you *know* that the index is valid. Requesting a hash for an + /// invalid index may cause the a panic + fn get_or_panic(&self, index: usize) -> &Self::Value; +} + +pub trait ArrayLikeExt { + type Value; + + /// Shortens the array, keeping the first len elements and dropping the rest. + fn truncate(&mut self, _len: usize) -> Result<(), MerkleMountainRangeError>; + + /// Execute the given closure for each value in the array + fn for_each(&self, f: F) -> Result<(), MerkleMountainRangeError> + where F: FnMut(Result<&Self::Value, MerkleMountainRangeError>); +} + +impl ArrayLike for Vec { + type Error = MerkleMountainRangeError; + type Value = T; + + fn len(&self) -> usize { + Vec::len(self) + } + + fn push(&mut self, item: Self::Value) -> Result { + Vec::push(self, item); + Ok(self.len() - 1) + } + + fn get(&self, index: usize) -> Option<&Self::Value> { + (self as &[Self::Value]).get(index) + } + + fn get_or_panic(&self, index: usize) -> &Self::Value { + &self[index] + } +} + +impl ArrayLikeExt for Vec { + type Value = T; + + fn truncate(&mut self, len: usize) -> Result<(), MerkleMountainRangeError> { + self.truncate(len); + Ok(()) + } + + fn for_each(&self, f: F) -> Result<(), MerkleMountainRangeError> + where F: FnMut(Result<&Self::Value, MerkleMountainRangeError>) { + self.iter().map(|v| Ok(v)).for_each(f); + Ok(()) + } +} diff --git a/base_layer/mmr/src/change_tracker.rs b/base_layer/mmr/src/change_tracker.rs new file mode 100644 index 0000000000..b7be1f2769 --- /dev/null +++ b/base_layer/mmr/src/change_tracker.rs @@ -0,0 +1,274 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + backend::{ArrayLike, ArrayLikeExt}, + error::MerkleMountainRangeError, + pruned_mmr::{prune_mutable_mmr, PrunedMutableMmr}, + Hash, + MutableMmr, +}; +use croaring::Bitmap; +use digest::Digest; +use std::{mem, ops::Deref}; + +/// A struct that wraps an MMR to keep track of changes to the MMR over time. This enables one to roll +/// back changes to a point in history. Think of `MerkleChangeTracker` as 'git' for MMRs. +/// +/// [MutableMMr] implements [std::ops::Deref], so that once you've wrapped the MMR, all the immutable methods are +/// available through the auto-dereferencing. +/// +/// The basic philosophy of `MerkleChangeTracker` is as follows: +/// * Start with a 'base' MMR. For efficiency, you usually want to make this a [pruned_mmr::PrunedMmr], but it +/// doesn't have to be. +/// * We then maintain a change-list for every append and delete that is made on the MMR. +/// * You can `commit` the change-set at any time, which will create a new [MerkleCheckPoint] summarising the +/// changes, and the current change-set is reset. +/// * You can `rewind` to a previously committed checkpoint, p. This entails resetting the MMR to the base state and +/// then replaying every checkpoint in sequence until checkpoint p is reached. `rewind_to_start` and `replay` perform +/// similar functions. +/// * You can `reset` the ChangeTracker, which clears the current change-set and moves you back to the most recent +/// checkpoint ('HEAD') +pub struct MerkleChangeTracker +where + D: Digest, + BaseBackend: ArrayLike, +{ + base: MutableMmr, + mmr: PrunedMutableMmr, + checkpoints: CpBackend, + // The hashes added since the last commit + current_additions: Vec, + // The deletions since the last commit + current_deletions: Bitmap, +} + +impl MerkleChangeTracker +where + D: Digest, + BaseBackend: ArrayLike, + CpBackend: ArrayLike + ArrayLikeExt, +{ + /// Wrap an MMR inside a change tracker. + /// + /// # Parameters + /// * `base`: The base, or anchor point of the change tracker. This represents the earliest point that you can + /// [MerkleChangeTracker::rewind] to. + /// * `mmr`: An empty MMR instance that will be used to maintain the current state of the MMR. + /// * `diffs`: The (usually empty) collection of diffs that will be used to store the MMR checkpoints. + /// + /// # Returns + /// A new `MerkleChangeTracker` instance that is configured using the MMR and ChangeTracker instances provided. + pub fn new( + base: MutableMmr, + diffs: CpBackend, + ) -> Result, MerkleMountainRangeError> + { + let mmr = prune_mutable_mmr::(&base)?; + Ok(MerkleChangeTracker { + base, + mmr, + checkpoints: diffs, + current_additions: Vec::new(), + current_deletions: Bitmap::create(), + }) + } + + /// Return the number of Checkpoints this change tracker has recorded + pub fn checkpoint_count(&self) -> usize { + self.checkpoints.len() + } + + /// Push the given hash into the MMR and update the current change-set + pub fn push(&mut self, hash: &Hash) -> Result { + let result = self.mmr.push(hash)?; + self.current_additions.push(hash.clone()); + Ok(result) + } + + /// Discards the current change-set and resets the MMR state to that of the last checkpoint + pub fn reset(&mut self) -> Result<(), MerkleMountainRangeError> { + self.replay(self.checkpoint_count()) + } + + /// Mark a node for deletion and optionally compress the deletion bitmap. See [MutableMmr::delete_and_compress] + /// for more details + pub fn delete_and_compress(&mut self, leaf_node_index: u32, compress: bool) -> bool { + let result = self.mmr.delete_and_compress(leaf_node_index, compress); + if result { + self.current_deletions.add(leaf_node_index) + } + result + } + + /// Mark a node for completion, and compress the roaring bitmap. See [delete_and_compress] for details. + pub fn delete(&mut self, leaf_node_index: u32) -> bool { + self.delete_and_compress(leaf_node_index, true) + } + + /// Compress the roaring bitmap mapping deleted nodes. You never have to call this method unless you have been + /// calling [delete_and_compress] with `compress` set to `false` ahead of a call to [get_merkle_root]. + pub fn compress(&mut self) -> bool { + self.mmr.compress() + } + + /// Commit the change history since the last commit to a new [MerkleCheckPoint] and clear the current change set. + pub fn commit(&mut self) -> Result<(), CpBackend::Error> { + let mut hash_set = Vec::new(); + mem::swap(&mut hash_set, &mut self.current_additions); + let mut deleted_set = Bitmap::create(); + mem::swap(&mut deleted_set, &mut self.current_deletions); + let diff = MerkleCheckPoint::new(hash_set, deleted_set); + self.checkpoints.push(diff)?; + Ok(()) + } + + /// Rewind the MMR state by the given number of Checkpoints. + /// + /// Example: + /// + /// Assuming we start with an empty Mutable MMR, and apply the following: + /// push(1), push(2), delete(1), *Checkpoint* (1) + /// push(3), push(4) *Checkpoint* (2) + /// push(5), delete(4) *Checkpoint* (3) + /// push(6) + /// + /// The state is now: + /// ```text + /// 1 2 3 4 5 6 + /// x x + /// ``` + /// + /// After calling `rewind(1)`, The push of 6 wasn't check-pointed, so it will be discarded, and rewinding back one + /// point to checkpoint 2 the state will be: + /// ```text + /// 1 2 3 4 + /// x + /// ``` + /// + /// Calling `rewind(1)` again will yield: + /// ```text + /// 1 2 + /// x + /// ``` + pub fn rewind(&mut self, steps_back: usize) -> Result<(), MerkleMountainRangeError> { + self.replay(self.checkpoint_count() - steps_back) + } + + /// Rewinds the MMR back to the state of the base MMR; essentially discarding all the history accumulated to date. + pub fn rewind_to_start(&mut self) -> Result<(), MerkleMountainRangeError> { + self.mmr = self.revert_mmr_to_base()?; + Ok(()) + } + + // Common function for rewind_to_start and replay + fn revert_mmr_to_base(&mut self) -> Result, MerkleMountainRangeError> { + let mmr = prune_mutable_mmr::(&self.base)?; + self.current_deletions = Bitmap::create(); + self.current_additions = Vec::new(); + Ok(mmr) + } + + /// Similar to [MerkleChangeTracker::rewind], `replay` moves the MMR state through checkpoints, but uses the base + /// MMR as the starting point and steps forward through `num_checkpoints` checkpoints, rather than rewinding from + /// the current state. + pub fn replay(&mut self, num_checkpoints: usize) -> Result<(), MerkleMountainRangeError> { + let mut mmr = self.revert_mmr_to_base()?; + self.checkpoints.truncate(num_checkpoints)?; + let mut result = Ok(()); + self.checkpoints.for_each(|v| { + if result.is_err() { + return; + } + result = match v { + Ok(cp) => cp.apply(&mut mmr), + Err(e) => Err(e), + }; + })?; + mmr.compress(); + self.mmr = mmr; + result + } + + pub fn get_checkpoint(&self, index: usize) -> Result { + match self.checkpoints.get(index) { + None => Err(MerkleMountainRangeError::OutOfRange), + Some(cp) => Ok(cp.clone()), + } + } +} + +impl Deref for MerkleChangeTracker +where + D: Digest, + BaseBackend: ArrayLike, +{ + type Target = PrunedMutableMmr; + + fn deref(&self) -> &Self::Target { + &self.mmr + } +} + +#[derive(Debug, Clone)] +pub struct MerkleCheckPoint { + nodes_added: Vec, + nodes_deleted: Bitmap, +} + +impl MerkleCheckPoint { + pub fn new(nodes_added: Vec, nodes_deleted: Bitmap) -> MerkleCheckPoint { + MerkleCheckPoint { + nodes_added, + nodes_deleted, + } + } + + /// Apply this checkpoint to the MMR provided. Take care: The `deleted` set is not compressed after returning + /// from here. + fn apply(&self, mmr: &mut MutableMmr) -> Result<(), MerkleMountainRangeError> + where + D: Digest, + B2: ArrayLike, + { + for node in &self.nodes_added { + mmr.push(node)?; + } + mmr.deleted.or_inplace(&self.nodes_deleted); + Ok(()) + } + + /// Return a reference to the hashes of the nodes added in the checkpoint + pub fn nodes_added(&self) -> &Vec { + &self.nodes_added + } + + /// Return a reference to the roaring bitmap of nodes that were deleted in this checkpoint + pub fn nodes_deleted(&self) -> &Bitmap { + &self.nodes_deleted + } + + /// Break a checkpoint up into its constituent parts + pub fn into_parts(self) -> (Vec, Bitmap) { + (self.nodes_added, self.nodes_deleted) + } +} diff --git a/base_layer/mmr/src/common.rs b/base_layer/mmr/src/common.rs new file mode 100644 index 0000000000..6263467b68 --- /dev/null +++ b/base_layer/mmr/src/common.rs @@ -0,0 +1,315 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, +// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. + +use crate::Hash; +use digest::Digest; + +const ALL_ONES: usize = std::usize::MAX; + +/// Returns the MMR index of the nth leaf node +pub fn leaf_index(n: usize) -> usize { + if n == 0 { + return 0; + } + 2 * n - n.count_ones() as usize +} + +/// Is this position a leaf in the MMR? +/// We know the positions of all leaves based on the postorder height of an MMR of any size (somewhat unintuitively +/// but this is how the PMMR is "append only"). +pub fn is_leaf(pos: usize) -> bool { + bintree_height(pos) == 0 +} + +/// Gets the postorder traversal index of all peaks in a MMR given its size. +/// Starts with the top peak, which is always on the left side of the range, and navigates toward lower siblings +/// toward the right of the range. +pub fn find_peaks(size: usize) -> Vec { + if size == 0 { + return vec![]; + } + let mut peak_size = ALL_ONES >> size.leading_zeros(); + let mut num_left = size; + let mut sum_prev_peaks = 0; + let mut peaks = vec![]; + while peak_size != 0 { + if num_left >= peak_size { + peaks.push(sum_prev_peaks + peak_size - 1); + sum_prev_peaks += peak_size; + num_left -= peak_size; + } + peak_size >>= 1; + } + if num_left > 0 { + return vec![]; + } + peaks +} + +/// Calculates the positions of the parent and sibling of the node at the provided position. +pub fn family(pos: usize) -> (usize, usize) { + let (peak_map, height) = peak_map_height(pos); + let peak = 1 << height; + if (peak_map & peak) != 0 { + (pos + 1, pos + 1 - 2 * peak) + } else { + (pos + 2 * peak, pos + 2 * peak - 1) + } +} + +/// For a given starting position calculate the parent and sibling positions +/// for the branch/path from that position to the peak of the tree. +/// We will use the sibling positions to generate the "path" of a Merkle proof. +pub fn family_branch(pos: usize, last_pos: usize) -> Vec<(usize, usize)> { + // loop going up the tree, from node to parent, as long as we stay inside + // the tree (as defined by last_pos). + let (peak_map, height) = peak_map_height(pos); + let mut peak = 1 << height; + let mut branch = vec![]; + let mut current = pos; + let mut sibling; + while current < last_pos { + if (peak_map & peak) != 0 { + current += 1; + sibling = current - 2 * peak; + } else { + current += 2 * peak; + sibling = current - 1; + }; + if current > last_pos { + break; + } + branch.push((current, sibling)); + peak <<= 1; + } + branch +} + +/// The height of a node in a full binary tree from its index. +#[inline(always)] +pub fn bintree_height(num: usize) -> usize { + if num == 0 { + return 0; + } + peak_map_height(num).1 +} + +/// return (peak_map, pos_height) of given 0-based node pos prior to its addition +/// Example: on input 4 returns (0b11, 0) as mmr state before adding 4 was +/// 2 +/// / \ +/// 0 1 3 +/// with 0b11 indicating presence of peaks of height 0 and 1. +/// NOTE: +/// the peak map also encodes the path taken from the root to the added node since the path turns left (resp. right) +/// if-and-only-if a peak at that height is absent (resp. present) +pub fn peak_map_height(mut pos: usize) -> (usize, usize) { + if pos == 0 { + return (0, 0); + } + let mut peak_size = ALL_ONES >> pos.leading_zeros(); + let mut bitmap = 0; + while peak_size != 0 { + bitmap <<= 1; + if pos >= peak_size { + pos -= peak_size; + bitmap |= 1; + } + peak_size >>= 1; + } + (bitmap, pos) +} + +/// sizes of peaks and height of next node in mmr of given size +/// Example: on input 5 returns ([3,1], 1) as mmr state before adding 5 was +/// 2 +/// / \ +/// 0 1 3 4 +pub fn peak_sizes_height(size: usize) -> (Vec, usize) { + if size == 0 { + return (vec![], 0); + } + let mut peak_size = ALL_ONES >> size.leading_zeros(); + let mut sizes = vec![]; + let mut size_left = size; + while peak_size != 0 { + if size_left >= peak_size { + sizes.push(peak_size); + size_left -= peak_size; + } + peak_size >>= 1; + } + (sizes, size_left) +} + +/// Is the node at this pos the "left" sibling of its parent? +pub fn is_left_sibling(pos: usize) -> bool { + let (peak_map, height) = peak_map_height(pos); + let peak = 1 << height; + (peak_map & peak) == 0 +} + +pub fn hash_together(left: &[u8], right: &[u8]) -> Hash { + D::new().chain(left).chain(right).result().to_vec() +} + +/// The number of leaves in a MMR of the provided size. +pub fn n_leaves(size: usize) -> usize { + let (sizes, height) = peak_sizes_height(size); + let nleaves = sizes.iter().map(|n| (n + 1) / 2).sum(); + if height == 0 { + nleaves + } else { + nleaves + 1 + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn leaf_indices() { + assert_eq!(leaf_index(0), 0); + assert_eq!(leaf_index(1), 1); + assert_eq!(leaf_index(2), 3); + assert_eq!(leaf_index(3), 4); + assert_eq!(leaf_index(5), 8); + assert_eq!(leaf_index(6), 10); + assert_eq!(leaf_index(7), 11); + assert_eq!(leaf_index(8), 15); + } + + #[test] + fn n_leaf_nodes() { + assert_eq!(n_leaves(0), 0); + assert_eq!(n_leaves(1), 1); + assert_eq!(n_leaves(3), 2); + assert_eq!(n_leaves(4), 3); + assert_eq!(n_leaves(8), 5); + assert_eq!(n_leaves(10), 6); + assert_eq!(n_leaves(11), 7); + assert_eq!(n_leaves(15), 8); + } + + #[test] + fn peak_vectors() { + assert_eq!(find_peaks(0), Vec::::new()); + assert_eq!(find_peaks(1), vec![0]); + assert_eq!(find_peaks(3), vec![2]); + assert_eq!(find_peaks(4), vec![2, 3]); + assert_eq!(find_peaks(15), vec![14]); + assert_eq!(find_peaks(23), vec![14, 21, 22]); + } + + #[test] + fn peak_map_heights() { + assert_eq!(peak_map_height(0), (0, 0)); + assert_eq!(peak_map_height(4), (0b11, 0)); + assert_eq!(peak_map_height(9), (0b101, 1)); + assert_eq!(peak_map_height(10), (0b110, 0)); + assert_eq!(peak_map_height(12), (0b111, 1)); + assert_eq!(peak_map_height(33), (0b10001, 1)); + assert_eq!(peak_map_height(34), (0b10010, 0)); + } + #[test] + fn is_sibling_left() { + assert_eq!(is_left_sibling(0), true); + assert_eq!(is_left_sibling(1), false); + assert_eq!(is_left_sibling(2), true); + assert_eq!(is_left_sibling(3), true); + assert_eq!(is_left_sibling(4), false); + assert_eq!(is_left_sibling(5), false); + assert_eq!(is_left_sibling(6), true); + assert_eq!(is_left_sibling(7), true); + assert_eq!(is_left_sibling(8), false); + assert_eq!(is_left_sibling(9), true); + assert_eq!(is_left_sibling(10), true); + assert_eq!(is_left_sibling(11), false); + assert_eq!(is_left_sibling(12), false); + assert_eq!(is_left_sibling(13), false); + assert_eq!(is_left_sibling(14), true); + assert_eq!(is_left_sibling(15), true); + } + + #[test] + fn families() { + assert_eq!(family(1), (2, 0)); + assert_eq!(family(0), (2, 1)); + assert_eq!(family(3), (5, 4)); + assert_eq!(family(9), (13, 12)); + assert_eq!(family(15), (17, 16)); + assert_eq!(family(6), (14, 13)); + assert_eq!(family(13), (14, 6)); + } + + #[test] + fn family_branches() { + // A 3 node tree (height 1) + assert_eq!(family_branch(0, 2), [(2, 1)]); + assert_eq!(family_branch(1, 2), [(2, 0)]); + assert_eq!(family_branch(2, 2), []); + + // leaf node in a larger tree of 7 nodes (height 2) + assert_eq!(family_branch(0, 6), [(2, 1), (6, 5)]); + + // note these only go as far up as the local peak, not necessarily the single root + assert_eq!(family_branch(0, 3), [(2, 1)]); + // pos 4 in a tree of size 4 is a local peak + assert_eq!(family_branch(3, 3), []); + // pos 4 in a tree of size 5 is also still a local peak + assert_eq!(family_branch(3, 4), []); + // pos 4 in a tree of size 6 has a parent and a sibling + assert_eq!(family_branch(3, 5), [(5, 4)]); + // a tree of size 7 is all under a single root + assert_eq!(family_branch(3, 6), [(5, 4), (6, 2)]); + + // A tree with over a million nodes in it find the "family path" back up the tree from a leaf node at 0. + // Note: the first two entries in the branch are consistent with a small 7 node tree. + // Note: each sibling is on the left branch, this is an example of the largest possible list of peaks + // before we start combining them into larger peaks. + assert_eq!(family_branch(0, 1_048_999), [ + (2, 1), + (6, 5), + (14, 13), + (30, 29), + (62, 61), + (126, 125), + (254, 253), + (510, 509), + (1022, 1021), + (2046, 2045), + (4094, 4093), + (8190, 8189), + (16382, 16381), + (32766, 32765), + (65534, 65533), + (131070, 131069), + (262142, 262141), + (524286, 524285), + (1048574, 1048573), + ]); + } +} diff --git a/base_layer/mmr/src/error.rs b/base_layer/mmr/src/error.rs new file mode 100644 index 0000000000..f99aa19370 --- /dev/null +++ b/base_layer/mmr/src/error.rs @@ -0,0 +1,40 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; + +#[derive(Debug, Error)] +pub enum MerkleMountainRangeError { + // The next position was not a leaf node as expected + CorruptDataStructure, + // Failed to add a new hash to the backend + BackendPushError, + // The Merkle tree is not internally consistent. A parent hash isn't equal to the hash of its children + InvalidMerkleTree, + // The tree has reached its maximum size + MaximumSizeReached, + // A node hash was not found for the given node index + #[error(non_std, no_from)] + HashNotFound(usize), + // A request was out of range + OutOfRange, +} diff --git a/infrastructure/merklemountainrange/src/lib.rs b/base_layer/mmr/src/lib.rs similarity index 51% rename from infrastructure/merklemountainrange/src/lib.rs rename to base_layer/mmr/src/lib.rs index cd96fdc581..cbc7c5626b 100644 --- a/infrastructure/merklemountainrange/src/lib.rs +++ b/base_layer/mmr/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2019 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -20,9 +20,13 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -//! The Merkle mountain range was invented by Peter Todd more about them can be read at: -//! https://github.com/opentimestamps/opentimestamps-server/blob/master/doc/merkle-mountain-range.md -//! https://github.com/mimblewimble/grin/blob/master/doc/mmr.md +//! # Merkle Mountain Ranges +//! +//! ## Introduction +//! +//! The Merkle mountain range was invented by Peter Todd more about them can be read at +//! [Open Timestamps](https://github.com/opentimestamps/opentimestamps-server/blob/master/doc/merkle-mountain-range.md) +//! and the [Grin project](https://github.com/mimblewimble/grin/blob/master/doc/mmr.md). //! //! A Merkle mountain range(MMR) is a binary tree where each parent is the concatenated hash of its two //! children. The leaves at the bottom of the MMR is the hashes of the data. The MMR allows easy to add and proof @@ -31,43 +35,26 @@ //! proof of the whole MMR) you have the bag the peaks of the individual trees, or mountain peaks. //! //! Lets take an example of how to construct one. Say you have the following MMR already made: -//! ''' +//! ```plaintext //! /\ //! / \ //! /\ /\ /\ //! /\/\/\/\ /\/\ /\ -//! ''' +//! ``` //! From this we can see we have 3 trees or mountains. We have constructed the largest possible tree's we can. -//! If we want to calculate the merkle route we will bag each of the mountains in the following way -//! ''' -//! /\ -//! /\ \ -//! / \ \ -//! /\ \ \ -//! / \ \ \ -//! /\ /\ /\ \ -//! /\/\/\/\/\/\/\ -//! ''' +//! If we want to calculate the merkle root we simply concatenate and then hash the three peaks. +//! //! Lets continue the example, by adding a single object. Our MMR now looks as follows -//! ''' +//! ```plaintext //! /\ //! / \ //! /\ /\ /\ //! /\/\/\/\ /\/\ /\ / -//! ''' -//! We now have 4 mountains. Lets bag and calculate the merkle root again -//! ''' -//! /\ -//! /\ \ -//! /\ \ \ -//! / \ \ \ -//! /\ \ \ \ -//! / \ \ \ \ -//! /\ /\ /\ \ \ -//! /\/\/\/\/\/\/\ \ -//! ''' +//! ``` +//! We now have 4 mountains. Calculating the root means hashing the concatenation of the (now) four peaks. +//! //! Lets continue thw example, by adding a single object. Our MMR now looks as follows -//! ''' +//! ```plaintext //! /\ //! / \ //! / \ @@ -76,10 +63,10 @@ //! / \ / \ //! /\ /\ /\ /\ //! /\/\/\/\/\/\/\/\ -//! ''' -//! Now we only have a single binary tree, we dont have to bag the mountains to calculate the merkle root. This +//! ``` +//! Now we only have a single binary tree, and the root is now the hash of the single peak's hash. This //! process continues as you add more objects to the MMR. -//! ''' +//! ```plaintext //! /\ //! / \ //! / \ @@ -94,90 +81,82 @@ //! / \ \ \ / \ \ //! /\ /\ /\ \ /\ /\ /\ //! /\/\/\/\/\/\/\ /\/\/\/\/\/\ -//! ''' -//! Due to the unique way the MMR is constructed we can easily represent the MMR as a list of the nodes, as when -//! adding nodes you only append. Lets take the following MMR and number the nodes in the order we create them. -//! ''' -//! 7 +//! ``` +//! Due to the unique way the MMR is constructed we can easily represent the MMR as a linear list of the nodes. Lets +//! take the following MMR and number the nodes in the order we create them. +//! ```plaintext +//! 6 //! / \ //! / \ -//! 3 6 +//! 2 5 //! / \ / \ -//! 1 2 4 5 -//! ''' -//! Looking above at the example of when you create the nodes, you will see the nodes will have been created in the +//! 0 1 3 4 +//! ``` +//! Looking above at the example of when you create the nodes, you will see the MMR nodes will have been created in the //! order as they are named. This means we can easily represent them as a list: //! Height: 0 | 0 | 1 | 0 | 0 | 1 | 2 -//! Node: 1 | 2 | 3 | 4 | 5 | 6 | 7 +//! Node: 0 | 1 | 2 | 3 | 4 | 5 | 6 //! //! Because of the list nature of the MMR we can easily navigate around the MMR using the following formulas: -//! Jump to sibling : 2^(H+1) -1 -//! find peak : 2^(H+1) -2 where < total elements -//! left down : 2^H -//! right down: -1 -//! Note that the formulas are for direct indexes in the array, meaning the nodes count from 0 and not 1 as in -//! the examples above. H - Height -//! I - Index //! -//! Pruning the MMR means flagging a node as pruned and only removing it if its sibling has been removed. -//! We do this as we require the sibling to prove the hash of the node. Taking the above example, let's prune leaf 1. -//! ''' -//! /\ -//! / \ -//! / \ -//! / \ -//! / \ -//! / \ -//! / \ -//! /\ \ -//! /\ \ /\ -//! / \ \ / \ -//! /\ \ \ /\ \ -//! / \ \ \ / \ \ -//! /\ /\ /\ \ /\ /\ /\ -//! /\/\/\/\/\/\/\ /\/\/\/\/\/\ -//! ''' -//! Node 1 has now only been marked as pruned but we cannot remove it as of yet because we still require it to -//! prove node 2. When we prune node 2, the MMR looks as follows -//! ''' -//! /\ -//! / \ -//! / \ -//! / \ -//! / \ -//! / \ -//! / \ -//! /\ \ -//! /\ \ /\ -//! / \ \ / \ -//! /\ \ \ /\ \ -//! / \ \ \ / \ \ -//! /\ /\ /\ \ /\ /\ /\ -//! /\/\/\/\/\/\ /\/\/\/\/\/\ -//! ''' -//! Although we have not removed node 1 and node 2 from the MMR, we cannot yet remove node 3 as we require node 3 -//! for the proof of node 6. Let's prune 4 and 5. -//! ''' -//! /\ -//! / \ -//! / \ -//! / \ -//! / \ -//! / \ -//! / \ -//! /\ \ -//! /\ \ /\ -//! / \ \ / \ -//! /\ \ \ /\ \ -//! / \ \ \ / \ \ -//! /\ /\ \ /\ /\ /\ -//! /\/\/\/\/\ /\/\/\/\/\/\ -//! ''' -//! Now we removed 3 from the MMR +//! Jump to right sibling : $$ n + 2^{H+1} - 1 $$ +//! Jump to left sibling : $$ n - 2^{H+1} - 1 $$ +//! peak of binary tree : $$ 2^{ H+1 } - 2 $$ +//! left down : $$ n - 2^H $$ +//! right down: $$ n-1 $$ +//! +//! ## Node numbering +//! +//! There can be some confusion about how nodes are numbered in an MMR. The following conventions are used in this +//! crate: +//! +//! * _All_ indices are numbered starting from zero. +//! * MMR nodes refer to all the nodes in the Merkle Mountain Range and are ordered in the canonical mmr ordering +//! described above. +//! * Leaf nodes are numbered counting from zero and increment by one each time a leaf is added. +//! +//! To illustrate, consider this MMR: +//! +//! //! ```plaintext +//! 14 +//! / \ +//! / \ +//! 6 13 21 <-- MMR indices +//! / \ / \ / \ +//! / \ / \ / \ +//! 2 5 9 12 17 21 +//! / \ / \ / \ / \ / \ / \ +//! 0 1 3 4 7 8 10 11 15 16 18 19 22 +//! ---------------------------------- +//! 0 1 2 3 4 5 6 7 8 9 10 11 12 <-- Leaf node indices +//! ---------------------------------- +//! ``` +pub type Hash = Vec; +pub type HashSlice = [u8]; + +mod backend; +mod change_tracker; +mod merkle_mountain_range; +mod merkle_proof; +mod mutable_mmr; +mod pruned_hashset; +mod serde_support; + +// Less commonly used exports +pub mod common; pub mod error; -pub mod merklemountainrange; -pub mod merklenode; -pub mod mmr { - pub use crate::merklemountainrange::*; -} +/// A function for snapshotting and pruning a Merkle Mountain Range +pub mod pruned_mmr; + +// Commonly used exports +/// A vector-based backend for [MerkleMountainRange] +pub use backend::ArrayLike; +/// A data structure that maintains a list of diffs on an MMR, enabling you to rewind to a previous state +pub use change_tracker::{MerkleChangeTracker, MerkleCheckPoint}; +/// An immutable, append-only Merkle Mountain range (MMR) data structure +pub use merkle_mountain_range::MerkleMountainRange; +/// A data structure for proving a hash inclusion in an MMR +pub use merkle_proof::{MerkleProof, MerkleProofError}; +/// An append-only Merkle Mountain range (MMR) data structure that allows deletion of existing leaf nodes. +pub use mutable_mmr::MutableMmr; diff --git a/base_layer/mmr/src/merkle_mountain_range.rs b/base_layer/mmr/src/merkle_mountain_range.rs new file mode 100644 index 0000000000..090f3138fe --- /dev/null +++ b/base_layer/mmr/src/merkle_mountain_range.rs @@ -0,0 +1,175 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, +// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. + +use crate::{ + backend::ArrayLike, + common::{bintree_height, find_peaks, hash_together, leaf_index, peak_map_height}, + error::MerkleMountainRangeError, + Hash, +}; +use digest::Digest; +use log::*; +use std::marker::PhantomData; + +const LOG_TARGET: &str = "mmr::merkle_mountain_range"; + +/// An implementation of a Merkle Mountain Range (MMR). The MMR is append-only and immutable. Only the hashes are +/// stored in this data structure. The data itself can be stored anywhere as long as you can maintain a 1:1 mapping +/// of the hash of that data to the leaf nodes in the MMR. +pub struct MerkleMountainRange +where B: ArrayLike +{ + pub(crate) hashes: B, + pub(crate) _hasher: PhantomData, +} + +impl MerkleMountainRange +where + D: Digest, + B: ArrayLike, +{ + /// Create a new Merkle mountain range using the given backend for storage + pub fn new(backend: B) -> MerkleMountainRange { + MerkleMountainRange { + hashes: backend, + _hasher: PhantomData, + } + } + + /// Return the number of nodes in the full Merkle Mountain range, excluding bagged hashes + #[inline(always)] + pub fn len(&self) -> usize { + self.hashes.len() + } + + /// Returns true if the MMR contains no hashes + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// This function returns the hash of the node index provided indexed from 0 + pub fn get_node_hash(&self, node_index: usize) -> Option<&Hash> { + self.hashes.get(node_index) + } + + /// This function returns the hash of the leaf index provided, indexed from 0 + pub fn get_leaf_hash(&self, leaf_node_index: usize) -> Option<&Hash> { + self.get_node_hash(leaf_index(leaf_node_index)) + } + + /// This function will return the single merkle root of the MMR by simply hashing the peaks together. + /// + /// Note that this differs from the bagging strategy used in other MMR implementations, and saves you a few hashes + pub fn get_merkle_root(&self) -> Hash { + if self.is_empty() { + return MerkleMountainRange::::null_hash(); + } + let hasher = D::new(); + self.hash_to_root(hasher).result().to_vec() + } + + pub(crate) fn hash_to_root(&self, hasher: D) -> D { + let peaks = find_peaks(self.hashes.len()); + peaks + .into_iter() + .map(|i| self.hashes.get_or_panic(i)) + .fold(hasher, |hasher, h| hasher.chain(h)) + } + + /// Push a new element into the MMR. Computes new related peaks at the same time if applicable. + /// Returns the new length of the merkle mountain range (the number of all nodes, not just leaf nodes). + pub fn push(&mut self, hash: &Hash) -> Result { + if self.is_empty() { + return self.push_hash(hash.clone()); + } + let mut pos = self.len(); + let (peak_map, height) = peak_map_height(pos); + if height != 0 { + return Err(MerkleMountainRangeError::CorruptDataStructure); + } + self.push_hash(hash.clone())?; + // hash with all immediately preceding peaks, as indicated by peak map + let mut peak = 1; + while (peak_map & peak) != 0 { + let left_sibling = pos + 1 - 2 * peak; + let left_hash = &self.hashes.get_or_panic(left_sibling); + peak *= 2; + pos += 1; + let last_hash = &self.hashes.get_or_panic(self.hashes.len() - 1); + let new_hash = hash_together::(left_hash, last_hash); + self.push_hash(new_hash)?; + } + Ok(pos) + } + + /// Walks the nodes in the MMR and revalidates all parent hashes + pub fn validate(&self) -> Result<(), MerkleMountainRangeError> { + // iterate on all parent nodes + for n in 0..self.len() { + let height = bintree_height(n); + if height > 0 { + let hash = self + .get_node_hash(n) + .ok_or(MerkleMountainRangeError::CorruptDataStructure)?; + let left_pos = n - (1 << height); + let right_pos = n - 1; + let left_child_hash = self + .get_node_hash(left_pos) + .ok_or(MerkleMountainRangeError::CorruptDataStructure)?; + let right_child_hash = self + .get_node_hash(right_pos) + .ok_or(MerkleMountainRangeError::CorruptDataStructure)?; + // hash the two child nodes together with parent_pos and compare + let hash_check = hash_together::(left_child_hash, right_child_hash); + if &hash_check != hash { + return Err(MerkleMountainRangeError::InvalidMerkleTree); + } + } + } + Ok(()) + } + + pub(crate) fn null_hash() -> Hash { + D::digest(b"").to_vec() + } + + fn push_hash(&mut self, hash: Hash) -> Result { + self.hashes.push(hash).map_err(|e| { + error!(target: LOG_TARGET, "{:?}", e); + MerkleMountainRangeError::BackendPushError + }) + } +} + +impl PartialEq> for MerkleMountainRange +where + D: Digest, + B: ArrayLike, + B2: ArrayLike, +{ + fn eq(&self, other: &MerkleMountainRange) -> bool { + (self.get_merkle_root() == other.get_merkle_root()) + } +} diff --git a/base_layer/mmr/src/merkle_proof.rs b/base_layer/mmr/src/merkle_proof.rs new file mode 100644 index 0000000000..d826ea9f97 --- /dev/null +++ b/base_layer/mmr/src/merkle_proof.rs @@ -0,0 +1,286 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, +// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. + +use crate::{ + backend::ArrayLike, + common::{family, family_branch, find_peaks, hash_together, is_leaf, is_left_sibling, leaf_index}, + serde_support, + Hash, + HashSlice, + MerkleMountainRange, +}; +use derive_error::Error; +use digest::Digest; +use log::error; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display, Formatter}; +use tari_utilities::hex::Hex; + +/// Merkle proof errors. +#[derive(Clone, Debug, PartialEq, Error)] +pub enum MerkleProofError { + // Merkle proof root hash does not match when attempting to verify. + RootMismatch, + // You tried to construct or verify a Merkle proof using a non-leaf node as the inclusion candidate + NonLeafNode, + // There was no hash in the merkle tree backend with the given position + #[error(non_std, no_from)] + HashNotFound(usize), + // The list of peak hashes provided in the proof has an error + IncorrectPeakMap, + // Unexpected + Unexpected, +} + +/// A Merkle proof that proves a particular element at a particular position exists in an MMR. +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, PartialOrd, Ord)] +pub struct MerkleProof { + /// The size of the MMR at the time the proof was created. + mmr_size: usize, + /// The sibling path from the leaf up to the final sibling hashing to the local root. + #[serde(with = "serde_support::hash")] + path: Vec, + /// The set of MMR peaks, not including the local peak for the candidate node + #[serde(with = "serde_support::hash")] + peaks: Vec, +} + +impl Default for MerkleProof { + fn default() -> MerkleProof { + MerkleProof { + mmr_size: 0, + path: Vec::default(), + peaks: Vec::default(), + } + } +} + +impl MerkleProof { + /// Build a Merkle Proof the given MMR at the given *leaf* position. This is usually the version you'll want to + /// call, since you'll know the leaf index more often than the MMR index. + /// + /// For the difference between leaf node and MMR node indices, see the [mod level](:tari_mmr) documentation. + /// + /// See [MerkleProof::for_node] for more details on how the proof is constructed. + pub fn for_leaf_node( + mmr: &MerkleMountainRange, + leaf_pos: usize, + ) -> Result + where + D: Digest, + B: ArrayLike, + { + let pos = leaf_index(leaf_pos); + MerkleProof::generate_proof(mmr, pos) + } + + /// Build a Merkle proof for the candidate node at the given MMR index. If you want to build a proof using the + /// leaf position, call [MerkleProof::for_leaf_node] instead. The given node position must be a leaf node, + /// otherwise a `MerkleProofError::NonLeafNode` error will be returned. + /// + /// The proof for the MMR consists of two parts: + /// a) A list of sibling node hashes starting from the candidate node and walking up the tree to the local root + /// (i.e. the root of the binary tree that the candidate node lives in. + /// b) A list of MMR peaks, excluding the local node hash. + /// The final Merkle proof is constructed by hashing all the peaks together (this is slightly different to how + /// other MMR implementations work). + pub fn for_node(mmr: &MerkleMountainRange, pos: usize) -> Result + where + D: Digest, + B: ArrayLike, + { + // check this pos is actually a leaf in the MMR + if !is_leaf(pos) { + return Err(MerkleProofError::NonLeafNode); + } + + MerkleProof::generate_proof(mmr, pos) + } + + fn generate_proof(mmr: &MerkleMountainRange, pos: usize) -> Result + where + D: Digest, + B: ArrayLike, + { + // check we actually have a hash in the MMR at this pos + mmr.get_node_hash(pos).ok_or(MerkleProofError::HashNotFound(pos))?; + let mmr_size = mmr.len(); + let family_branch = family_branch(pos, mmr_size); + + // Construct a vector of sibling hashes from the candidate node's position to the local peak + let path = family_branch + .iter() + .map(|(_, sibling)| { + mmr.get_node_hash(*sibling) + .map(|v| v.clone()) + .ok_or(MerkleProofError::HashNotFound(*sibling)) + }) + .collect::>()?; + + let peak_pos = match family_branch.last() { + Some(&(parent, _)) => parent, + None => pos, + }; + + // Get the peaks of the merkle trees, which are bagged together to form the root + // For the proof, we must leave out the local root for the candidate node + let peaks = find_peaks(mmr_size); + let mut peak_hashes = Vec::with_capacity(peaks.len() - 1); + for peak_index in peaks { + if peak_index != peak_pos { + let hash = mmr + .get_node_hash(peak_index) + .ok_or(MerkleProofError::HashNotFound(peak_index))? + .clone(); + peak_hashes.push(hash); + } + } + Ok(MerkleProof { + mmr_size, + path, + peaks: peak_hashes, + }) + } + + pub fn verify_leaf( + &self, + root: &HashSlice, + hash: &HashSlice, + leaf_pos: usize, + ) -> Result<(), MerkleProofError> + { + let pos = leaf_index(leaf_pos); + self.verify::(root, hash, pos) + } + + /// Verifies the Merkle proof against the provided root hash, element and position in the MMR. + pub fn verify(&self, root: &HashSlice, hash: &HashSlice, pos: usize) -> Result<(), MerkleProofError> { + let mut proof = self.clone(); + // calculate the peaks once as these are based on overall MMR size (and will not change) + let peaks = find_peaks(self.mmr_size); + proof.verify_consume::(root, hash, pos, &peaks) + } + + /// Calculate a merkle root from the given hash, its peak position, and the peak hashes given with the proof + /// Because of how the proofs are generated, the peak hashes given in the proof will always be an array one + /// shorter then the canonical peak list for an MMR of a given size. e.g.: For an MMR of size 10: + /// ```text + /// 6 + /// 2 5 9 + /// 0 1 3 4 7 8 + /// ``` + /// The peak list is (6,9). But if we have an inclusion proof for say, 3, then we'll calculate 6 from the sibling + /// data, therefore the proof only needs to provide 9. + /// + /// After running [verify_consume], we'll know the hash of 6 and it's position (the local root), and so we'll also + /// know where to insert the hash in the peak list. + fn check_root(&self, hash: &HashSlice, pos: usize, peaks: &[usize]) -> Result { + // The peak hash list provided in the proof does not include the local peak determined from the candidate + // node, so len(peak) must be len(self.peaks) + 1. + if peaks.len() != self.peaks.len() + 1 { + return Err(MerkleProofError::IncorrectPeakMap); + } + let hasher = D::new(); + // We're going to hash the peaks together, but insert the provided hash in the correct position. + let peak_hashes = self.peaks.iter(); + let (hasher, _) = peaks + .iter() + .fold((hasher, peak_hashes), |(hasher, mut peak_hashes), i| { + if *i == pos { + (hasher.chain(hash), peak_hashes) + } else { + let hash = peak_hashes.next().unwrap(); + (hasher.chain(hash), peak_hashes) + } + }); + Ok(hasher.result().to_vec()) + } + + /// Consumes the Merkle proof while verifying it. + /// This method works by walking up the sibling path given in the proof. Since the only info we're given in the + /// proof are the sibling hashes and the size of the MMR, there are a lot of bit-twiddling checks to determine + /// where we are in the MMR. + /// + /// This algorithm works as follows: + /// First we calculate the "local root" of the MMR by getting to the root of the full binary tree indicated by + /// `pos` and `self.mmr_size`. + /// This is done by popping a sibling hash off `self.path`, figuring out if it's on the left or right branch, + /// calculating the parent hash, and then calling `verify_consume` again using the parent hash and position. + /// Once `self.path` is empty, we have the local root and position, this data is used to hash all the peaks + /// together in `check_root` to calculate the final merkle root. + fn verify_consume( + &mut self, + root: &HashSlice, + hash: &HashSlice, + pos: usize, + peaks: &[usize], + ) -> Result<(), MerkleProofError> + { + // If path is empty, we've got the hash of a local peak, so now we need to hash all the peaks together to + // calculate the merkle root + if self.path.is_empty() { + let calculated_root = self.check_root::(hash, pos, peaks)?; + return if root == calculated_root.as_slice() { + Ok(()) + } else { + Err(MerkleProofError::RootMismatch) + }; + } + + let sibling = self.path.remove(0); // FIXME Compare perf vs using a VecDeque + let (parent_pos, sibling_pos) = family(pos); + if parent_pos > self.mmr_size { + error!( + "Found edge case. pos: {}, peaks: {:?}, mmr_size: {}, siblings: {:?}, peak_path: {:?}", + pos, peaks, self.mmr_size, &self.path, &self.peaks + ); + return Err(MerkleProofError::Unexpected); + } else { + let parent = if is_left_sibling(sibling_pos) { + hash_together::(&sibling, hash) + } else { + hash_together::(hash, &sibling) + }; + self.verify_consume::(root, &parent, parent_pos, peaks) + } + } +} + +impl Display for MerkleProof { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str(&format!("MMR Size: {}\n", self.mmr_size))?; + f.write_str("Siblings:\n")?; + self.path + .iter() + .enumerate() + .fold(Ok(()), |_, (i, h)| f.write_str(&format!("{:3}: {}\n", i, h.to_hex())))?; + f.write_str("Peaks:\n")?; + self.peaks + .iter() + .enumerate() + .fold(Ok(()), |_, (i, h)| f.write_str(&format!("{:3}: {}\n", i, h.to_hex())))?; + Ok(()) + } +} diff --git a/base_layer/mmr/src/mutable_mmr.rs b/base_layer/mmr/src/mutable_mmr.rs new file mode 100644 index 0000000000..e18aa21246 --- /dev/null +++ b/base_layer/mmr/src/mutable_mmr.rs @@ -0,0 +1,211 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + backend::ArrayLike, + common::{leaf_index, n_leaves}, + error::MerkleMountainRangeError, + Hash, + MerkleMountainRange, +}; +use croaring::Bitmap; +use digest::Digest; + +/// Unlike a pure MMR, which is append-only, in `MutableMmr`, leaf nodes can be marked as deleted. +/// +/// In `MutableMmr` a roaring bitmap tracks which data have been marked as deleted, and the merklish root is modified +/// to include the hash of the roaring bitmap. +/// +/// The `MutableMmr` API maps nearly 1:1 to that of MerkleMountainRange so that you should be able to use it as a +/// drop-in replacement for the latter in most cases. +pub struct MutableMmr +where + D: Digest, + B: ArrayLike, +{ + pub(crate) mmr: MerkleMountainRange, + pub(crate) deleted: Bitmap, + // The number of leaf nodes in the MutableMmr. Bitmap is limited to 4 billion elements, which is plenty. + // [croaring::Treemap] is a 64bit alternative, but this would break things on 32bit systems. A good TODO would be + // to select the bitmap backend using a feature flag + pub(crate) size: u32, +} + +impl MutableMmr +where + D: Digest, + B: ArrayLike, +{ + /// Create a new mutable MMR using the backend provided + pub fn new(mmr_backend: B) -> MutableMmr { + let mmr = MerkleMountainRange::new(mmr_backend); + MutableMmr { + mmr, + deleted: Bitmap::create(), + size: 0, + } + } + + /// Return the number of leaf nodes in the `MutableMmr` that have not been marked as deleted. + /// + /// NB: This is semantically different to `MerkleMountainRange::len()`. The latter returns the total number of + /// nodes in the MMR, while this function returns the number of leaf nodes minus the number of nodes marked for + /// deletion. + #[inline(always)] + pub fn len(&self) -> u32 { + self.size - self.deleted.cardinality() as u32 + } + + /// Returns true if the the MMR contains no nodes, OR all nodes have been marked for deletion + pub fn is_empty(&self) -> bool { + self.mmr.is_empty() || self.deleted.cardinality() == self.size as u64 + } + + /// This function returns the hash of the leaf index provided, indexed from 0. If the hash does not exist, or if it + /// has been marked for deletion, `None` is returned. + pub fn get_leaf_hash(&self, leaf_node_index: u32) -> Option<&Hash> { + if self.deleted.contains(leaf_node_index) { + return None; + } + self.mmr.get_node_hash(leaf_index(leaf_node_index as usize)) + } + + /// Returns the hash of the leaf index provided, as well as its deletion status. The node has been marked for + /// deletion if the boolean value is true. + pub fn get_leaf_status(&self, leaf_node_index: u32) -> (Option<&Hash>, bool) { + let hash = self.mmr.get_node_hash(leaf_index(leaf_node_index as usize)); + let deleted = self.deleted.contains(leaf_node_index); + (hash, deleted) + } + + /// Returns a merkle(ish) root for this merkle set. + /// + /// The root is calculated by concatenating the MMR merkle root with the compressed serialisation of the bitmap + /// and then hashing the result. + pub fn get_merkle_root(&self) -> Hash { + // Note that two MutableMmrs could both return true for `is_empty()`, but have different merkle roots by + // virtue of the fact that the underlying MMRs could be different, but all elements are marked as deleted in + // both sets. + let mmr_root = self.mmr.get_merkle_root(); + let mut hasher = D::new(); + hasher.input(&mmr_root); + self.hash_deleted(hasher).result().to_vec() + } + + /// Push a new element into the MMR. Computes new related peaks at the same time if applicable. + /// Returns the new number of leaf nodes (regardless of deleted state) in the mutable MMR + pub fn push(&mut self, hash: &Hash) -> Result { + if self.size >= std::u32::MAX { + return Err(MerkleMountainRangeError::MaximumSizeReached); + } + let result = self.mmr.push(hash); + if result.is_ok() { + self.size += 1; + } + Ok(self.size as usize) + } + + /// Mark a node for deletion and optionally compress the deletion bitmap. Don't call this function unless you're + /// in a tight loop and want to eke out some extra performance by delaying the bitmap compression until after the + /// batch deletion. + /// + /// Note that this function doesn't actually + /// delete anything (the underlying MMR structure is immutable), but marks the leaf node as deleted. Once a leaf + /// node has been marked for deletion: + /// * `get_leaf_hash(n)` will return None, + /// * `len()` will not count this node anymore + /// + /// # Parameters + /// * `leaf_node_index`: The index of the leaf node to mark for deletion, zero-based. + /// * `compress`: Indicates whether the roaring bitmap should be compressed after marking the node for deletion. + /// **NB**: You should set this to true unless you are in a loop and deleting multiple nodes, and you **must** set + /// this to true if you are about to call `get_merkle_root()`. If you don't, the merkle root will be incorrect. + /// + /// # Return + /// The function returns true if a node was actually marked for deletion. If the index is out of bounds, or was + /// already deleted, the function returns false. + pub fn delete_and_compress(&mut self, leaf_node_index: u32, compress: bool) -> bool { + if (leaf_node_index >= self.size) || self.deleted.contains(leaf_node_index) { + return false; + } + self.deleted.add(leaf_node_index); + // The serialization is different in compressed vs. uncompressed form, but the merkle root must be 100% + // deterministic based on input, so just be consistent an use the compressed form all the time. + if compress { + self.compress(); + } + true + } + + /// Mark a node for completion, and compress the roaring bitmap. See [delete_and_compress] for details. + pub fn delete(&mut self, leaf_node_index: u32) -> bool { + self.delete_and_compress(leaf_node_index, true) + } + + /// Compress the roaring bitmap mapping deleted nodes. You never have to call this method unless you have been + /// calling [delete_and_compress] with `compress` set to `false` ahead of a call to [get_merkle_root]. + pub fn compress(&mut self) -> bool { + self.deleted.run_optimize() + } + + /// Walks the nodes in the MMR and validates all parent hashes + /// + /// This just calls through to the underlying MMR's validate method. There's nothing we can do to check whether + /// the roaring bitmap represents all the leaf nodes that we want to delete. Note: A struct that uses + /// `MutableMmr` and links it to actual data should be able to do this though. + pub fn validate(&self) -> Result<(), MerkleMountainRangeError> { + self.mmr.validate() + } + + /// Hash the roaring bitmap of nodes that are marked for deletion + fn hash_deleted(&self, mut hasher: D) -> D { + let bitmap_ser = self.deleted.serialize(); + hasher.input(&bitmap_ser); + hasher + } +} + +impl PartialEq> for MutableMmr +where + D: Digest, + B: ArrayLike, + B2: ArrayLike, +{ + fn eq(&self, other: &MutableMmr) -> bool { + (self.get_merkle_root() == other.get_merkle_root()) + } +} + +impl From> for MutableMmr +where + D: Digest, + B: ArrayLike, +{ + fn from(mmr: MerkleMountainRange) -> Self { + let size = n_leaves(mmr.len()) as u32; + MutableMmr { + mmr, + deleted: Bitmap::create(), + size, + } + } +} diff --git a/base_layer/mmr/src/pruned_hashset.rs b/base_layer/mmr/src/pruned_hashset.rs new file mode 100644 index 0000000000..3f0d80e14f --- /dev/null +++ b/base_layer/mmr/src/pruned_hashset.rs @@ -0,0 +1,102 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{common::find_peaks, error::MerkleMountainRangeError, ArrayLike, Hash, MerkleMountainRange}; +use digest::Digest; +use std::convert::TryFrom; + +/// This is a specialised struct that represents a pruned hash set for Merkle Mountain Ranges. +/// +/// The basic idea is that when adding a new hash, only the peaks to the left of the new node hierarchy are ever needed. +/// This means that if we don't care about the data earlier than a given leaf node index, n_0, (i.e. we still have the +/// hashes, but can't recalculate them from source), we _only need to store the local peaks for the MMR at that time_ +/// and we can forget about the rest. There will never be a request for a hash other than those at the peaks for the +/// MMR with n_0 leaf nodes. +/// +/// The awesome thing is that this struct can be dropped into [MerkleMountainRange] as a backend and it. just. works. +pub struct PrunedHashSet { + /// The size of the base MMR. Only peaks are available for indices less than this value + base_offset: usize, + /// The array of peak indices for an MMR of size `base_offset` + peak_indices: Vec, + /// The array of hashes at the MMR peaks + peak_hashes: Vec, + /// New hashes added subsequent to `base_offset`. + hashes: Vec, +} + +impl TryFrom<&MerkleMountainRange> for PrunedHashSet +where + D: Digest, + B: ArrayLike, +{ + type Error = MerkleMountainRangeError; + + fn try_from(base_mmr: &MerkleMountainRange) -> Result { + let base_offset = base_mmr.len(); + let peak_indices = find_peaks(base_offset); + let peak_hashes = peak_indices + .iter() + .map(|i| match base_mmr.get_node_hash(*i) { + Some(h) => Ok(h.clone()), + None => Err(MerkleMountainRangeError::HashNotFound(*i)), + }) + .collect::>()?; + Ok(PrunedHashSet { + base_offset, + peak_indices, + peak_hashes, + hashes: Vec::new(), + }) + } +} + +impl ArrayLike for PrunedHashSet { + type Error = MerkleMountainRangeError; + type Value = Hash; + + #[inline(always)] + fn len(&self) -> usize { + self.base_offset + self.hashes.len() + } + + fn push(&mut self, item: Self::Value) -> Result { + self.hashes.push(item); + Ok(self.len() - 1) + } + + fn get(&self, index: usize) -> Option<&Self::Value> { + // If the index is from before we started adding hashes, we can return the hash *if and only if* it is a peak + if index < self.base_offset { + return match self.peak_indices.binary_search(&index) { + Ok(nth_peak) => Some(&self.peak_hashes[nth_peak]), + Err(_) => None, + }; + } + self.hashes.get(index - self.base_offset) + } + + fn get_or_panic(&self, index: usize) -> &Self::Value { + self.get(index) + .expect("PrunedHashSet only tracks peaks before the offset") + } +} diff --git a/base_layer/mmr/src/pruned_mmr.rs b/base_layer/mmr/src/pruned_mmr.rs new file mode 100644 index 0000000000..b9e8398fc3 --- /dev/null +++ b/base_layer/mmr/src/pruned_mmr.rs @@ -0,0 +1,67 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + error::MerkleMountainRangeError, + pruned_hashset::PrunedHashSet, + ArrayLike, + Hash, + MerkleMountainRange, + MutableMmr, +}; +use digest::Digest; +use serde::export::PhantomData; +use std::convert::TryFrom; + +pub type PrunedMmr = MerkleMountainRange; +pub type PrunedMutableMmr = MutableMmr; + +/// Create a pruned Merkle Mountain Range from the provided MMR. Pruning entails throwing all the hashes of the +/// pruned MMR away, except for the current peaks. A new MMR instance is returned that allows you to continue +/// adding onto the MMR as before. Most functions of the pruned MMR will work as expected, but obviously, any +/// leaf hashes prior to the base point won't be available. `get_leaf_hash` will return `None` for those nodes, and +/// `validate` will throw an error. +pub fn prune_mmr(mmr: &MerkleMountainRange) -> Result, MerkleMountainRangeError> +where + D: Digest, + B: ArrayLike, +{ + let backend = PrunedHashSet::try_from(mmr)?; + Ok(MerkleMountainRange { + hashes: backend, + _hasher: PhantomData, + }) +} + +/// A convenience function in the same vein as [prune_mmr], but applied to `MutableMmr` instances. +pub fn prune_mutable_mmr(mmr: &MutableMmr) -> Result, MerkleMountainRangeError> +where + D: Digest, + B: ArrayLike, +{ + let backend = PrunedHashSet::try_from(&mmr.mmr)?; + Ok(MutableMmr { + mmr: MerkleMountainRange::new(backend), + deleted: mmr.deleted.clone(), + size: mmr.size, + }) +} diff --git a/base_layer/mmr/src/serde_support.rs b/base_layer/mmr/src/serde_support.rs new file mode 100644 index 0000000000..f6e8273c1b --- /dev/null +++ b/base_layer/mmr/src/serde_support.rs @@ -0,0 +1,81 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +// TODO - move all the to_hex serde stuff into a common module +pub mod hash { + use crate::Hash; + use serde::{ + de::{self, SeqAccess, Visitor}, + ser::SerializeSeq, + Deserializer, + Serializer, + }; + use std::fmt; + use tari_utilities::hex::{self, Hex}; + + pub fn serialize(hashes: &[Hash], ser: S) -> Result + where S: Serializer { + let is_human_readable = ser.is_human_readable(); + let mut seq = ser.serialize_seq(Some(hashes.len()))?; + for hash in hashes { + if is_human_readable { + seq.serialize_element(&hash.to_hex())?; + } else { + seq.serialize_element(hash.as_slice())?; + } + } + seq.end() + } + + pub fn deserialize<'de, D>(de: D) -> Result, D::Error> + where D: Deserializer<'de> { + struct HashVecVisitor(bool); + + impl<'de> Visitor<'de> for HashVecVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a vector of hashes") + } + + fn visit_seq(self, mut seq: A) -> Result + where A: SeqAccess<'de> { + let is_human_readable = self.0; + let mut result = Vec::::with_capacity(seq.size_hint().unwrap_or(10)); + if is_human_readable { + while let Some(v) = seq.next_element::()? { + let val = hex::from_hex(&v).map_err(de::Error::custom)?; + result.push(val); + } + } else { + while let Some(v) = seq.next_element()? { + result.push(v); + } + } + Ok(result) + } + } + let is_human_readable = de.is_human_readable(); + de.deserialize_seq(HashVecVisitor(is_human_readable)) + } +} diff --git a/base_layer/mmr/tests/change_tracker.rs b/base_layer/mmr/tests/change_tracker.rs new file mode 100644 index 0000000000..95a89fb355 --- /dev/null +++ b/base_layer/mmr/tests/change_tracker.rs @@ -0,0 +1,151 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod support; + +use croaring::Bitmap; +use support::{create_mmr, int_to_hash, Hasher}; +use tari_mmr::{MerkleChangeTracker, MutableMmr}; +use tari_utilities::hex::Hex; + +#[test] +fn change_tracker() { + let mmr = MutableMmr::::new(Vec::default()); + let mmr = MerkleChangeTracker::new(mmr, Vec::new()).unwrap(); + assert_eq!(mmr.checkpoint_count(), 0); + assert!(mmr.is_empty()); +} + +#[test] +/// Test the same MMR structure as the test in mutable_mmr, but add in rewinding and restoring of state +fn checkpoints() { + //----------- Construct and populate the initial MMR -------------------------- + let base = MutableMmr::::new(Vec::default()); + let mut mmr = MerkleChangeTracker::new(base, Vec::new()).unwrap(); + for i in 0..5 { + assert!(mmr.push(&int_to_hash(i)).is_ok()); + } + assert_eq!(mmr.len(), 5); + assert_eq!(mmr.checkpoint_count(), 0); + //----------- Commit the history thus far ----------------------------------- + assert!(mmr.commit().is_ok()); + assert_eq!(mmr.checkpoint_count(), 1); + let root_at_1 = mmr.get_merkle_root(); + assert_eq!( + &root_at_1.to_hex(), + "7b7ddec2af4f3d0b9b165750cf2ff15813e965d29ecd5318e0c8fea901ceaef4" + ); + //----------- Add a node and delete a few nodes ----------------------------- + assert!(mmr.push(&int_to_hash(5)).is_ok()); + assert!(mmr.delete_and_compress(0, false)); + assert!(mmr.delete_and_compress(2, false)); + assert!(mmr.delete_and_compress(4, true)); + //----------- Commit the history again, and check the expected sizes -------- + let root_at_2 = mmr.get_merkle_root(); + assert_eq!( + &root_at_2.to_hex(), + "69e69ba0c6222f2d9caa68282de0ba7f1259a0fa2b8d84af68f907ef4ec05054" + ); + assert!(mmr.commit().is_ok()); + assert_eq!(mmr.len(), 3); + assert_eq!(mmr.checkpoint_count(), 2); + //----------- Generate another checkpoint, the MMR is now empty -------- + assert!(mmr.delete_and_compress(1, false)); + assert!(mmr.delete_and_compress(5, false)); + assert!(mmr.delete(3)); + assert!(mmr.commit().is_ok()); + assert!(mmr.is_empty()); + assert_eq!(mmr.checkpoint_count(), 3); + let root = mmr.get_merkle_root(); + assert_eq!( + &root.to_hex(), + "2a540797d919e63cff8051e54ae13197315000bcfde53efd3f711bb3d24995bc" + ); + //----------- Create an empty checkpoint ------------------------------- + assert!(mmr.commit().is_ok()); + assert_eq!(mmr.checkpoint_count(), 4); + assert_eq!( + &mmr.get_merkle_root().to_hex(), + "2a540797d919e63cff8051e54ae13197315000bcfde53efd3f711bb3d24995bc" + ); + //----------- Rewind the MMR two commits---------------------------------- + assert!(mmr.rewind(2).is_ok()); + assert_eq!(mmr.get_merkle_root().to_hex(), root_at_2.to_hex()); + //----------- Perform an empty commit ------------------------------------ + assert!(mmr.commit().is_ok()); + assert_eq!(mmr.len(), 3); + assert_eq!(mmr.checkpoint_count(), 3); +} + +#[test] +fn reset_and_replay() { + // You don't have to use a Pruned MMR... any MMR implementation is fine + let base = MutableMmr::from(create_mmr(5)); + let mut mmr = MerkleChangeTracker::new(base, Vec::new()).unwrap(); + let root = mmr.get_merkle_root(); + // Add some new nodes etc + assert!(mmr.push(&int_to_hash(10)).is_ok()); + assert!(mmr.push(&int_to_hash(11)).is_ok()); + assert!(mmr.push(&int_to_hash(12)).is_ok()); + assert!(mmr.delete(7)); + // Reset - should be back to base state + assert!(mmr.reset().is_ok()); + assert_eq!(mmr.get_merkle_root(), root); + + // Change some more state + assert!(mmr.delete(1)); + assert!(mmr.delete(3)); + assert!(mmr.commit().is_ok()); //--- Checkpoint 0 --- + let root = mmr.get_merkle_root(); + + // Change a bunch more things + let hash_5 = int_to_hash(5); + assert!(mmr.push(&hash_5).is_ok()); + assert!(mmr.commit().is_ok()); //--- Checkpoint 1 --- + assert!(mmr.push(&int_to_hash(6)).is_ok()); + assert!(mmr.commit().is_ok()); //--- Checkpoint 2 --- + + assert!(mmr.push(&int_to_hash(7)).is_ok()); + assert!(mmr.commit().is_ok()); //--- Checkpoint 3 --- + assert!(mmr.delete(0)); + assert!(mmr.delete(6)); + assert!(mmr.commit().is_ok()); //--- Checkpoint 4 --- + + // Get checkpoint 1 + let cp = mmr.get_checkpoint(1).unwrap(); + assert_eq!(cp.nodes_added(), &[hash_5]); + assert_eq!(*cp.nodes_deleted(), Bitmap::create()); + + // Get checkpoint 0 + let cp = mmr.get_checkpoint(0).unwrap(); + assert!(cp.nodes_added().is_empty()); + let mut deleted = Bitmap::create(); + deleted.add(1); + deleted.add(3); + assert_eq!(*cp.nodes_deleted(), deleted); + + // Roll back to last time we save the root + assert!(mmr.replay(1).is_ok()); + assert_eq!(mmr.len(), 3); + + assert_eq!(mmr.get_merkle_root(), root); +} diff --git a/base_layer/mmr/tests/merkle_mountain_range.rs b/base_layer/mmr/tests/merkle_mountain_range.rs new file mode 100644 index 0000000000..10632d40d3 --- /dev/null +++ b/base_layer/mmr/tests/merkle_mountain_range.rs @@ -0,0 +1,116 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod support; + +use digest::Digest; +use support::{combine_hashes, create_mmr, int_to_hash, Hasher}; +use tari_mmr::MerkleMountainRange; + +/// MMRs with no elements should provide sane defaults. The merkle root must be the hash of an empty string, b"". +#[test] +fn zero_length_mmr() { + let mmr = MerkleMountainRange::::new(Vec::default()); + assert_eq!(mmr.len(), 0); + assert!(mmr.is_empty()); + let empty_hash = Hasher::digest(b"").to_vec(); + assert_eq!(&mmr.get_merkle_root(), &empty_hash); +} + +/// Successively build up an MMR and check that the roots, heights and indices are all correct. +#[test] +fn build_mmr() { + let mut mmr = MerkleMountainRange::::new(Vec::default()); + // Add a single item + let h0 = int_to_hash(0); + + assert!(mmr.push(&h0).is_ok()); + // The root of a single hash is the hash of that hash + assert_eq!(mmr.len(), 1); + assert_eq!(mmr.get_merkle_root(), combine_hashes(&[&h0])); + // Two leaf item items: + // 2 + // 0 1 + let h1 = int_to_hash(1); + assert!(mmr.push(&h1).is_ok()); + let h_2 = combine_hashes(&[&h0, &h1]); + assert_eq!(mmr.get_merkle_root(), combine_hashes(&[&h_2])); + assert_eq!(mmr.len(), 3); + // Three leaf item items: + // 2 + // 0 1 3 + let h3 = int_to_hash(3); + assert!(mmr.push(&h3).is_ok()); + // The root is a bagged root + let root = combine_hashes(&[&h_2, &h3]); + assert_eq!(mmr.get_merkle_root(), root); + assert_eq!(mmr.len(), 4); + // Four leaf items: + // 6 + // 2 5 + // 0 1 3 4 + let h4 = int_to_hash(4); + assert!(mmr.push(&h4).is_ok()); + let h_5 = combine_hashes(&[&h3, &h4]); + let h_6 = combine_hashes(&[&h_2, &h_5]); + assert_eq!(mmr.get_merkle_root(), combine_hashes(&[&h_6])); + assert_eq!(mmr.len(), 7); + // Five leaf items: + // 6 + // 2 5 + // 0 1 3 4 7 + let h7 = int_to_hash(7); + assert!(mmr.push(&h7).is_ok()); + let root = combine_hashes(&[&h_6, &h7]); + assert_eq!(mmr.get_merkle_root(), root); + assert_eq!(mmr.len(), 8); + // Six leaf item items: + // 6 + // 2 5 9 + // 0 1 3 4 7 8 + let h8 = int_to_hash(8); + let h_9 = combine_hashes(&[&h7, &h8]); + assert!(mmr.push(&h8).is_ok()); + let root = combine_hashes(&[&h_6, &h_9]); + assert_eq!(mmr.get_merkle_root(), root); + assert_eq!(mmr.len(), 10); +} + +#[test] +fn equality_check() { + let mut ma = MerkleMountainRange::::new(Vec::default()); + let mut mb = MerkleMountainRange::::new(Vec::default()); + assert!(ma == mb); + assert!(ma.push(&int_to_hash(1)).is_ok()); + assert!(ma != mb); + assert!(mb.push(&int_to_hash(1)).is_ok()); + assert!(ma == mb); + assert!(ma.push(&int_to_hash(2)).is_ok()); + assert!(mb.push(&int_to_hash(3)).is_ok()); + assert!(ma != mb); +} + +#[test] +fn validate() { + let mmr = create_mmr(65); + assert!(mmr.validate().is_ok()); +} diff --git a/base_layer/mmr/tests/merkle_proof.rs b/base_layer/mmr/tests/merkle_proof.rs new file mode 100644 index 0000000000..06d3dd7f52 --- /dev/null +++ b/base_layer/mmr/tests/merkle_proof.rs @@ -0,0 +1,123 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +mod support; + +use support::{create_mmr, int_to_hash, Hasher}; +use tari_mmr::{ + common::{is_leaf, leaf_index}, + MerkleProof, + MerkleProofError, +}; +use tari_utilities::hex::{self, Hex}; + +#[test] +fn zero_size_mmr() { + let mmr = create_mmr(0); + match MerkleProof::for_node(&mmr, 0) { + Err(MerkleProofError::HashNotFound(i)) => assert_eq!(i, 0), + _ => panic!("Incorrect zero-length merkle proof"), + } +} + +/// Thorough check of MerkleProof process for each position in various MMR sizes +#[test] +fn merkle_proof_small_mmrs() { + for size in 1..32 { + let mmr = create_mmr(size); + let root = mmr.get_merkle_root(); + let mut hash_value = 0usize; + for pos in 0..mmr.len() { + if is_leaf(pos) { + let hash = int_to_hash(hash_value); + hash_value += 1; + let proof = MerkleProof::for_node(&mmr, pos).unwrap(); + assert!(proof.verify::(&root, &hash, pos).is_ok()); + } else { + assert_eq!(MerkleProof::for_node(&mmr, pos), Err(MerkleProofError::NonLeafNode)); + } + } + } +} + +#[test] +fn med_mmr() { + let size = 500; + let mmr = create_mmr(size); + let root = mmr.get_merkle_root(); + let i = 499; + let pos = leaf_index(i); + let hash = int_to_hash(i); + let proof = MerkleProof::for_node(&mmr, pos).unwrap(); + assert!(proof.verify::(&root, &hash, pos).is_ok()); +} + +#[test] +fn a_big_proof() { + let mmr = create_mmr(100_000); + let leaf_pos = 28_543; + let mmr_index = leaf_index(leaf_pos); + let root = mmr.get_merkle_root(); + let hash = int_to_hash(leaf_pos); + let proof = MerkleProof::for_node(&mmr, mmr_index).unwrap(); + assert!(proof.verify::(&root, &hash, mmr_index).is_ok()) +} + +#[test] +fn for_leaf_node() { + let mmr = create_mmr(100); + let root = mmr.get_merkle_root(); + let leaf_pos = 28; + let hash = int_to_hash(leaf_pos); + let proof = MerkleProof::for_leaf_node(&mmr, leaf_pos).unwrap(); + assert!(proof.verify_leaf::(&root, &hash, leaf_pos).is_ok()) +} + +const JSON_PROOF: &str = r#"{"mmr_size":8,"path":["e88b43fded6323ef02ffeffbd8c40846ee09bf316271bd22369659c959dd733a","8bdd601372fd4d8242591e4b42815bc35826b0209ce5b78eb06609110b002b9d"],"peaks":["e96760d274653a39b429a87ebaae9d3aa4fdf58b9096cf0bebc7c4e5a4c2ed8d"]}"#; +const BINCODE_PROOF: &str = "080000000000000002000000000000002000000000000000e88b43fded6323ef02ffeffbd8c40846ee09bf316271bd22369659c959dd733a20000000000000008bdd601372fd4d8242591e4b42815bc35826b0209ce5b78eb06609110b002b9d01000000000000002000000000000000e96760d274653a39b429a87ebaae9d3aa4fdf58b9096cf0bebc7c4e5a4c2ed8d"; + +#[test] +fn serialisation() { + let mmr = create_mmr(5); + let proof = MerkleProof::for_leaf_node(&mmr, 3).unwrap(); + let json_proof = serde_json::to_string(&proof).unwrap(); + assert_eq!(&json_proof, JSON_PROOF); + + let bincode_proof = bincode::serialize(&proof).unwrap(); + assert_eq!(bincode_proof.to_hex(), BINCODE_PROOF); +} + +#[test] +fn deserialization() { + let root = hex::from_hex("167a34de2d13b7911093344cd2697b4c6311c5308a9f45476d094e3b3ef6e669").unwrap(); + // Verify JSON-derived proof + let proof: MerkleProof = serde_json::from_str(JSON_PROOF).unwrap(); + println!("{}", proof); + assert!(proof.verify_leaf::(&root, &int_to_hash(3), 3).is_ok()); + + // Verify bincode-derived proof + let bin_proof = hex::from_hex(BINCODE_PROOF).unwrap(); + let proof: MerkleProof = bincode::deserialize(&bin_proof).unwrap(); + println!("{}", proof); + assert!(proof.verify_leaf::(&root, &int_to_hash(3), 3).is_ok()); +} diff --git a/base_layer/mmr/tests/mutable_mmr.rs b/base_layer/mmr/tests/mutable_mmr.rs new file mode 100644 index 0000000000..4a7719018c --- /dev/null +++ b/base_layer/mmr/tests/mutable_mmr.rs @@ -0,0 +1,147 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod support; + +use croaring::Bitmap; +use digest::Digest; +use support::{create_mmr, int_to_hash, Hasher}; +use tari_mmr::{Hash, HashSlice, MutableMmr}; +use tari_utilities::hex::Hex; + +fn hash_with_bitmap(hash: &HashSlice, bitmap: &mut Bitmap) -> Hash { + bitmap.run_optimize(); + let hasher = Hasher::new(); + hasher.chain(hash).chain(&bitmap.serialize()).result().to_vec() +} + +/// MMRs with no elements should provide sane defaults. The merkle root must be the hash of an empty string, b"". +#[test] +fn zero_length_mmr() { + let mmr = MutableMmr::::new(Vec::default()); + assert_eq!(mmr.len(), 0); + assert!(mmr.is_empty()); + let empty_hash = Hasher::digest(b"").to_vec(); + assert_eq!( + mmr.get_merkle_root(), + hash_with_bitmap(&empty_hash, &mut Bitmap::create()) + ); +} + +#[test] +// Note the hardcoded hashes are only valid when using Blake256 as the Hasher +fn delete() { + let mut mmr = MutableMmr::::new(Vec::default()); + assert!(mmr.is_empty()); + for i in 0..5 { + assert!(mmr.push(&int_to_hash(i)).is_ok()); + } + assert_eq!(mmr.len(), 5); + let root = mmr.get_merkle_root(); + assert_eq!( + &root.to_hex(), + "7b7ddec2af4f3d0b9b165750cf2ff15813e965d29ecd5318e0c8fea901ceaef4" + ); + // Can't delete past bounds + assert_eq!(mmr.delete_and_compress(5, true), false); + assert_eq!(mmr.len(), 5); + assert!(!mmr.is_empty()); + assert_eq!(mmr.get_merkle_root(), root); + // Delete some nodes + assert!(mmr.push(&int_to_hash(5)).is_ok()); + assert!(mmr.delete_and_compress(0, false)); + assert!(mmr.delete_and_compress(2, false)); + assert!(mmr.delete_and_compress(4, true)); + let root = mmr.get_merkle_root(); + assert_eq!( + &root.to_hex(), + "69e69ba0c6222f2d9caa68282de0ba7f1259a0fa2b8d84af68f907ef4ec05054" + ); + assert_eq!(mmr.len(), 3); + assert!(!mmr.is_empty()); + // Can't delete that which has already been deleted + assert!(!mmr.delete_and_compress(0, false)); + assert!(!mmr.delete_and_compress(2, false)); + assert!(!mmr.delete_and_compress(0, true)); + // .. or beyond bounds of MMR + assert!(!mmr.delete_and_compress(99, true)); + assert_eq!(mmr.len(), 3); + assert!(!mmr.is_empty()); + // Merkle root should not have changed: + assert_eq!(mmr.get_merkle_root(), root); + assert!(mmr.delete_and_compress(1, false)); + assert!(mmr.delete_and_compress(5, false)); + assert!(mmr.delete(3)); + assert_eq!(mmr.len(), 0); + assert!(mmr.is_empty()); + let root = mmr.get_merkle_root(); + assert_eq!( + &root.to_hex(), + "2a540797d919e63cff8051e54ae13197315000bcfde53efd3f711bb3d24995bc" + ); +} + +/// Successively build up an MMR and check that the roots, heights and indices are all correct. +#[test] +fn build_mmr() { + // Check the mutable MMR against a standard MMR and a roaring bitmap. Create one with 5 leaf nodes *8 MMR nodes) + let mmr_check = create_mmr(5); + assert_eq!(mmr_check.len(), 8); + let mut bitmap = Bitmap::create(); + // Create a small mutable MMR + let mut mmr = MutableMmr::::new(Vec::default()); + for i in 0..5 { + assert!(mmr.push(&int_to_hash(i)).is_ok()); + } + // MutableMmr::len gives the size in terms of leaf nodes: + assert_eq!(mmr.len(), 5); + let mmr_root = mmr_check.get_merkle_root(); + let root_check = hash_with_bitmap(&mmr_root, &mut bitmap); + assert_eq!(mmr.get_merkle_root(), root_check); + // Delete a node + assert!(mmr.delete_and_compress(3, true)); + bitmap.add(3); + let root_check = hash_with_bitmap(&mmr_root, &mut bitmap); + assert_eq!(mmr.get_merkle_root(), root_check); +} + +#[test] +fn equality_check() { + let mut ma = MutableMmr::::new(Vec::default()); + let mut mb = MutableMmr::::new(Vec::default()); + assert!(ma == mb); + assert!(ma.push(&int_to_hash(1)).is_ok()); + assert!(ma != mb); + assert!(mb.push(&int_to_hash(1)).is_ok()); + assert!(ma == mb); + assert!(ma.push(&int_to_hash(2)).is_ok()); + assert!(ma != mb); + assert!(ma.delete(1)); + // Even though the two trees have the same apparent elements, they're still not equal, because we don't actually + // delete anything + assert!(ma != mb); + // Add the same hash to mb and then delete it + assert!(mb.push(&int_to_hash(2)).is_ok()); + assert!(mb.delete(1)); + // Now they're equal! + assert!(ma == mb); +} diff --git a/base_layer/mmr/tests/pruned_mmr.rs b/base_layer/mmr/tests/pruned_mmr.rs new file mode 100644 index 0000000000..2e6e4166b3 --- /dev/null +++ b/base_layer/mmr/tests/pruned_mmr.rs @@ -0,0 +1,56 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod support; + +use support::{create_mmr, int_to_hash}; +use tari_mmr::pruned_mmr::prune_mmr; + +#[test] +fn pruned_mmr_empty() { + let mmr = create_mmr(0); + let root = mmr.get_merkle_root(); + let pruned = prune_mmr(&mmr).expect("Could not create empty pruned MMR"); + assert!(pruned.is_empty()); + assert_eq!(pruned.get_merkle_root(), root); +} + +#[test] +fn pruned_mmrs() { + for size in &[6, 14, 63, 64, 65, 127] { + let mmr = create_mmr(*size); + let mmr2 = create_mmr(size + 2); + + let root = mmr.get_merkle_root(); + let mut pruned = prune_mmr(&mmr).expect("Could not create empty pruned MMR"); + assert_eq!(pruned.len(), mmr.len()); + assert_eq!(pruned.get_merkle_root(), root); + // The pruned MMR works just like the normal one + let new_hash = int_to_hash(*size); + assert!(pruned.push(&new_hash).is_ok()); + assert!(pruned.push(&int_to_hash(*size + 1)).is_ok()); + assert_eq!(pruned.get_merkle_root(), mmr2.get_merkle_root()); + // But you can only get recent hashes + assert!(pruned.get_leaf_hash(*size / 2).is_none()); + assert_eq!(pruned.get_leaf_hash(*size).unwrap(), &new_hash) + } +} diff --git a/base_layer/mmr/tests/support/mod.rs b/base_layer/mmr/tests/support/mod.rs new file mode 100644 index 0000000000..6a633731ac --- /dev/null +++ b/base_layer/mmr/tests/support/mod.rs @@ -0,0 +1,50 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +use digest::Digest; +use tari_crypto::common::Blake256; +use tari_mmr::{Hash, HashSlice, MerkleMountainRange}; + +pub type Hasher = Blake256; + +pub fn create_mmr(size: usize) -> MerkleMountainRange> { + let mut mmr = MerkleMountainRange::::new(Vec::default()); + for i in 0..size { + let hash = int_to_hash(i); + assert!(mmr.push(&hash).is_ok()); + } + mmr +} + +pub fn int_to_hash(n: usize) -> Vec { + Hasher::digest(&n.to_le_bytes()).to_vec() +} + +pub fn combine_hashes(hashes: &[&HashSlice]) -> Hash { + let hasher = Hasher::new(); + hashes + .iter() + .fold(hasher, |hasher, h| hasher.chain(*h)) + .result() + .to_vec() +} diff --git a/base_layer/mmr/tests/with_blake512_hash.rs b/base_layer/mmr/tests/with_blake512_hash.rs new file mode 100644 index 0000000000..4563ec1f0e --- /dev/null +++ b/base_layer/mmr/tests/with_blake512_hash.rs @@ -0,0 +1,100 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use blake2::Blake2b; +use digest::Digest; +use std::string::ToString; +use tari_mmr::MerkleMountainRange; +use tari_utilities::hex::Hex; + +pub fn hash_values() -> Vec { + let mut hashvalues = Vec::new(); + // list of hex values of blake2b hashes + hashvalues.push("1ced8f5be2db23a6513eba4d819c73806424748a7bc6fa0d792cc1c7d1775a9778e894aa91413f6eb79ad5ae2f871eafcc78797e4c82af6d1cbfb1a294a10d10".to_string()); // 1 + hashvalues.push("c5faca15ac2f93578b39ef4b6bbb871bdedce4ddd584fd31f0bb66fade3947e6bb1353e562414ed50638a8829ff3daccac7ef4a50acee72a5384ba9aeb604fc9".to_string()); // 2 + hashvalues.push("4d3d9d4c8da746e2dcf236f31b53850e0e35a07c1d6082be51b33e7c1e11c39cf5e309953bf56866b0ccede95cdf3ae5f9823f6cf3bcc6ada19cf21b09884717".to_string()); // (1-2) + hashvalues.push("6f760b9e9eac89f07ab0223b0f4acb04d1e355d893a1b86a83f4d4b405adee99913dacb7bc3d6e6a46f996e59b965e82b1ffa1994062bcd8bef867bcf743c07c".to_string()); // 3 + hashvalues.push("e8e70dc170e14333627b32c20ac6051fb9b6bd369c036afbaca2d9cd7ac3de65aeda9d9651423af4343fd8e13f6481081b473e22a58f3f0e2a28143e4fb70bc2".to_string()); // 4 + hashvalues.push("ebf20fe26f69ab804b760fbf55eac3eba8f6cffa3f85d7b0c29ffd4a66a28deecc6f9eaaae758c49334f8b10ccfc743cee732e5486166cd3313a1881f7e0519e".to_string()); // (3-4) + hashvalues.push("8989c1ea10efac5b9897e9c227b307fd029005ba4f8e1590ec23942c3e788d7d280bb3cdbbd76cc9814755ee508174cb1d79a45f575a33240ac4b892ada7f850".to_string()); // (1-2)(3-4) + hashvalues.push("73776e3e4cd3684316d26ec93cc6c438497ace5b08e359698667af6dbded88b6750ba0b2c11ba7d52b69180f1924884a158d0b83d87ca9c65d2dae9d73387e43".to_string()); // 5 + hashvalues.push("8d322d4b02d9fcfb05bc70e486406e53c3cf9b97a252bf64752cafc5c2aaf95baef7f6e30d0a64826921ad01ec9d8c010805367078e5b5963ab4be3efd8f4a78".to_string()); // 6 + hashvalues.push("4a10141b2ba124991ddd81b4df78655f582872ba67928bbfc48282609de20ca40f745f622989cf3b71c790de6136173f6282780b2b7770b561f239ecddd40b78".to_string()); // (5-6) + // index 10 ^ + hashvalues.push("d5c47f63555ae063383c2a0df82bf309d90932bc8dd66a056d80e4d913e821faacf7e0e962c7bbac6c193e1e638b58b8baa1e71f57a945958b84c11536b7a82d".to_string()); // 7 + hashvalues.push("818af2ae014b14c85a35639901ac6bfc47908bcbd94a7f5211627b1f52f316a994e1296503701dd6827a8e5969d33d1d0b68c452eb95e481035b168a6c0f09c4".to_string()); // 8 + hashvalues.push("5b2abeb00cacb7465131a995bd4f5463032e69e1d3d9a55823536660d130a41bb23b529eec173ddd88a42e5db97cf6983cad0b36ef3de452ac66aba9f37b08ee".to_string()); // (7-8) + hashvalues.push("53d5d4b1b2f78468fea0292af1cb9e63a2e7460a66cc741756166e135817f20a6b96b60a76dee7f83615d881dfd58e3830003177d4aff13e392889e36f8c5718".to_string()); // (5-6)(7-8) + hashvalues.push("84247c7a397b4e7314a2a5edc993b12196fcbd2d8b3793d7cf8a63e9c5c8004103874260defe34a4ac739ed21d58bb9c325f96ba9d917d63295f71f45ce0054c".to_string()); // (1-2)(3-4)(5-6)(7-8) + hashvalues.push("2b57bf7664a4de943d93e4f5473a42da0d7a35065afd559303196fcc33414e73a91042f8d238fcaca45a93b17e577ad15191f95c6d7cf7c19e240a1e05100ad6".to_string()); // 9 + hashvalues.push("f2e74cbc3eff574bbc45333c30edb947858543afda4cafdde2903324c9de0bd908b00575c556bd7b8aa2e32a32598a4d5f95cd4490b60a567a3d53680a3310f2".to_string()); // 10 + hashvalues.push("7c7f5fb40b9d000435c001b05ab6e1409160d24292d8acb9bbd0936a07613fa82ccb01d65b92d5cd3f2103514fba108bdf1d960eeb4c75948cb716cde5c7fb4a".to_string()); // (9-10) + hashvalues.push("7aa7e388f8145d395ac616bb526eaa35b10069f49e2b36d7327157d1d4af360dfbbfea805aa7e405ed025ce5eadd56c27c40b92991727a5a16b51df5604ad006".to_string()); // 11 + hashvalues.push("b7a5a0f0fb0c4a128b8a3e042fc860775d68d825bb3bf180479d0e12b1884e2652fe51ddb9c991b73824fc15609d82cb1cc19053db7dc7637288091f6027bbce".to_string()); // 12 + // index 20 ^ + hashvalues.push("354db9c951738783a2d7c8c7301b1aedb4ed469df4b3bfa0368a69ab260ef0087952a7aca45ea67e7cd646aaacff6c9d75b60f194b39e6ad1f194df8b35a27c0".to_string()); // (11-12) + hashvalues.push("aea22e000365db9566cdab7d709c3c26e738bd41ac1f71cd2e4ad4d6f99e4286801e10d77cdea087b49ad135446130a0a32792250ba28bd211ffb68fe5d04fb0".to_string()); // (9-10)(11-12) + hashvalues.push("1da541ba91a8560c5dd0c1a4adc836dc4ac96bf5c407a89edb0a49d46de058a713c7b3d3fc8e0324f602c3a41978ef01dccb989eed22aa65bddc5621765713d3".to_string()); // 13 + hashvalues.push("2b789cf44e92c3eacb652124e394b132337fc19378664e376a932723cebf2e0da057319d509a04fe403f2c563542932d1f44476b8f4cad6ccefbd2693c432d1c".to_string()); // 14 + hashvalues.push("e4c46b221c1a82165c03816066af4c9546440705328dd1e419a04a17fbba70a717f67423fe1a553043c51e49cd369f02da979245007a5d09fd6ce0f2cb745491".to_string()); // (13-14) + hashvalues.push("4a9bb12a4834e77430779ea6759d0f4eb45abb9400a67b81985cd4b85e0a28b5d6b59f896ccc72cd6aad3390b51b02c7d6aeeb8f0dce205f425697e5180b35ae".to_string()); // 15 + hashvalues.push("3346703bc50521b2bf93e8d581605de18ad415c3dcdc38373e37c1800fd332e67c9ef7267d546913b63f5e24324d0c5565c177030d6c30c254d647440191d95f".to_string()); // 16 + hashvalues.push("39495d1ad29c6469ae18bc7316d98977754e0fdbb04a9e3e17c86c34f7fa751e09bbec588e8cfd5d4e55824b9705b1f52ab1a37b5b1fa5c8ea57b0951bdbccf3".to_string()); // (15-16) + hashvalues.push("77f4a6e8cb87bc79fea9893eeb2dde8a047b0d5786d324a2fb53f43414cfc8051d704f6088102fdf244de046fd5f8ea6cef854dc97488b173a0bb8d540c406ef".to_string()); // (13-14)(15-16) + hashvalues.push("af3f03f275e586e4449ff44146a27792b0f5a2143483a6dd6fe8405bd66a7ebe13f916d56bd3a152c2a25b6423f8b1bb4620f6d27fe55f1b82da61ff9b0825da".to_string()); // (9-10)(11-12)(13-14)(15-16) + // index 30 ^ + hashvalues.push("9a9a504247f809735602e7fdbe191c6129c075f6e1e1530bcfd45ab5e0f1c5974cce5d3eafed04b64b5c881ce369a272f6eca5f403178a51f677aedd6fe66d84".to_string()); // (1-2)(3-4)(5-6)(7-8)(9-10)(11-12)(13-14)(15-16) + hashvalues.push("5c3f20d14860fb11dca47a3ea972842763165f4cd657608df25fc8afe0cd67666d906cc36b556dccf7d0f9deafbd934fa466391a4f97d03b9fd3cf48f43346ad".to_string()); // 17 + hashvalues.push("2344823c898d803bb0421d8e0e99dafb3feabd3fff02f98a9dae1eabf748c99c6beeb899a65c6a1a83ce60dc8c58332571ccefd11515447d69c73cb4415903a4".to_string()); // 18 + hashvalues.push("a6ff99c73df5c5e9e01b2d6ffd923deba66c1eaa5c60699665c941569b09c756af55aaec9ff8469c7ffe9abd3ca5a3d1ada50ed4ee2cd3ff949177975f4f5141".to_string()); // (17-18) + hashvalues.push("33a389ba39d39595f2e43650eeaac81187c3a11c56f2930b042325c67adad310dad7ff9ed8077cfb0fa5136a2cfa725e55d567e7dac3483d5fb0ee787a0765ec".to_string()); // 19 + hashvalues.push("92ce61bf50a5c299bc88d6adad5db7b68c4b61abb7760947e8b9898c99312b18ba974d427e1699ede1be7c1c25b03440235a41a71ab2b4d1410399b72da87111".to_string()); // 20 + hashvalues.push("74d84a50748a78c7b98dcc9e22a62d64c726cb0e30126a26d8168e7252f4a67149506a4acde7a307372ebd0a0bcd3ef5f5670434262783d41675448ab7d06e3f".to_string()); // (19-20) + hashvalues.push("a14d655ecdf12d3dacc2bb9c6779345db9e08fa8ddaa2163ada5d2ff3c21b9bd5b9d59f4f7fbe489543deafd0e2ca45b75d7f7fd047b83e74b85b1ea0a5ec5ff".to_string()); // (17-18)(19-20) + hashvalues.push("8c715c0b894785852fbc391d662e2131bf0f0c703852f25b1c07429f35dc67ec8df5998acd4cafd4f1ff7019ebfda0877f79d6b91c1b98084efbb7314258608c".to_string()); // 21 + hashvalues.push("3733d5bf4f3d2608ba160adf4a8cddbf545f77b417e3ee3a9e5d3b0afb351579125db853e5bce15d5e82c723f29de1ef294341f0ca3e8b3d3431cec7ac316f34".to_string()); // 22 + // index 40 ^ + hashvalues.push("77288840877c30ddc8769efac9786505e15729f3a4736996a3b4aed483e896f001acee59b8592ae3d37acbdc60467239dac09bf80a999675b0c2aca058a4003d".to_string()); // (21-22) + hashvalues.push("08949f758439c6293fe5924defaf3e32bb79b9a93c1331f019c51b386557a9412b27f5a60a80bfa1f524c0d0c2e1f63c5b93d108a9a3af8cdb7fc87c765fca3f".to_string()); // 23 + + hashvalues +} + +fn create_mmr() -> MerkleMountainRange>> { + let mut mmr = MerkleMountainRange::::new(Vec::default()); + for i in 1..24 { + let hash = Blake2b::digest(i.to_string().as_bytes()).to_vec(); + assert!(mmr.push(&hash).is_ok()); + } + mmr +} + +#[test] +fn check_mmr_hashes() { + let mmr = create_mmr(); + let hashes = hash_values(); + assert_eq!(mmr.len(), 42); + for i in 0..42 { + let hash = mmr.get_node_hash(i).unwrap(); + assert_eq!(hash.to_hex(), hashes[i]); + } +} diff --git a/base_layer/p2p/Cargo.toml b/base_layer/p2p/Cargo.toml index d29d74b525..86f3f19078 100644 --- a/base_layer/p2p/Cargo.toml +++ b/base_layer/p2p/Cargo.toml @@ -1,5 +1,43 @@ [package] -name = "p2p" -version = "0.0.1" +name = "tari_p2p" +version = "0.0.5" +edition = "2018" [dependencies] +tari_comms = { version = "^0.0", path = "../../comms"} +tari_crypto = { version = "^0.0", path = "../../infrastructure/crypto"} +tari_utilities = { version = "^0.0", path = "../../infrastructure/tari_util"} +tari_service_framework = { version = "^0.0", path = "../service_framework"} + +chrono = { version = "0.4.6", features = ["serde"]} +crossbeam-channel = "0.3.8" +derive-error = "0.0.4" +futures = "0.1.28" +lmdb-zero = "0.4.4" +log = "0.4.6" +rand = "0.6.5" +rmp-serde = "0.13.7" +serde = "1.0.90" +serde_derive = "1.0.90" +tari_storage = {version = "^0.0", path = "../../infrastructure/storage"} +threadpool = "1.7.1" +tokio = "0.1.22" +tokio-threadpool = "0.1.15" +tower-service = "0.2.0" +tower-util = "0.1.0" +tracing = "0.1.5" +ttl_cache = "0.5.1" + +[dev-dependencies] +clap = "2.33.0" +cursive = "0.12.0" +lazy_static = "1.3.0" +simple_logger = "1.3.0" +stream-cancel = "0.4.4" +tempdir = "0.3.7" +tokio-mock-task = "0.1.1" + +[dev-dependencies.log4rs] +version ="0.8.3" +features = ["console_appender", "file_appender", "file", "yaml_format"] +default-features = false diff --git a/base_layer/p2p/examples/README.md b/base_layer/p2p/examples/README.md new file mode 100644 index 0000000000..57a34acb96 --- /dev/null +++ b/base_layer/p2p/examples/README.md @@ -0,0 +1,39 @@ +# Tari p2p examples + +Examples for using the `tari_p2p` crate. + +To run: + +```bash +cargo run --example $name -- [args] +``` + +## C Dependencies + +- [libzmq](https://github.com/zeromq/libzmq) +- [ncurses](https://github.com/mirror/ncurses) + +--- + +## Examples + +### [`gen_node_identity.rs`](gen_node_identity.rs) + +Generates a random node identity JSON file. A node identity contains a node's public and secret keys, it's node id and +an address used to establish peer connections. The files generated from this example are used to populate the +peer manager in other examples. + +```bash +cargo run --example gen_node_identity -- --help +cargo run --example gen_node_identity -- --output=examples/sample_identities/node-identity.json +``` + +### [`pingpong.rs`](pingpong.rs) + +A basic ncurses UI that sends ping and receives pong messages to a single peer using the `tari_p2p` library. +Press 'p' to send a ping. + +```bash +cargo run --example pingpong -- --help +cargo run --example pingpong -- --node-identity examples/sample_identities/node-identity1.json --peer-identity examples/sample_identities/node-identity2.json +``` diff --git a/base_layer/p2p/examples/example-log-config.yml b/base_layer/p2p/examples/example-log-config.yml new file mode 100644 index 0000000000..930657fbf2 --- /dev/null +++ b/base_layer/p2p/examples/example-log-config.yml @@ -0,0 +1,29 @@ +# See https://docs.rs/log4rs/0.8.3/log4rs/encode/pattern/index.html for deciphering the log pattern. +appenders: + # An appender named "network" that writes to a file with a custom pattern encoder + network: + kind: file + path: "base_layer/p2p/examples/log/comms-debug.log" + encoder: + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{M}#{L}] [{t}] {l:5} {m} (({T}:{I})){n}" + + # An appender named "pingpong" that writes to a file with a custom pattern encoder + pingpong: + kind: file + path: "base_layer/p2p/examples/log/pingpong-debug.log" + encoder: + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{M}#{L}] [{t}] {l:5} {m} (({T}:{I})){n}" + +# Set the default logging level to "debug" and attach the "base_layer" appender to the root +root: + level: debug + appenders: + - pingpong + +loggers: + # Route log events sent to the "comms" logger to the "network" appender + comms: + level: debug + appenders: + - network + additive: false \ No newline at end of file diff --git a/base_layer/p2p/examples/gen_node_identity.rs b/base_layer/p2p/examples/gen_node_identity.rs new file mode 100644 index 0000000000..edfb33ce1c --- /dev/null +++ b/base_layer/p2p/examples/gen_node_identity.rs @@ -0,0 +1,74 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// Generates a random node identity JSON file. A node identity contains a node's public and secret keys, it's node +/// id and an address used to establish peer connections. The files generated from this example are used to +/// populate the peer manager in other examples. +use clap::{App, Arg}; +use rand::{rngs::OsRng, Rng}; +use std::{env::current_dir, fs, net::Ipv4Addr, path::Path}; +use tari_comms::{ + connection::{net_address::ip::SocketAddress, NetAddress}, + peer_manager::NodeIdentity, +}; +use tari_utilities::message_format::MessageFormat; + +fn random_address() -> NetAddress { + let mut rng = OsRng::new().unwrap(); + let port = rng.gen_range(9000, std::u16::MAX); + let socket_addr: SocketAddress = (Ipv4Addr::LOCALHOST, port).into(); + socket_addr.into() +} + +fn to_abs_path(path: &str) -> String { + let path = Path::new(path); + if path.is_absolute() { + path.to_str().unwrap().to_string() + } else { + let mut abs_path = current_dir().unwrap(); + abs_path.push(path); + abs_path.to_str().unwrap().to_string() + } +} + +fn main() { + let matches = App::new("Peer file generator") + .version("1.0") + .about("Generates peer json files") + .arg( + Arg::with_name("output") + .value_name("FILE") + .long("output") + .short("o") + .help("The relative path of the file to output") + .takes_value(true) + .required(true), + ) + .get_matches(); + + let mut rng = OsRng::new().unwrap(); + let address = random_address(); + let node_identity = NodeIdentity::random(&mut rng, address).unwrap(); + let json = node_identity.to_json().unwrap(); + let out_path = to_abs_path(matches.value_of("output").unwrap()); + fs::write(out_path, json).unwrap(); +} diff --git a/base_layer/p2p/examples/pingpong.rs b/base_layer/p2p/examples/pingpong.rs new file mode 100644 index 0000000000..33e26eb9ad --- /dev/null +++ b/base_layer/p2p/examples/pingpong.rs @@ -0,0 +1,217 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// A basic ncurses UI that sends ping and receives pong messages to a single peer using the `tari_p2p` library. +/// Press 'p' to send a ping. + +#[macro_use] +extern crate lazy_static; + +use clap::{App, Arg}; +use cursive::{ + view::Identifiable, + views::{Dialog, TextView}, + Cursive, +}; +use rand::{distributions::Alphanumeric, rngs::OsRng, Rng}; +use std::{ + fs, + iter, + sync::{ + mpsc::{channel, RecvTimeoutError}, + Arc, + RwLock, + }, + thread, + time::Duration, +}; +use tari_comms::{ + control_service::ControlServiceConfig, + peer_manager::{NodeIdentity, Peer, PeerFlags, PeerNodeIdentity}, +}; +use tari_p2p::{ + initialization::{initialize_comms, CommsConfig}, + ping_pong::{PingPongService, PingPongServiceApi}, + sync_services::{ServiceError, ServiceExecutor, ServiceRegistry}, +}; +use tari_utilities::message_format::MessageFormat; +use tempdir::TempDir; + +fn load_identity(path: &str) -> NodeIdentity { + let contents = fs::read_to_string(path).unwrap(); + NodeIdentity::from_json(contents.as_str()).unwrap() +} + +pub fn random_string(len: usize) -> String { + let mut rng = OsRng::new().unwrap(); + iter::repeat(()).map(|_| rng.sample(Alphanumeric)).take(len).collect() +} + +fn main() { + let matches = App::new("Tari comms peer to peer ping pong example") + .version("1.0") + .about("PingPong between two peers") + .arg( + Arg::with_name("node-identity") + .value_name("FILE") + .long("node-identity") + .short("n") + .help("The relative path of the node identity file to use for this node") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("peer-identity") + .value_name("FILE") + .long("peer-identity") + .short("p") + .help("The relative path of the node identity file of the other node") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("log-config") + .value_name("FILE") + .long("log-config") + .short("l") + .help("The relative path of the log config file of the other node") + .takes_value(true) + .default_value("base_layer/p2p/examples/example-log-config.yml"), + ) + .get_matches(); + + log4rs::init_file(matches.value_of("log-config").unwrap(), Default::default()).unwrap(); + + let node_identity = load_identity(matches.value_of("node-identity").unwrap()); + let peer_identity = load_identity(matches.value_of("peer-identity").unwrap()); + + let comms_config = CommsConfig { + public_key: node_identity.identity.public_key.clone(), + host: "0.0.0.0".parse().unwrap(), + socks_proxy_address: None, + control_service: ControlServiceConfig { + listener_address: node_identity.control_service_address().unwrap(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(2000), + }, + secret_key: node_identity.secret_key.clone(), + public_address: node_identity.control_service_address().unwrap(), + datastore_path: TempDir::new(random_string(8).as_str()) + .unwrap() + .path() + .to_str() + .unwrap() + .to_string(), + peer_database_name: random_string(8), + }; + + let pingpong_service = PingPongService::new(); + let pingpong = pingpong_service.get_api(); + let services = ServiceRegistry::new().register(pingpong_service); + + let comms = initialize_comms(comms_config).unwrap(); + let peer = Peer::new( + peer_identity.identity.public_key.clone(), + peer_identity.identity.node_id.clone(), + peer_identity.control_service_address().unwrap().into(), + PeerFlags::empty(), + ); + comms.peer_manager().add_peer(peer).unwrap(); + + let services = ServiceExecutor::execute(&comms, services); + + run_ui(services, peer_identity.identity, pingpong); + + let comms = Arc::try_unwrap(comms) + .map_err(|_| ServiceError::CommsServiceOwnershipError) + .unwrap(); + + comms.shutdown().unwrap(); +} + +lazy_static! { + /// Used to keep track of the counts displayed in the UI + /// (sent ping count, recv ping count, recv pong count) + static ref COUNTER_STATE: Arc> = Arc::new(RwLock::new((0, 0, 0))); +} + +fn run_ui(services: ServiceExecutor, peer_identity: PeerNodeIdentity, pingpong_api: Arc) { + let mut app = Cursive::default(); + + app.add_layer( + Dialog::around(TextView::new("Loading...").with_id("counter")) + .title("PingPong") + .button("Quit", |s| s.quit()), + ); + + let update_sink = app.cb_sink().clone(); + + let inner_api = pingpong_api.clone(); + let (shutdown_tx, shutdown_rx) = channel(); + let app_handle = thread::spawn(move || loop { + { + let mut lock = COUNTER_STATE.write().unwrap(); + *lock = ( + lock.0, + pingpong_api.ping_count().unwrap(), + pingpong_api.pong_count().unwrap(), + ); + } + update_sink.send(Box::new(update_count)).unwrap(); + match shutdown_rx.recv_timeout(Duration::from_millis(100)) { + Ok(_) => break, + Err(RecvTimeoutError::Timeout) => {}, + Err(RecvTimeoutError::Disconnected) => break, + } + }); + + let ping_update_cb = app.cb_sink().clone(); + + let pk_to_ping = peer_identity.public_key.clone(); + app.add_global_callback('p', move |_| { + inner_api.ping(pk_to_ping.clone()).unwrap(); + { + let mut lock = COUNTER_STATE.write().unwrap(); + *lock = (lock.0 + 1, lock.1, lock.2); + } + ping_update_cb.send(Box::new(update_count)).unwrap(); + }); + app.add_global_callback('q', |s| s.quit()); + + app.run(); + + shutdown_tx.send(()).unwrap(); + services.shutdown().unwrap(); + services.join_timeout(Duration::from_millis(1000)).unwrap(); + app_handle.join().unwrap(); +} + +fn update_count(s: &mut Cursive) { + s.call_on_id("counter", move |view: &mut TextView| { + let lock = COUNTER_STATE.read().unwrap(); + view.set_content(format!( + "Pings sent: {}\nPings Received: {}\nPongs Received: {}\n\n(p) Send ping (q) Quit", + lock.0, lock.1, lock.2 + )); + }); + s.set_autorefresh(true); +} diff --git a/base_layer/p2p/examples/pingpong_async.rs b/base_layer/p2p/examples/pingpong_async.rs new file mode 100644 index 0000000000..fea3710f53 --- /dev/null +++ b/base_layer/p2p/examples/pingpong_async.rs @@ -0,0 +1,249 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// A basic ncurses UI that sends ping and receives pong messages to a single peer using the `tari_p2p` library. +/// Press 'p' to send a ping. + +#[macro_use] +extern crate lazy_static; + +use clap::{App, Arg}; +use cursive::{ + view::Identifiable, + views::{Dialog, TextView}, + Cursive, +}; +use futures::{future, sync::mpsc, Future, Sink, Stream}; +use rand::{distributions::Alphanumeric, rngs::OsRng, Rng}; +use std::{ + fs, + iter, + sync::{Arc, RwLock}, + time::{Duration, Instant}, +}; +use stream_cancel::{StreamExt, Tripwire}; +use tari_comms::{ + control_service::ControlServiceConfig, + peer_manager::{NodeIdentity, Peer, PeerFlags, PeerNodeIdentity}, +}; +use tari_p2p::{ + initialization::{initialize_comms, CommsConfig}, + services::{ + comms_outbound::CommsOutboundServiceInitializer, + liveness::{LivenessHandle, LivenessInitializer, LivenessRequest, LivenessResponse}, + ServiceHandles, + ServiceName, + }, +}; +use tari_service_framework::StackBuilder; +use tari_utilities::message_format::MessageFormat; +use tempdir::TempDir; +use tokio::{runtime::Runtime, timer::Interval}; +use tower_service::Service; + +fn load_identity(path: &str) -> NodeIdentity { + let contents = fs::read_to_string(path).unwrap(); + NodeIdentity::from_json(contents.as_str()).unwrap() +} + +pub fn random_string(len: usize) -> String { + let mut rng = OsRng::new().unwrap(); + iter::repeat(()).map(|_| rng.sample(Alphanumeric)).take(len).collect() +} + +fn main() { + let matches = App::new("Tari comms peer to peer ping pong example") + .version("1.0") + .about("PingPong between two peers") + .arg( + Arg::with_name("node-identity") + .value_name("FILE") + .long("node-identity") + .short("n") + .help("The relative path of the node identity file to use for this node") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("peer-identity") + .value_name("FILE") + .long("peer-identity") + .short("p") + .help("The relative path of the node identity file of the other node") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("log-config") + .value_name("FILE") + .long("log-config") + .short("l") + .help("The relative path of the log config file of the other node") + .takes_value(true) + .default_value("base_layer/p2p/examples/example-log-config.yml"), + ) + .get_matches(); + + log4rs::init_file(matches.value_of("log-config").unwrap(), Default::default()).unwrap(); + + let node_identity = load_identity(matches.value_of("node-identity").unwrap()); + let peer_identity = load_identity(matches.value_of("peer-identity").unwrap()); + + let comms_config = CommsConfig { + public_key: node_identity.identity.public_key.clone(), + host: "0.0.0.0".parse().unwrap(), + socks_proxy_address: None, + control_service: ControlServiceConfig { + listener_address: node_identity.control_service_address().unwrap(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(2000), + }, + secret_key: node_identity.secret_key.clone(), + public_address: node_identity.control_service_address().unwrap(), + datastore_path: TempDir::new(random_string(8).as_str()) + .unwrap() + .path() + .to_str() + .unwrap() + .to_string(), + peer_database_name: random_string(8), + }; + + let comms = initialize_comms(comms_config).unwrap(); + let peer = Peer::new( + peer_identity.identity.public_key.clone(), + peer_identity.identity.node_id.clone(), + peer_identity.control_service_address().unwrap().into(), + PeerFlags::empty(), + ); + comms.peer_manager().add_peer(peer).unwrap(); + + let mut rt = Runtime::new().unwrap(); + + let initialize = StackBuilder::new() + .add_initializer(CommsOutboundServiceInitializer::new(comms.outbound_message_service())) + .add_initializer(LivenessInitializer::new(Arc::clone(&comms))) + .finish(); + + let handles = rt.block_on(initialize).unwrap(); + + run_ui(peer_identity.identity, handles); + + let comms = Arc::try_unwrap(comms).map_err(|_| ()).unwrap(); + + comms.shutdown().unwrap(); +} + +lazy_static! { + /// Used to keep track of the counts displayed in the UI + /// (sent ping count, recv ping count, recv pong count) + static ref COUNTER_STATE: Arc> = Arc::new(RwLock::new((0, 0, 0))); +} + +fn run_ui(peer_identity: PeerNodeIdentity, handles: Arc) { + tokio::run(future::lazy(move || { + let mut app = Cursive::default(); + + app.add_layer( + Dialog::around(TextView::new("Loading...").with_id("counter")) + .title("PingPong") + .button("Quit", |s| s.quit()), + ); + + let (trigger, trip_wire) = Tripwire::new(); + + let update_sink = app.cb_sink().clone(); + + let mut liveness_handle = handles.get_handle::(ServiceName::Liveness).unwrap(); + let update_ui_stream = Interval::new(Instant::now(), Duration::from_millis(100)) + .take_until(trip_wire.clone()) + .map_err(|_| ()) + .for_each(move |_| { + let update_inner = update_sink.clone(); + liveness_handle + .call(LivenessRequest::GetPingCount) + .join(liveness_handle.call(LivenessRequest::GetPongCount)) + .and_then(move |(pings, pongs)| { + match (pings, pongs) { + (Ok(LivenessResponse::Count(num_pings)), Ok(LivenessResponse::Count(num_pongs))) => { + { + let mut lock = COUNTER_STATE.write().unwrap(); + *lock = (lock.0, num_pings, num_pongs); + } + let _ = update_inner.send(Box::new(update_count)); + }, + _ => {}, + } + + future::ok(()) + }) + .map_err(|_| ()) + }); + + tokio::spawn(update_ui_stream); + + let ping_update_cb = app.cb_sink().clone(); + let mut liveness_handle = handles.get_handle::(ServiceName::Liveness).unwrap(); + let pk_to_ping = peer_identity.public_key.clone(); + + let (mut send_ping_tx, send_ping_rx) = mpsc::channel(10); + app.add_global_callback('p', move |_| { + let _ = send_ping_tx.start_send(()); + }); + + let p_stream = send_ping_rx.take_until(trip_wire).for_each(move |_| { + let ping_update_inner = ping_update_cb.clone(); + liveness_handle + .call(LivenessRequest::SendPing(pk_to_ping.clone())) + .map_err(|_| ()) + .and_then(move |_| { + { + let mut lock = COUNTER_STATE.write().unwrap(); + *lock = (lock.0 + 1, lock.1, lock.2); + } + ping_update_inner.send(Box::new(update_count)).unwrap(); + future::ok(()) + }) + }); + + tokio::spawn(p_stream); + + app.add_global_callback('q', |s| s.quit()); + + app.run(); + + // Signal UI streams to stop + trigger.cancel(); + future::ok(()) + })); +} + +fn update_count(s: &mut Cursive) { + s.call_on_id("counter", move |view: &mut TextView| { + let lock = COUNTER_STATE.read().unwrap(); + view.set_content(format!( + "Pings sent: {}\nPings Received: {}\nPongs Received: {}\n\n(p) Send ping (q) Quit", + lock.0, lock.1, lock.2 + )); + }); + s.set_autorefresh(true); +} diff --git a/base_layer/p2p/examples/sample_identities/node-identity1.json b/base_layer/p2p/examples/sample_identities/node-identity1.json new file mode 100644 index 0000000000..7351da716f --- /dev/null +++ b/base_layer/p2p/examples/sample_identities/node-identity1.json @@ -0,0 +1,8 @@ +{ + "identity": { + "node_id": "b3892370783330158ac6f40b7d38029ee33d554cd4522d0548058c6d9cb0ce7b", + "public_key": "b6663ccdec9546d8bc528cba869cb57c28826da2dbe2df2735b795eff6605117" + }, + "secret_key": "7756a61d2911481492e7156ce6ddb1e46c5ea24a24157c599486f929ea288c02", + "control_service_address": {"IP": "127.0.0.1:57203"} +} diff --git a/base_layer/p2p/examples/sample_identities/node-identity2.json b/base_layer/p2p/examples/sample_identities/node-identity2.json new file mode 100644 index 0000000000..fef3473d8f --- /dev/null +++ b/base_layer/p2p/examples/sample_identities/node-identity2.json @@ -0,0 +1,8 @@ +{ + "identity": { + "node_id": "037ff81d7e04338961dedbc6750eaea87b5aa5f42b90ef0b27681a78f9673c54", + "public_key": "c4b2f064fdc61229f9d3e7cc67a2e326e252ace50885f315f4d6f262fff1f869" + }, + "secret_key": "b1e8f6e901c6722e71e36bf7fb8255cf5a8c31803913cab3c10428e76cca3e0e", + "control_service_address": {"IP": "127.0.0.1:48579"} +} diff --git a/base_layer/p2p/src/consts.rs b/base_layer/p2p/src/consts.rs new file mode 100644 index 0000000000..1472ae129c --- /dev/null +++ b/base_layer/p2p/src/consts.rs @@ -0,0 +1,33 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::time::Duration; + +/// The maximum number of peer nodes that a message will be sent to +pub const DHT_BROADCAST_NODE_COUNT: usize = 8; + +/// The maximum number of messages that can be stored using the Store-and-forward service +pub const SAF_MSG_CACHE_STORAGE_CAPACITY: usize = 10000; +/// The time-to-live duration used for storage of low priority messages by the Store-and-forward service +pub const SAF_LOW_PRIORITY_MSG_STORAGE_TTL: Duration = Duration::from_secs(6 * 60 * 60); +/// The time-to-live duration used for storage of high priority messages by the Store-and-forward service +pub const SAF_HIGH_PRIORITY_MSG_STORAGE_TTL: Duration = Duration::from_secs(24 * 60 * 60); diff --git a/base_layer/p2p/src/dht_service/dht_messages.rs b/base_layer/p2p/src/dht_service/dht_messages.rs new file mode 100644 index 0000000000..21b887decd --- /dev/null +++ b/base_layer/p2p/src/dht_service/dht_messages.rs @@ -0,0 +1,64 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::tari_message::{NetMessage, TariMessageType}; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use tari_comms::{ + connection::NetAddress, + message::{Message, MessageError}, + peer_manager::NodeId, +}; + +/// The JoinMessage stores the information required for a network join request. It has all the information required to +/// locate and contact the source node, but network behaviour is different compared to DiscoverMessage. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct JoinMessage { + pub node_id: NodeId, + // TODO: node_type + pub net_address: Vec, +} + +impl TryInto for JoinMessage { + type Error = MessageError; + + fn try_into(self) -> Result { + Ok((TariMessageType::new(NetMessage::Join), self).try_into()?) + } +} + +/// The DiscoverMessage stores the information required for a network discover request. It has all the information +/// required to locate and contact the source node, but network behaviour is different compared to JoinMessage. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct DiscoverMessage { + pub node_id: NodeId, + // TODO: node_type + pub net_address: Vec, +} + +impl TryInto for DiscoverMessage { + type Error = MessageError; + + fn try_into(self) -> Result { + Ok((TariMessageType::new(NetMessage::Discover), self).try_into()?) + } +} diff --git a/base_layer/p2p/src/dht_service/dht_service.rs b/base_layer/p2p/src/dht_service/dht_service.rs new file mode 100644 index 0000000000..c8c619a0a5 --- /dev/null +++ b/base_layer/p2p/src/dht_service/dht_service.rs @@ -0,0 +1,455 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + consts::DHT_BROADCAST_NODE_COUNT, + dht_service::{ + dht_messages::{DiscoverMessage, JoinMessage}, + DHTError, + }, + sync_services::{ + Service, + ServiceApiWrapper, + ServiceContext, + ServiceControlMessage, + ServiceError, + DEFAULT_API_TIMEOUT_MS, + }, + tari_message::{NetMessage, TariMessageType}, +}; +use crossbeam_channel as channel; +use log::*; +use std::{ + convert::TryInto, + sync::{Arc, Mutex}, + time::Duration, +}; +use tari_comms::{ + connection::NetAddress, + domain_connector::MessageInfo, + message::{Frame, Message, MessageEnvelope, MessageFlags, NodeDestination}, + outbound_message_service::{outbound_message_service::OutboundMessageService, BroadcastStrategy, ClosestRequest}, + peer_manager::{NodeId, NodeIdentity, Peer, PeerFlags, PeerManager}, + types::CommsPublicKey, + DomainConnector, +}; +use tari_utilities::message_format::MessageFormat; + +const LOG_TARGET: &str = "base_layer::p2p::dht"; + +/// The DHTService manages joining the network and discovery of peers. +pub struct DHTService { + node_identity: Option>, + prev_control_service_address: Option, + oms: Option>, + peer_manager: Option>, + api: ServiceApiWrapper, +} + +impl DHTService { + /// Create a new DHT service + pub fn new() -> Self { + Self { + node_identity: None, + prev_control_service_address: None, + oms: None, + peer_manager: None, + api: Self::setup_api(), + } + } + + /// Return this services API + pub fn get_api(&self) -> Arc { + self.api.get_api() + } + + fn setup_api() -> ServiceApiWrapper { + let (api_sender, service_receiver) = channel::bounded(0); + let (service_sender, api_receiver) = channel::bounded(0); + + let api = Arc::new(DHTServiceApi::new(api_sender, api_receiver)); + ServiceApiWrapper::new(service_receiver, service_sender, api) + } + + /// Construct a new join message that contains the current nodes identity and net_addresses + fn construct_join_msg(&self) -> Result { + let node_identity = self.node_identity.as_ref().ok_or(DHTError::NodeIdentityUndefined)?; + Ok(JoinMessage { + node_id: node_identity.identity.node_id.clone(), + net_address: vec![node_identity.control_service_address()?], + }) + } + + /// Construct a new discover message that contains the current nodes identity and net_addresses + fn construct_discover_msg(&self) -> Result { + let node_identity = self.node_identity.as_ref().ok_or(DHTError::NodeIdentityUndefined)?; + Ok(DiscoverMessage { + node_id: node_identity.identity.node_id.clone(), + net_address: vec![node_identity.control_service_address()?], + }) + } + + /// Send a new network join request to the peers that are closest to the current nodes network location. The Join + /// Request will allow other peers to be able to find this node on the network. + fn send_join(&self) -> Result<(), DHTError> { + let oms = self.oms.as_ref().ok_or(DHTError::OMSUndefined)?; + let node_identity = self.node_identity.as_ref().ok_or(DHTError::NodeIdentityUndefined)?; + + oms.send_message( + BroadcastStrategy::Closest(ClosestRequest { + n: DHT_BROADCAST_NODE_COUNT, + node_id: node_identity.identity.node_id.clone(), + excluded_peers: Vec::new(), + }), + MessageFlags::NONE, + self.construct_join_msg()?, + )?; + trace!(target: LOG_TARGET, "Join Request Sent"); + + Ok(()) + } + + /// Send a network join update request directly to a specific known peer + fn send_join_direct(&self, dest_public_key: CommsPublicKey) -> Result<(), DHTError> { + let oms = self.oms.as_ref().ok_or(DHTError::OMSUndefined)?; + + oms.send_message( + BroadcastStrategy::DirectPublicKey(dest_public_key), + MessageFlags::ENCRYPTED, + self.construct_join_msg()?, + )?; + trace!(target: LOG_TARGET, "Direct Join Request Sent"); + + Ok(()) + } + + /// Send a discover request to find a specific peer on the network + fn send_discover( + &self, + dest_public_key: CommsPublicKey, + dest_node_id: Option, + header_dest: NodeDestination, + ) -> Result<(), DHTError> + { + let oms = self.oms.as_ref().ok_or(DHTError::OMSUndefined)?; + let node_identity = self.node_identity.as_ref().ok_or(DHTError::NodeIdentityUndefined)?; + + let discover_msg: Message = self + .construct_discover_msg()? + .try_into() + .map_err(DHTError::MessageSerializationError)?; + let message_envelope_body: Frame = discover_msg.to_binary().map_err(DHTError::MessageFormatError)?; + let message_envelope = MessageEnvelope::construct( + &node_identity, + dest_public_key, + header_dest.clone(), + message_envelope_body.clone(), + MessageFlags::ENCRYPTED, + ) + .map_err(DHTError::MessageSerializationError)?; + + let broadcast_strategy = BroadcastStrategy::discover( + node_identity.identity.node_id.clone(), + dest_node_id, + header_dest, + Vec::new(), + ); + oms.forward_message(broadcast_strategy, message_envelope)?; + + Ok(()) + } + + /// Process an incoming join request. The peer specified in the join request will be added to the PeerManager. If + /// the current Node and the join request Node are from the same region of the network then the current node will + /// send a join request back to that peer informing it that the current node is a neighbouring node. The join + /// request is then forwarded to closer nodes. + fn receive_join(&mut self, connector: &DomainConnector<'static>) -> Result<(), DHTError> { + let oms = self.oms.as_ref().ok_or(DHTError::OMSUndefined)?; + let peer_manager = self.peer_manager.as_ref().ok_or(DHTError::PeerManagerUndefined)?; + let node_identity = self.node_identity.as_ref().ok_or(DHTError::NodeIdentityUndefined)?; + + let incoming_msg: Option<(MessageInfo, JoinMessage)> = connector + .receive_timeout(Duration::from_millis(1)) + .map_err(DHTError::ConnectorError)?; + if let Some((info, join_msg)) = incoming_msg { + // TODO: Check/Verify the received peers information + + // Add peer or modify existing peer using received join request + if peer_manager.exists(&info.origin_source)? { + peer_manager.update_peer( + &info.origin_source, + Some(join_msg.node_id.clone()), + Some(join_msg.net_address.clone()), + None, + )?; + } else { + let peer = Peer::new( + info.origin_source.clone(), + join_msg.node_id.clone(), + join_msg.net_address.clone().into(), + PeerFlags::default(), + ); + peer_manager.add_peer(peer)?; + } + + // Send a join request back to the source peer of the join request if that peer is from the same region + // of network. Also, only Send a join request back if this copy of the received join + // request was not sent directly from the original source peer but was forwarded. If it + // was not forwarded then that source peer already has the current peers info in its + // PeerManager. + if (info.origin_source != info.peer_source.public_key) && + (peer_manager.in_network_region( + &join_msg.node_id, + &node_identity.identity.node_id, + DHT_BROADCAST_NODE_COUNT, + )?) + { + self.send_join_direct(info.origin_source.clone())?; + } + + // Propagate message to closer peers + // oms.forward_message( + // BroadcastStrategy::Closest(ClosestRequest { + // n: DHT_BROADCAST_NODE_COUNT, + // node_id: join_msg.node_id.clone(), + // excluded_peers: vec![info.origin_source, info.peer_source.public_key], + // }), + // info.message_envelope, + // )?; + } + + Ok(()) + } + + /// Process an incoming discover request that was meant for the current node + fn receive_discover(&mut self, connector: &DomainConnector<'static>) -> Result<(), DHTError> { + let peer_manager = self.peer_manager.as_ref().ok_or(DHTError::PeerManagerUndefined)?; + + let incoming_msg: Option<(MessageInfo, DiscoverMessage)> = connector + .receive_timeout(Duration::from_millis(1)) + .map_err(DHTError::ConnectorError)?; + if let Some((info, discover_msg)) = incoming_msg { + // TODO: Check/Verify the received peers information + + // Add peer or modify existing peer using received discover request + if peer_manager.exists(&info.origin_source)? { + peer_manager.update_peer( + &info.origin_source, + Some(discover_msg.node_id.clone()), + Some(discover_msg.net_address.clone()), + None, + )?; + } else { + let peer = Peer::new( + info.origin_source.clone(), + discover_msg.node_id.clone(), + discover_msg.net_address.clone().into(), + PeerFlags::default(), + ); + peer_manager.add_peer(peer)?; + } + + // Send the origin the current nodes latest contact info + self.send_join_direct(info.origin_source)?; + } + + Ok(()) + } + + /// The auto_join function sends a join request on startup or on the detection of a control_service_address change + fn auto_join(&mut self) -> Result<(), DHTError> { + let node_identity = self.node_identity.as_ref().ok_or(DHTError::NodeIdentityUndefined)?; + + if match self.prev_control_service_address.as_ref() { + Some(control_service_address) => *control_service_address != node_identity.control_service_address()?, /* Identity change detected */ + None => true, // Startup detected + } { + self.prev_control_service_address = Some(node_identity.control_service_address()?); + self.send_join()?; + } + + Ok(()) + } + + /// This handler is called when the Service executor loops receives an API request + fn handle_api_message(&self, msg: DHTApiRequest) -> Result<(), ServiceError> { + trace!( + target: LOG_TARGET, + "[{}] Received API message: {:?}", + self.get_name(), + msg + ); + let resp = match msg { + DHTApiRequest::SendJoin => self.send_join().map(|_| DHTApiResponse::JoinSent), + DHTApiRequest::SendDiscover(dest_public_key, dest_node_id, header_dest) => self + .send_discover(dest_public_key, dest_node_id, header_dest) + .map(|_| DHTApiResponse::DiscoverSent), + }; + + trace!(target: LOG_TARGET, "[{}] Replying to API: {:?}", self.get_name(), resp); + self.api + .send_reply(resp) + .map_err(ServiceError::internal_service_error()) + } +} + +/// The Domain Service trait implementation for the DHTService +impl Service for DHTService { + fn get_name(&self) -> String { + "dht".to_string() + } + + fn get_message_types(&self) -> Vec { + vec![NetMessage::Join.into(), NetMessage::Discover.into()] + } + + fn execute(&mut self, context: ServiceContext) -> Result<(), ServiceError> { + let connector_join = context.create_connector(&NetMessage::Join.into()).map_err(|err| { + ServiceError::ServiceInitializationFailed(format!("Failed to create connector for service: {}", err)) + })?; + + let connector_discover = context.create_connector(&NetMessage::Discover.into()).map_err(|err| { + ServiceError::ServiceInitializationFailed(format!("Failed to create connector for service: {}", err)) + })?; + + self.oms = Some(context.outbound_message_service()); + self.peer_manager = Some(context.peer_manager()); + self.node_identity = Some(context.node_identity()); + debug!(target: LOG_TARGET, "Starting DHT Service executor"); + loop { + if let Some(msg) = context.get_control_message(Duration::from_millis(5)) { + match msg { + ServiceControlMessage::Shutdown => break, + } + } + + match self.receive_join(&connector_join) { + Ok(_) => {}, + Err(err) => { + error!(target: LOG_TARGET, "DHT service had error: {:?}", err); + }, + } + + match self.receive_discover(&connector_discover) { + Ok(_) => {}, + Err(err) => { + error!(target: LOG_TARGET, "DHT service had error: {:?}", err); + }, + } + + match self.auto_join() { + Ok(_) => {}, + Err(err) => { + error!(target: LOG_TARGET, "DHT service had an auto join error: {:?}", err); + }, + } + + if let Some(msg) = self + .api + .recv_timeout(Duration::from_millis(5)) + .map_err(ServiceError::internal_service_error())? + { + self.handle_api_message(msg)?; + } + } + + Ok(()) + } +} + +/// API Request enum +#[derive(Debug)] +pub enum DHTApiRequest { + /// Send a join request to neighbouring peers on the network + SendJoin, + /// Send a discovery request to find a selected peer + SendDiscover(CommsPublicKey, Option, NodeDestination), +} + +/// API Response enum +#[derive(Debug)] +pub enum DHTApiResponse { + JoinSent, + DiscoverSent, +} + +/// Result for all API requests +pub type DHTApiResult = Result; + +/// The DHT service public API that other services and application will use to interact with this service. +/// The requests and responses are transmitted via channels into the Service Executor thread where this service is +/// running +pub struct DHTServiceApi { + sender: channel::Sender, + receiver: channel::Receiver, + mutex: Mutex<()>, + timeout: Duration, +} + +impl DHTServiceApi { + fn new(sender: channel::Sender, receiver: channel::Receiver) -> Self { + Self { + sender, + receiver, + mutex: Mutex::new(()), + timeout: Duration::from_millis(DEFAULT_API_TIMEOUT_MS), + } + } + + pub fn send_join(&self) -> Result<(), DHTError> { + self.send_recv(DHTApiRequest::SendJoin).and_then(|resp| match resp { + DHTApiResponse::JoinSent => Ok(()), + _ => Err(DHTError::UnexpectedApiResponse), + }) + } + + pub fn send_discover( + &self, + dest_public_key: CommsPublicKey, + dest_node_id: Option, + header_dest: NodeDestination, + ) -> Result<(), DHTError> + { + self.send_recv(DHTApiRequest::SendDiscover(dest_public_key, dest_node_id, header_dest)) + .and_then(|resp| match resp { + DHTApiResponse::DiscoverSent => Ok(()), + _ => Err(DHTError::UnexpectedApiResponse), + }) + } + + fn send_recv(&self, msg: DHTApiRequest) -> DHTApiResult { + self.lock(|| -> DHTApiResult { + self.sender.send(msg).map_err(|_| DHTError::ApiSendFailed)?; + self.receiver + .recv_timeout(self.timeout) + .map_err(|_| DHTError::ApiReceiveFailed)? + }) + } + + fn lock(&self, func: F) -> T + where F: FnOnce() -> T { + let lock = acquire_lock!(self.mutex); + let res = func(); + drop(lock); + res + } +} diff --git a/base_layer/p2p/src/dht_service/error.rs b/base_layer/p2p/src/dht_service/error.rs new file mode 100644 index 0000000000..53f67c4944 --- /dev/null +++ b/base_layer/p2p/src/dht_service/error.rs @@ -0,0 +1,53 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; +use tari_comms::{ + domain_connector::ConnectorError, + outbound_message_service::OutboundError, + peer_manager::PeerManagerError, +}; + +use tari_comms::{message::MessageError, peer_manager::node_identity::NodeIdentityError}; +use tari_utilities::message_format::MessageFormatError; + +#[derive(Debug, Error)] +pub enum DHTError { + OutboundError(OutboundError), + ConnectorError(ConnectorError), + /// OMS has not been initialized + OMSUndefined, + /// The current nodes identity is undefined + NodeIdentityUndefined, + /// PeerManager has not been initialized + PeerManagerUndefined, + PeerManagerError(PeerManagerError), + /// Failed to send from API + ApiSendFailed, + /// Failed to receive in API from service + ApiReceiveFailed, + /// Received an unexpected response type from the API + UnexpectedApiResponse, + MessageFormatError(MessageFormatError), + MessageSerializationError(MessageError), + NodeIdentityError(NodeIdentityError), +} diff --git a/base_layer/p2p/src/dht_service/mod.rs b/base_layer/p2p/src/dht_service/mod.rs new file mode 100644 index 0000000000..313c62fd3e --- /dev/null +++ b/base_layer/p2p/src/dht_service/mod.rs @@ -0,0 +1,31 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod dht_messages; +mod dht_service; +mod error; + +pub use self::{ + dht_messages::{DiscoverMessage, JoinMessage}, + dht_service::{DHTService, DHTServiceApi}, + error::DHTError, +}; diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs new file mode 100644 index 0000000000..d1796dc3d3 --- /dev/null +++ b/base_layer/p2p/src/initialization.rs @@ -0,0 +1,89 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::tari_message::TariMessageType; +use derive_error::Error; +use std::{net::IpAddr, sync::Arc}; +use tari_comms::{ + builder::{CommsBuilderError, CommsServices, CommsServicesError}, + connection::{net_address::ip::SocketAddress, NetAddress}, + connection_manager::PeerConnectionConfig, + control_service::ControlServiceConfig, + peer_manager::{node_identity::NodeIdentityError, NodeIdentity}, + types::{CommsPublicKey, CommsSecretKey}, + CommsBuilder, +}; +use tari_storage::{lmdb_store::LMDBBuilder, LMDBWrapper}; + +#[derive(Debug, Error)] +pub enum CommsInitializationError { + NodeIdentityError(NodeIdentityError), + CommsBuilderError(CommsBuilderError), + CommsServicesError(CommsServicesError), +} + +#[derive(Clone)] +pub struct CommsConfig { + pub control_service: ControlServiceConfig, + pub socks_proxy_address: Option, + pub host: IpAddr, + pub public_key: CommsPublicKey, + pub secret_key: CommsSecretKey, + pub public_address: NetAddress, + pub datastore_path: String, + pub peer_database_name: String, +} + +pub fn initialize_comms(config: CommsConfig) -> Result>, CommsInitializationError> { + let node_identity = NodeIdentity::new(config.secret_key, config.public_key, config.public_address) + .map_err(CommsInitializationError::NodeIdentityError)?; + + let _ = std::fs::create_dir(&config.datastore_path).unwrap_or_default(); + let datastore = LMDBBuilder::new() + .set_path(&config.datastore_path) + .set_environment_size(10) + .set_max_number_of_databases(1) + .add_database(&config.peer_database_name, lmdb_zero::db::CREATE) + .build() + .unwrap(); + let peer_database = datastore.get_handle(&config.peer_database_name).unwrap(); + let peer_database = LMDBWrapper::new(Arc::new(peer_database)); + + let builder = CommsBuilder::new() + .with_node_identity(node_identity) + .with_peer_storage(peer_database) + .configure_control_service(config.control_service) + .configure_peer_connections(PeerConnectionConfig { + socks_proxy_address: config.socks_proxy_address, + host: config.host, + ..Default::default() + }); + + let comms = builder + .build() + .map_err(CommsInitializationError::CommsBuilderError)? + .start() + .map(Arc::new) + .map_err(CommsInitializationError::CommsServicesError)?; + + Ok(comms) +} diff --git a/base_layer/p2p/src/lib.rs b/base_layer/p2p/src/lib.rs index 7c8005227e..6a4dc64c65 100644 --- a/base_layer/p2p/src/lib.rs +++ b/base_layer/p2p/src/lib.rs @@ -19,3 +19,21 @@ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Used to eliminate the need for boxing futures in many cases. +// Tracking issue: https://github.com/rust-lang/rust/issues/63063 +#![feature(type_alias_impl_trait)] + +#[macro_use] +mod macros; +mod consts; + +// TODO Put these back in after Futures Comms stack refactor +// pub mod saf_service; +// pub mod dht_service; +pub mod initialization; +pub mod peer; +pub mod ping_pong; +pub mod services; +pub mod sync_services; +pub mod tari_message; diff --git a/base_layer/blockchain/src/blockchainstate.rs b/base_layer/p2p/src/macros.rs similarity index 79% rename from base_layer/blockchain/src/blockchainstate.rs rename to base_layer/p2p/src/macros.rs index 3f33a00dfe..03c4ceadc2 100644 --- a/base_layer/blockchain/src/blockchainstate.rs +++ b/base_layer/p2p/src/macros.rs @@ -1,4 +1,4 @@ -// Copyright 2018 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -20,13 +20,21 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// This file is used to store the current blockchain state - -/// The BlockchainState struct keeps record of the current UTXO, total kernels and headers. -pub struct BlockchainState {} +macro_rules! acquire_lock { + ($e:expr, $m:ident) => { + match $e.$m() { + Ok(lock) => lock, + Err(poisoned) => poisoned.into_inner(), + } + }; + ($e:expr) => { + acquire_lock!($e, lock) + }; +} -impl BlockchainState { - pub fn new() -> BlockchainState { - BlockchainState {} - } +#[cfg(test)] +macro_rules! acquire_read_lock { + ($e:expr) => { + acquire_lock!($e, read) + }; } diff --git a/base_layer/p2p/src/peer.rs b/base_layer/p2p/src/peer.rs new file mode 100644 index 0000000000..a69d47315e --- /dev/null +++ b/base_layer/p2p/src/peer.rs @@ -0,0 +1,22 @@ +use tari_comms::peer_manager::peer::Peer; + +#[derive(Debug)] +pub enum PeerType { + BaseNode, + ValidatorNode, + Wallet, + TokenWallet, +} + +#[derive(Debug)] +pub struct PeerWithType { + pub peer: Peer, + pub peer_type: PeerType, +} + +impl PeerWithType { + /// Constructs a new peer with peer type + pub fn new(peer: Peer, peer_type: PeerType) -> PeerWithType { + PeerWithType { peer, peer_type } + } +} diff --git a/base_layer/p2p/src/ping_pong.rs b/base_layer/p2p/src/ping_pong.rs new file mode 100644 index 0000000000..3b2e9913b3 --- /dev/null +++ b/base_layer/p2p/src/ping_pong.rs @@ -0,0 +1,321 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + sync_services::{ + Service, + ServiceApiWrapper, + ServiceContext, + ServiceControlMessage, + ServiceError, + DEFAULT_API_TIMEOUT_MS, + }, + tari_message::{NetMessage, TariMessageType}, +}; +use crossbeam_channel as channel; +use derive_error::Error; +use log::*; +use serde::{Deserialize, Serialize}; +use std::{ + convert::TryInto, + fmt, + sync::{Arc, Mutex}, + time::Duration, +}; +use tari_comms::{ + domain_subscriber::{MessageInfo, SyncDomainSubscription}, + message::{Message, MessageError, MessageFlags}, + outbound_message_service::{outbound_message_service::OutboundMessageService, BroadcastStrategy, OutboundError}, + types::CommsPublicKey, +}; +use tari_utilities::{hex::Hex, message_format::MessageFormatError}; + +const LOG_TARGET: &str = "base_layer::p2p::ping_pong"; + +#[derive(Debug, Error)] +pub enum PingPongError { + OutboundError(OutboundError), + /// OMS has not been initialized + OMSNotInitialized, + SerializationFailed(MessageFormatError), + MessageError(MessageError), + /// Failed to send from API + ApiSendFailed, + /// Failed to receive in API from service + ApiReceiveFailed, + /// Received an unexpected response type from the API + UnexpectedApiResponse, +} + +/// The PingPong message +#[derive(Serialize, Deserialize)] +pub enum PingPong { + Ping, + Pong, +} + +impl TryInto for PingPong { + type Error = MessageError; + + fn try_into(self) -> Result { + Ok((TariMessageType::new(NetMessage::PingPong), self).try_into()?) + } +} + +pub struct PingPongService { + // Needed because the public ping method needs OMS + oms: Option>, + ping_count: usize, + pong_count: usize, + api: ServiceApiWrapper, +} + +impl PingPongService { + /// Create a new ping pong service + pub fn new() -> Self { + Self { + oms: None, + ping_count: 0, + pong_count: 0, + api: Self::setup_api(), + } + } + + /// Return this services API + pub fn get_api(&self) -> Arc { + self.api.get_api() + } + + fn setup_api() -> ServiceApiWrapper { + let (api_sender, service_receiver) = channel::bounded(0); + let (service_sender, api_receiver) = channel::bounded(0); + + let api = Arc::new(PingPongServiceApi::new(api_sender, api_receiver)); + ServiceApiWrapper::new(service_receiver, service_sender, api) + } + + fn send_msg(&self, broadcast_strategy: BroadcastStrategy, msg: PingPong) -> Result<(), PingPongError> { + let oms = self.oms.as_ref().ok_or(PingPongError::OMSNotInitialized)?; + oms.send_message(broadcast_strategy, MessageFlags::empty(), msg) + .map_err(PingPongError::OutboundError) + } + + fn receive_ping(&mut self, info: MessageInfo, message: PingPong) -> Result<(), PingPongError> { + match message { + PingPong::Ping => { + debug!( + target: LOG_TARGET, + "Received ping from {}", + info.peer_source.public_key.to_hex(), + ); + + self.ping_count += 1; + + // Reply with Pong + self.send_msg(BroadcastStrategy::DirectPublicKey(info.origin_source), PingPong::Pong)?; + }, + PingPong::Pong => { + debug!( + target: LOG_TARGET, + "Received pong from {}", + info.peer_source.public_key.to_hex() + ); + + self.pong_count += 1; + }, + } + + Ok(()) + } + + fn ping(&self, pub_key: CommsPublicKey) -> Result<(), PingPongError> { + self.send_msg(BroadcastStrategy::DirectPublicKey(pub_key), PingPong::Ping) + } + + fn handle_api_message(&self, msg: PingPongApiRequest) -> Result<(), ServiceError> { + trace!(target: LOG_TARGET, "[{}] Received API message", self.get_name()); + let resp = match msg { + PingPongApiRequest::Ping(pk) => self.ping(pk).map(|_| PingPongApiResponse::PingSent), + PingPongApiRequest::GetPingCount => Ok(PingPongApiResponse::Count(self.ping_count)), + PingPongApiRequest::GetPongCount => Ok(PingPongApiResponse::Count(self.pong_count)), + }; + + trace!(target: LOG_TARGET, "[{}] Replying to API", self.get_name()); + self.api + .send_reply(resp) + .map_err(ServiceError::internal_service_error()) + } +} + +impl Service for PingPongService { + fn get_name(&self) -> String { + "ping-pong".to_string() + } + + fn get_message_types(&self) -> Vec { + vec![NetMessage::PingPong.into()] + } + + fn execute(&mut self, context: ServiceContext) -> Result<(), ServiceError> { + let mut subscription = SyncDomainSubscription::new( + context + .inbound_message_subscription_factory() + .get_subscription_fused(NetMessage::PingPong.into()), + ); + + self.oms = Some(context.outbound_message_service()); + + loop { + if let Some(msg) = context.get_control_message(Duration::from_millis(5)) { + match msg { + ServiceControlMessage::Shutdown => break, + } + } + + for m in subscription.receive_messages()?.drain(..) { + match self.receive_ping(m.0, m.1) { + Ok(_) => {}, + Err(err) => { + error!(target: LOG_TARGET, "PingPong service had error: {}", err); + }, + } + } + + if let Some(msg) = self + .api + .recv_timeout(Duration::from_millis(5)) + .map_err(ServiceError::internal_service_error())? + { + self.handle_api_message(msg)?; + } + } + + Ok(()) + } +} + +/// API Request enum +#[derive(Debug)] +pub enum PingPongApiRequest { + /// Send a ping to the given public key + Ping(CommsPublicKey), + /// Retrieve the total number of pings received + GetPingCount, + /// Retrieve the total number of pongs received + GetPongCount, +} + +/// API Response enum +#[derive(Debug)] +pub enum PingPongApiResponse { + PingSent, + Count(usize), +} + +impl fmt::Display for PingPongApiResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PingPongApiResponse::PingSent => write!(f, "PingSent"), + PingPongApiResponse::Count(n) => write!(f, "Count({})", n), + } + } +} + +/// Result for all API requests +pub type PingPongApiResult = Result; + +/// The PingPong service public api +pub struct PingPongServiceApi { + sender: channel::Sender, + receiver: channel::Receiver, + mutex: Mutex<()>, + timeout: Duration, +} + +impl PingPongServiceApi { + fn new(sender: channel::Sender, receiver: channel::Receiver) -> Self { + Self { + sender, + receiver, + mutex: Mutex::new(()), + timeout: Duration::from_millis(DEFAULT_API_TIMEOUT_MS), + } + } + + /// Send a ping message to the given peer + pub fn ping(&self, public_key: CommsPublicKey) -> Result<(), PingPongError> { + self.send_recv(PingPongApiRequest::Ping(public_key)) + .and_then(|resp| match resp { + PingPongApiResponse::PingSent => Ok(()), + _ => Err(PingPongError::UnexpectedApiResponse), + }) + } + + /// Fetch the ping count from the service + pub fn ping_count(&self) -> Result { + self.send_recv(PingPongApiRequest::GetPingCount) + .and_then(|resp| match resp { + PingPongApiResponse::Count(n) => Ok(n), + _ => Err(PingPongError::UnexpectedApiResponse), + }) + } + + /// Fetch the pong count from the service + pub fn pong_count(&self) -> Result { + self.send_recv(PingPongApiRequest::GetPongCount) + .and_then(|resp| match resp { + PingPongApiResponse::Count(n) => Ok(n), + _ => Err(PingPongError::UnexpectedApiResponse), + }) + } + + fn send_recv(&self, msg: PingPongApiRequest) -> PingPongApiResult { + self.lock(|| -> PingPongApiResult { + self.sender.send(msg).map_err(|_| PingPongError::ApiSendFailed)?; + self.receiver + .recv_timeout(self.timeout) + .map_err(|_| PingPongError::ApiReceiveFailed)? + }) + } + + fn lock(&self, func: F) -> T + where F: FnOnce() -> T { + let lock = acquire_lock!(self.mutex); + let res = func(); + drop(lock); + res + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn new() { + let service = PingPongService::new(); + assert_eq!(service.get_name(), "ping-pong"); + assert_eq!(service.get_message_types(), vec![NetMessage::PingPong.into()]); + assert_eq!(service.ping_count, 0); + assert_eq!(service.pong_count, 0); + } +} diff --git a/base_layer/p2p/src/saf_service/error.rs b/base_layer/p2p/src/saf_service/error.rs new file mode 100644 index 0000000000..090283f782 --- /dev/null +++ b/base_layer/p2p/src/saf_service/error.rs @@ -0,0 +1,55 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; +use tari_comms::{ + connection::ConnectionError, + domain_connector::ConnectorError, + message::MessageError, + outbound_message_service::OutboundError, + peer_manager::PeerManagerError, +}; + +#[derive(Debug, Error)] +pub enum SAFError { + OutboundError(OutboundError), + ConnectorError(ConnectorError), + /// OMS has not been initialized + OMSUndefined, + /// The current nodes identity is undefined + NodeIdentityUndefined, + /// PeerManager has not been initialized + PeerManagerUndefined, + PeerManagerError(PeerManagerError), + /// ZMQContext has not been initialized + ZMQContextUndefined, + /// Message Sink Address for InboundMessageService has not been initialized + IMSMessageSinkAddressUndefined, + /// Failed to send from API + ApiSendFailed, + /// Failed to receive in API from service + ApiReceiveFailed, + /// Received an unexpected response type from the API + UnexpectedApiResponse, + MessageError(MessageError), + ConnectionError(ConnectionError), +} diff --git a/base_layer/p2p/src/saf_service/mod.rs b/base_layer/p2p/src/saf_service/mod.rs new file mode 100644 index 0000000000..07d806b644 --- /dev/null +++ b/base_layer/p2p/src/saf_service/mod.rs @@ -0,0 +1,31 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod saf_messages; +mod saf_service; + +pub use self::{ + error::SAFError, + saf_messages::{RetrieveMsgsMessage, StoredMsgsMessage}, + saf_service::{SAFService, SAFServiceApi}, +}; diff --git a/base_layer/p2p/src/saf_service/saf_messages.rs b/base_layer/p2p/src/saf_service/saf_messages.rs new file mode 100644 index 0000000000..b9b233bfea --- /dev/null +++ b/base_layer/p2p/src/saf_service/saf_messages.rs @@ -0,0 +1,57 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::tari_message::{NetMessage, TariMessageType}; +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use tari_comms::message::{Message, MessageEnvelope, MessageError}; + +/// The RetrieveMsgsMessage is used for requesting the set of stored messages from neighbouring peer nodes. If a +/// start_time is provided then only messages after the specified time will be sent, otherwise all applicable messages +/// will be sent. +#[derive(Serialize, Deserialize)] +pub struct RetrieveMsgsMessage { + pub start_time: Option>, +} + +impl TryInto for RetrieveMsgsMessage { + type Error = MessageError; + + fn try_into(self) -> Result { + Ok((TariMessageType::new(NetMessage::RetrieveMessages), self).try_into()?) + } +} + +/// The StoredMsgsMessage contains the set of applicable messages retrieved from a neighbouring peer node. +#[derive(Serialize, Deserialize)] +pub struct StoredMsgsMessage { + pub message_envelopes: Vec, +} + +impl TryInto for StoredMsgsMessage { + type Error = MessageError; + + fn try_into(self) -> Result { + Ok((TariMessageType::new(NetMessage::StoredMessages), self).try_into()?) + } +} diff --git a/base_layer/p2p/src/saf_service/saf_service.rs b/base_layer/p2p/src/saf_service/saf_service.rs new file mode 100644 index 0000000000..d981a5481d --- /dev/null +++ b/base_layer/p2p/src/saf_service/saf_service.rs @@ -0,0 +1,437 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + consts::{ + DHT_BROADCAST_NODE_COUNT, + SAF_HIGH_PRIORITY_MSG_STORAGE_TTL, + SAF_LOW_PRIORITY_MSG_STORAGE_TTL, + SAF_MSG_CACHE_STORAGE_CAPACITY, + }, + saf_service::{RetrieveMsgsMessage, SAFError, StoredMsgsMessage}, + sync_services::{ + Service, + ServiceApiWrapper, + ServiceContext, + ServiceControlMessage, + ServiceError, + DEFAULT_API_TIMEOUT_MS, + }, + tari_message::{NetMessage, TariMessageType}, +}; +use chrono::prelude::*; +use crossbeam_channel as channel; +use log::*; +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; +use tari_comms::{ + connection::{Connection, Direction, InprocAddress, SocketEstablishment, ZmqContext}, + domain_connector::MessageInfo, + message::{Frame, MessageData, MessageEnvelope, MessageEnvelopeHeader, MessageFlags, NodeDestination}, + outbound_message_service::{outbound_message_service::OutboundMessageService, BroadcastStrategy, ClosestRequest}, + peer_manager::{NodeIdentity, PeerManager}, + DomainConnector, +}; +use ttl_cache::TtlCache; + +const LOG_TARGET: &str = "base_layer::p2p::saf"; + +/// Storage for a single message envelope, including the date and time when the element was stored +pub struct StoredMessage { + store_time: DateTime, + message_envelope: MessageEnvelope, + message_envelope_header: MessageEnvelopeHeader, +} + +impl StoredMessage { + /// Create a new StorageMessage from a MessageEnvelope + pub fn from(message_envelope: MessageEnvelope, message_envelope_header: MessageEnvelopeHeader) -> Self { + Self { + store_time: Utc::now(), + message_envelope, + message_envelope_header, + } + } +} + +/// The Store-and-forward Service manages the storage of forwarded message and provides an api for neighbouring peers to +/// retrieve the stored messages. +pub struct SAFService { + node_identity: Option>, + oms: Option>, + peer_manager: Option>, + zmq_context: Option, + ims_message_sink_address: Option, + api: ServiceApiWrapper, + msg_storage: TtlCache, +} + +impl SAFService { + /// Create a new Store-and-forward service. + pub fn new() -> Self { + Self { + node_identity: None, + oms: None, + peer_manager: None, + zmq_context: None, + ims_message_sink_address: None, + api: Self::setup_api(), + msg_storage: TtlCache::new(SAF_MSG_CACHE_STORAGE_CAPACITY), + } + } + + /// Return this services API. + pub fn get_api(&self) -> Arc { + self.api.get_api() + } + + fn setup_api() -> ServiceApiWrapper { + let (api_sender, service_receiver) = channel::bounded(0); + let (service_sender, api_receiver) = channel::bounded(0); + + let api = Arc::new(SAFServiceApi::new(api_sender, api_receiver)); + ServiceApiWrapper::new(service_receiver, service_sender, api) + } + + /// Send a message retrieval request to all neighbouring peers that are in the same network region. + fn send_retrieval_request(&self, start_time: Option>) -> Result<(), SAFError> { + let oms = self.oms.as_ref().ok_or(SAFError::OMSUndefined)?; + let node_identity = self.node_identity.as_ref().ok_or(SAFError::NodeIdentityUndefined)?; + + oms.send_message( + BroadcastStrategy::Closest(ClosestRequest { + n: DHT_BROADCAST_NODE_COUNT, + node_id: node_identity.identity.node_id.clone(), + excluded_peers: Vec::new(), + }), + MessageFlags::ENCRYPTED, + RetrieveMsgsMessage { start_time }, + )?; + trace!(target: LOG_TARGET, "Message retrieval request sent"); + + Ok(()) + } + + /// Process an incoming message retrieval request. + fn receive_retrieval_request(&mut self, connector: &DomainConnector<'static>) -> Result<(), SAFError> { + let oms = self.oms.as_ref().ok_or(SAFError::OMSUndefined)?; + let peer_manager = self.peer_manager.as_ref().ok_or(SAFError::PeerManagerUndefined)?; + let node_identity = self.node_identity.as_ref().ok_or(SAFError::NodeIdentityUndefined)?; + + let incoming_msg: Option<(MessageInfo, RetrieveMsgsMessage)> = connector + .receive_timeout(Duration::from_millis(1)) + .map_err(SAFError::ConnectorError)?; + if let Some((info, retrieval_request_msg)) = incoming_msg { + if peer_manager.in_network_region( + &info.peer_source.node_id, + &node_identity.identity.node_id, + DHT_BROADCAST_NODE_COUNT, + )? { + // Compile a set of stored messages for the requesting peer + // TODO: compiling the bundle of messages is slow, especially when there are many stored messages, a + // better approach should be used + let mut stored_msgs_response = StoredMsgsMessage { + message_envelopes: Vec::new(), + }; + for (_, stored_message) in self.msg_storage.iter() { + if retrieval_request_msg + .start_time + .map(|start_time| start_time <= stored_message.store_time) + .unwrap_or(true) + { + match stored_message.message_envelope_header.dest.clone() { + NodeDestination::Unknown => { + stored_msgs_response + .message_envelopes + .push(stored_message.message_envelope.clone()); + }, + NodeDestination::PublicKey(dest_public_key) => { + if dest_public_key == info.peer_source.public_key { + stored_msgs_response + .message_envelopes + .push(stored_message.message_envelope.clone()); + } + }, + NodeDestination::NodeId(dest_node_id) => { + if dest_node_id == info.peer_source.node_id { + stored_msgs_response + .message_envelopes + .push(stored_message.message_envelope.clone()); + } + }, + }; + } + } + + oms.send_message( + BroadcastStrategy::DirectPublicKey(info.peer_source.public_key), + MessageFlags::ENCRYPTED, + stored_msgs_response, + )?; + trace!(target: LOG_TARGET, "Responded to received message retrieval request"); + } + } + + Ok(()) + } + + /// Process an incoming set of retrieved messages. + fn receive_stored_messages(&mut self, connector: &DomainConnector<'static>) -> Result<(), SAFError> { + let ims_message_sink_address = self + .ims_message_sink_address + .as_ref() + .ok_or(SAFError::IMSMessageSinkAddressUndefined)?; + let zmq_context = self.zmq_context.as_ref().ok_or(SAFError::ZMQContextUndefined)?; + + let incoming_msg: Option<(MessageInfo, StoredMsgsMessage)> = connector + .receive_timeout(Duration::from_millis(1)) + .map_err(SAFError::ConnectorError)?; + if let Some((info, stored_msgs)) = incoming_msg { + // Send each received MessageEnvelope to the InboundMessageService + let ims_connection = Connection::new(&zmq_context, Direction::Outbound) + .set_socket_establishment(SocketEstablishment::Connect) + .establish(&ims_message_sink_address)?; + for message_envelope in stored_msgs.message_envelopes { + let message_data = MessageData::new(info.peer_source.node_id.clone(), false, message_envelope); + let message_data_frame_set = message_data.into_frame_set(); + ims_connection.send(message_data_frame_set.clone())?; + } + trace!(target: LOG_TARGET, "Received stored messages from neighbouring peer"); + } + + Ok(()) + } + + /// Store messages of known neighbouring peers, this network region and messages with undefined destinations. + /// Undefined destinations have a lower priority TTL. + fn store_message(&mut self, message_envelope: MessageEnvelope) -> Result<(), SAFError> { + let peer_manager = self.peer_manager.as_ref().ok_or(SAFError::PeerManagerUndefined)?; + let node_identity = self.node_identity.as_ref().ok_or(SAFError::NodeIdentityUndefined)?; + + let message_envelope_header = message_envelope.deserialize_header()?; + match message_envelope_header.dest.clone() { + NodeDestination::Unknown => { + self.msg_storage.insert( + message_envelope.body_frame().clone(), + StoredMessage::from(message_envelope, message_envelope_header), + SAF_LOW_PRIORITY_MSG_STORAGE_TTL, + ); + }, + NodeDestination::PublicKey(dest_public_key) => { + if peer_manager.exists(&dest_public_key)? { + self.msg_storage.insert( + message_envelope.body_frame().clone(), + StoredMessage::from(message_envelope, message_envelope_header), + SAF_HIGH_PRIORITY_MSG_STORAGE_TTL, + ); + } + }, + NodeDestination::NodeId(dest_node_id) => { + if (peer_manager.exists_node_id(&dest_node_id)?) | + (peer_manager.in_network_region( + &dest_node_id, + &node_identity.identity.node_id, + DHT_BROADCAST_NODE_COUNT, + )?) + { + self.msg_storage.insert( + message_envelope.body_frame().clone(), + StoredMessage::from(message_envelope, message_envelope_header), + SAF_HIGH_PRIORITY_MSG_STORAGE_TTL, + ); + } + }, + }; + + Ok(()) + } + + /// This handler is called when the Service executor loops receives an API request + fn handle_api_message(&mut self, msg: SAFApiRequest) -> Result<(), ServiceError> { + trace!( + target: LOG_TARGET, + "[{}] Received API message: {:?}", + self.get_name(), + msg + ); + let resp = match msg { + SAFApiRequest::SendRetrievalRequest(start_time) => self + .send_retrieval_request(start_time) + .map(|_| SAFApiResponse::RetrievalRequestSent), + SAFApiRequest::StoreMessage(message_envelope) => self + .store_message(message_envelope) + .map(|_| SAFApiResponse::MessageStored), + }; + + trace!(target: LOG_TARGET, "[{}] Replying to API: {:?}", self.get_name(), resp); + self.api + .send_reply(resp) + .map_err(ServiceError::internal_service_error()) + } +} + +/// The Domain Service trait implementation for the Store-and-forward Service +impl Service for SAFService { + fn get_name(&self) -> String { + "store-and-forward".to_string() + } + + fn get_message_types(&self) -> Vec { + vec![NetMessage::RetrieveMessages.into(), NetMessage::StoredMessages.into()] + } + + fn execute(&mut self, context: ServiceContext) -> Result<(), ServiceError> { + let connector_retrieve_messages = + context + .create_connector(&NetMessage::RetrieveMessages.into()) + .map_err(|err| { + ServiceError::ServiceInitializationFailed(format!( + "Failed to create connector for service: {}", + err + )) + })?; + + let connector_stored_messages = + context + .create_connector(&NetMessage::StoredMessages.into()) + .map_err(|err| { + ServiceError::ServiceInitializationFailed(format!( + "Failed to create connector for service: {}", + err + )) + })?; + + self.node_identity = Some(context.node_identity()); + self.oms = Some(context.outbound_message_service()); + self.peer_manager = Some(context.peer_manager()); + self.zmq_context = Some(context.zmq_context().clone()); + self.ims_message_sink_address = Some(context.ims_message_sink_address().clone()); + + debug!(target: LOG_TARGET, "Starting Store-and-forward Service executor"); + loop { + if let Some(msg) = context.get_control_message(Duration::from_millis(5)) { + match msg { + ServiceControlMessage::Shutdown => break, + } + } + + match self.receive_retrieval_request(&connector_retrieve_messages) { + Ok(_) => {}, + Err(err) => { + error!(target: LOG_TARGET, "Store-and-forward service had error: {:?}", err); + }, + } + + match self.receive_stored_messages(&connector_stored_messages) { + Ok(_) => {}, + Err(err) => { + error!(target: LOG_TARGET, "Store-and-forward service had error: {:?}", err); + }, + } + + if let Some(msg) = self + .api + .recv_timeout(Duration::from_millis(5)) + .map_err(ServiceError::internal_service_error())? + { + self.handle_api_message(msg)?; + } + } + + Ok(()) + } +} + +/// API Request enum +#[derive(Debug)] +pub enum SAFApiRequest { + /// Send a request to retrieve stored messages from neighbouring peers + SendRetrievalRequest(Option>), + /// Store message of known neighbouring peers and forwarded messages + StoreMessage(MessageEnvelope), +} + +/// API Response enum +#[derive(Debug)] +pub enum SAFApiResponse { + RetrievalRequestSent, + MessageStored, +} + +/// Result for all API requests +pub type SAFApiResult = Result; + +/// The Store-and-forward service public API that other services and application will use to interact with this service. +/// The requests and responses are transmitted via channels into the Service Executor thread where this service is +/// running +pub struct SAFServiceApi { + sender: channel::Sender, + receiver: channel::Receiver, + mutex: Mutex<()>, + timeout: Duration, +} + +impl SAFServiceApi { + fn new(sender: channel::Sender, receiver: channel::Receiver) -> Self { + Self { + sender, + receiver, + mutex: Mutex::new(()), + timeout: Duration::from_millis(DEFAULT_API_TIMEOUT_MS), + } + } + + pub fn retrieve(&self, start_time: Option>) -> Result<(), SAFError> { + self.send_recv(SAFApiRequest::SendRetrievalRequest(start_time)) + .and_then(|resp| match resp { + SAFApiResponse::RetrievalRequestSent => Ok(()), + _ => Err(SAFError::UnexpectedApiResponse), + }) + } + + pub fn store(&self, message_envelope: MessageEnvelope) -> Result<(), SAFError> { + self.send_recv(SAFApiRequest::StoreMessage(message_envelope)) + .and_then(|resp| match resp { + SAFApiResponse::MessageStored => Ok(()), + _ => Err(SAFError::UnexpectedApiResponse), + }) + } + + fn send_recv(&self, msg: SAFApiRequest) -> SAFApiResult { + self.lock(|| -> SAFApiResult { + self.sender.send(msg).map_err(|_| SAFError::ApiSendFailed)?; + self.receiver + .recv_timeout(self.timeout) + .map_err(|_| SAFError::ApiReceiveFailed)? + }) + } + + fn lock(&self, func: F) -> T + where F: FnOnce() -> T { + let lock = acquire_lock!(self.mutex); + let res = func(); + drop(lock); + res + } +} diff --git a/base_layer/p2p/src/services/comms_outbound/error.rs b/base_layer/p2p/src/services/comms_outbound/error.rs new file mode 100644 index 0000000000..a57b23ff6d --- /dev/null +++ b/base_layer/p2p/src/services/comms_outbound/error.rs @@ -0,0 +1,36 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; +use tari_comms::{message::MessageError, outbound_message_service::OutboundError}; +use tari_utilities::message_format::MessageFormatError; +use tokio_threadpool::BlockingError; + +#[derive(Debug, Error)] +pub enum CommsOutboundServiceError { + // Comms errors + OutboundError(OutboundError), + MessageSerializationError(MessageError), + MessageFormatError(MessageFormatError), + #[error(non_std)] + BlockingError(BlockingError), +} diff --git a/base_layer/p2p/src/services/comms_outbound/handle.rs b/base_layer/p2p/src/services/comms_outbound/handle.rs new file mode 100644 index 0000000000..f263f6cbf8 --- /dev/null +++ b/base_layer/p2p/src/services/comms_outbound/handle.rs @@ -0,0 +1,226 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + services::comms_outbound::{ + error::CommsOutboundServiceError, + messages::{CommsOutboundRequest, CommsOutboundResponse}, + }, + tari_message::TariMessageType, +}; +use futures::{ + future::{self, Either, Future}, + Poll, +}; +use tari_comms::{ + message::{Frame, Message, MessageEnvelope, MessageFlags, MessageHeader}, + outbound_message_service::BroadcastStrategy, +}; +use tari_service_framework::transport::{AwaitResponseError, Requester}; +use tari_utilities::message_format::MessageFormat; +use tokio_threadpool::{blocking, BlockingError}; +use tower_service::Service; + +type CommsOutboundRequester = Requester>; + +/// Handle for the CommsOutboundService. +#[derive(Clone)] +pub struct CommsOutboundHandle { + requester: CommsOutboundRequester, +} + +impl CommsOutboundHandle { + /// Create a new CommsOutboundHandle, which makes requests using the + /// given Requester + pub fn new(requester: CommsOutboundRequester) -> Self { + Self { requester } + } + + /// Send a comms message + pub fn send_message( + &mut self, + broadcast_strategy: BroadcastStrategy, + flags: MessageFlags, + message_type: TariMessageType, + message: T, + ) -> impl Future, Error = AwaitResponseError> + 'static + where + T: MessageFormat + 'static, + { + let mut requester = self.requester.clone(); + Self::message_body_serializer(message_type, message) + .or_else(|err| future::ok(Err(CommsOutboundServiceError::BlockingError(err)))) + .and_then(move |res| match res { + Ok(body) => Either::A(requester.call(CommsOutboundRequest::SendMsg { + broadcast_strategy, + flags, + body: Box::new(body), + })), + Err(err) => Either::B(future::ok(Err(err))), + }) + } + + /// Forward a comms message + pub fn forward_message( + mut self, + broadcast_strategy: BroadcastStrategy, + envelope: MessageEnvelope, + ) -> impl Future, Error = AwaitResponseError> + { + self.requester.call(CommsOutboundRequest::Forward { + broadcast_strategy, + message_envelope: Box::new(envelope), + }) + } + + /// Return a message body serializer future + fn message_body_serializer(message_type: TariMessageType, message: T) -> MessageBodySerializer + where T: MessageFormat { + MessageBodySerializer::new(message_type, message) + } +} + +#[must_use = "futures do nothing unless polled"] +struct MessageBodySerializer { + message: Option, + message_type: Option, +} + +impl MessageBodySerializer { + fn new(message_type: TariMessageType, message: T) -> Self { + Self { + message: Some(message), + message_type: Some(message_type), + } + } +} + +impl Future for MessageBodySerializer +where T: MessageFormat +{ + type Error = BlockingError; + type Item = Result; + + fn poll(&mut self) -> Poll { + let message_type = self.message_type.take().expect("called poll twice"); + let message = self.message.take().expect("called poll twice"); + blocking(move || { + let header = + MessageHeader::new(message_type).map_err(CommsOutboundServiceError::MessageSerializationError)?; + let msg = Message::from_message_format(header, message) + .map_err(CommsOutboundServiceError::MessageSerializationError)?; + + msg.to_binary().map_err(CommsOutboundServiceError::MessageFormatError) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use rand::rngs::OsRng; + use tari_comms::{ + message::{MessageEnvelopeHeader, NodeDestination}, + types::CommsPublicKey, + }; + use tari_crypto::keys::PublicKey; + use tari_service_framework::transport; + use tokio::runtime::Runtime; + use tower_util::service_fn; + + #[test] + fn message_body_serializer() { + // Require tokio threadpool for blocking call + let mut rt = Runtime::new().unwrap(); + + let message_type = TariMessageType::new(0); + let message = "FOO".to_string(); + let fut = CommsOutboundHandle::message_body_serializer(message_type.clone(), message); + + let body = rt.block_on(fut).unwrap().unwrap(); + + let msg = Message::from_binary(&body).unwrap(); + let header: MessageHeader = msg.deserialize_header().unwrap(); + let body_msg: String = msg.deserialize_message().unwrap(); + assert_eq!(header.message_type, message_type); + assert_eq!(body_msg, "FOO"); + } + + #[test] + fn send_message() { + let mut rt = Runtime::new().unwrap(); + + let (req, res) = transport::channel(service_fn(|req| { + match req { + CommsOutboundRequest::SendMsg { .. } => {}, + _ => panic!("Unexpected request"), + } + future::ok::<_, ()>(Ok(())) + })); + + rt.spawn(res); + + let mut handle = CommsOutboundHandle::new(req); + let fut = handle.send_message( + BroadcastStrategy::Flood, + MessageFlags::empty(), + TariMessageType::new(0), + "FOO".to_string(), + ); + + rt.block_on(fut).unwrap().unwrap(); + } + + #[test] + fn forward() { + let mut rt = Runtime::new().unwrap(); + + let (req, res) = transport::channel(service_fn(|req| { + match req { + CommsOutboundRequest::Forward { .. } => {}, + _ => panic!("Unexpected request"), + } + future::ok::<_, ()>(Ok(())) + })); + + rt.spawn(res); + + let handle = CommsOutboundHandle::new(req); + let mut rng = OsRng::new().unwrap(); + let header = MessageEnvelopeHeader { + version: 0, + origin_source: CommsPublicKey::random_keypair(&mut rng).1, + peer_source: CommsPublicKey::random_keypair(&mut rng).1, + dest: NodeDestination::Unknown, + origin_signature: vec![], + peer_signature: vec![], + flags: MessageFlags::empty(), + }; + + let fut = handle.forward_message( + BroadcastStrategy::Flood, + MessageEnvelope::new(vec![0], header.to_binary().unwrap(), vec![]), + ); + + rt.block_on(fut).unwrap().unwrap(); + } +} diff --git a/base_layer/p2p/src/services/comms_outbound/messages.rs b/base_layer/p2p/src/services/comms_outbound/messages.rs new file mode 100644 index 0000000000..087758ae9c --- /dev/null +++ b/base_layer/p2p/src/services/comms_outbound/messages.rs @@ -0,0 +1,45 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use tari_comms::{ + message::{Frame, MessageEnvelope, MessageFlags}, + outbound_message_service::BroadcastStrategy, +}; + +/// Represents requests to the CommsOutboundService +pub enum CommsOutboundRequest { + /// Send a message using the given broadcast strategy + SendMsg { + broadcast_strategy: BroadcastStrategy, + flags: MessageFlags, + body: Box, + }, + /// Forward a message envelope + Forward { + broadcast_strategy: BroadcastStrategy, + message_envelope: Box, + }, +} + +/// Represents a response from the CommsOutboundService. Currently, there are no requests +/// which result in a value. +pub type CommsOutboundResponse = (); diff --git a/base_layer/p2p/src/services/comms_outbound/mod.rs b/base_layer/p2p/src/services/comms_outbound/mod.rs new file mode 100644 index 0000000000..bbc7bb78ad --- /dev/null +++ b/base_layer/p2p/src/services/comms_outbound/mod.rs @@ -0,0 +1,56 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod handle; +mod messages; +mod service; + +use self::service::CommsOutboundService; +use crate::services::{ServiceHandlesFuture, ServiceName}; +use std::sync::Arc; +use tari_comms::outbound_message_service::outbound_message_service::OutboundMessageService; +use tari_service_framework::{transport, ServiceInitializationError, ServiceInitializer}; + +pub use self::{error::CommsOutboundServiceError, handle::CommsOutboundHandle, messages::CommsOutboundRequest}; + +/// Initializer for CommsOutbound service +pub struct CommsOutboundServiceInitializer { + oms: Arc, +} + +impl CommsOutboundServiceInitializer { + pub fn new(oms: Arc) -> Self { + Self { oms } + } +} + +impl ServiceInitializer for CommsOutboundServiceInitializer { + fn initialize(self: Box, handles: ServiceHandlesFuture) -> Result<(), ServiceInitializationError> { + let service = CommsOutboundService::new(self.oms); + let (requester, responder) = transport::channel(service); + + tokio::spawn(responder); + handles.insert(ServiceName::CommsOutbound, CommsOutboundHandle::new(requester)); + Ok(()) + } +} diff --git a/base_layer/p2p/src/services/comms_outbound/service.rs b/base_layer/p2p/src/services/comms_outbound/service.rs new file mode 100644 index 0000000000..1600c5b864 --- /dev/null +++ b/base_layer/p2p/src/services/comms_outbound/service.rs @@ -0,0 +1,224 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::services::comms_outbound::{ + error::CommsOutboundServiceError, + messages::{CommsOutboundRequest, CommsOutboundResponse}, +}; +use futures::{ + future::{self, Either}, + Future, + Poll, +}; +use std::sync::Arc; +use tari_comms::{ + message::{Frame, MessageEnvelope, MessageFlags}, + outbound_message_service::{outbound_message_service::OutboundMessageService, BroadcastStrategy}, +}; +use tower_service::Service; + +/// Service responsible for sending messages to the comms OMS +pub struct CommsOutboundService { + oms: Arc, +} + +impl CommsOutboundService { + pub fn new(oms: Arc) -> Self { + Self { oms } + } + + fn send_msg( + &self, + broadcast_strategy: BroadcastStrategy, + flags: MessageFlags, + body: Frame, + ) -> impl Future, Error = CommsOutboundServiceError> + { + // TODO(sdbondi): Change required when oms is async + future::ok( + self.oms + .send_raw(broadcast_strategy, flags, body) + .map_err(CommsOutboundServiceError::OutboundError), + ) + } + + fn forward_message( + &self, + broadcast_strategy: BroadcastStrategy, + envelope: MessageEnvelope, + ) -> impl Future, Error = CommsOutboundServiceError> + { + // TODO(sdbondi): Change required when oms is async + future::ok( + self.oms + .forward_message(broadcast_strategy, envelope) + .map_err(CommsOutboundServiceError::OutboundError), + ) + } +} + +impl Service for CommsOutboundService { + type Error = CommsOutboundServiceError; + type Future = impl Future; + type Response = Result; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(().into()) + } + + fn call(&mut self, req: CommsOutboundRequest) -> Self::Future { + match req { + // Send a ping synchronously for now until comms is async + CommsOutboundRequest::SendMsg { + broadcast_strategy, + flags, + body, + } => Either::A(self.send_msg(broadcast_strategy, flags, *body)), + CommsOutboundRequest::Forward { + broadcast_strategy, + message_envelope, + } => Either::B(self.forward_message(broadcast_strategy, *message_envelope)), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crossbeam_channel as channel; + use futures::Async; + use rand::{distributions::Alphanumeric, rngs::OsRng, Rng}; + use std::iter; + use tari_comms::{ + connection::NetAddress, + message::{MessageEnvelopeHeader, NodeDestination}, + outbound_message_service::OutboundMessage, + peer_manager::{NodeId, NodeIdentity, Peer, PeerFlags, PeerManager}, + types::CommsPublicKey, + }; + use tari_crypto::keys::PublicKey; + use tari_storage::{lmdb_store::LMDBBuilder, LMDBWrapper}; + use tari_utilities::message_format::MessageFormat; + use tempdir::TempDir; + + pub fn random_string(len: usize) -> String { + let mut rng = OsRng::new().unwrap(); + iter::repeat(()).map(|_| rng.sample(Alphanumeric)).take(len).collect() + } + + // TODO: Thankfully, this won't be needed in 'Future' :P - Remove this once the OMS is a Sink. + fn setup_oms() -> (Arc, channel::Receiver) { + let tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let mut rng = OsRng::new().unwrap(); + let node_identity = NodeIdentity::random(&mut rng, "127.0.0.1:9000".parse().unwrap()) + .map(Arc::new) + .unwrap(); + + let (_, pk) = CommsPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let net_addresses = "127.0.0.1:55445".parse::().unwrap().into(); + let dest_peer = Peer::new(pk, node_id, net_addresses, PeerFlags::default()); + let database_name = random_string(8); + let datastore = LMDBBuilder::new() + .set_path(tmpdir.path().to_str().unwrap()) + .set_environment_size(10) + .set_max_number_of_databases(1) + .add_database(&database_name, lmdb_zero::db::CREATE) + .build() + .unwrap(); + + let peer_database = datastore.get_handle(&database_name).unwrap(); + let peer_database = LMDBWrapper::new(Arc::new(peer_database)); + + // Add a peer so that something will be sent + let peer_manager = PeerManager::new(peer_database).map(Arc::new).unwrap(); + peer_manager.add_peer(dest_peer.clone()).unwrap(); + + let (message_sender, message_receiver) = channel::unbounded(); + ( + OutboundMessageService::new(node_identity.clone(), message_sender, peer_manager) + .map(Arc::new) + .unwrap(), + message_receiver, + ) + } + + #[test] + fn poll_ready() { + let (oms, _) = setup_oms(); + let mut service = CommsOutboundService::new(oms); + + // Always ready + assert!(service.poll_ready().unwrap().is_ready()); + } + + #[test] + fn call_send_message() { + let (oms, oms_rx) = setup_oms(); + let mut service = CommsOutboundService::new(oms); + + let mut fut = service.call(CommsOutboundRequest::SendMsg { + broadcast_strategy: BroadcastStrategy::Flood, + flags: MessageFlags::empty(), + body: Box::new(Vec::new()), + }); + + match fut.poll().unwrap() { + Async::Ready(Ok(_)) => {}, + Async::Ready(Err(err)) => panic!("unexpected failed result for send_message: {:?}", err), + _ => panic!("future is not ready"), + } + + // We only care that OMS got called (i.e the Receiver received something) + assert!(!oms_rx.is_empty()); + } + + #[test] + fn call_forward() { + let (oms, oms_rx) = setup_oms(); + let mut service = CommsOutboundService::new(oms); + let mut rng = OsRng::new().unwrap(); + let header = MessageEnvelopeHeader { + version: 0, + origin_source: CommsPublicKey::random_keypair(&mut rng).1, + peer_source: CommsPublicKey::random_keypair(&mut rng).1, + dest: NodeDestination::Unknown, + origin_signature: vec![], + peer_signature: vec![], + flags: MessageFlags::empty(), + }; + + let mut fut = service.call(CommsOutboundRequest::Forward { + broadcast_strategy: BroadcastStrategy::Flood, + message_envelope: Box::new(MessageEnvelope::new(vec![0], header.to_binary().unwrap(), vec![])), + }); + + match fut.poll().unwrap() { + Async::Ready(Ok(_)) => {}, + Async::Ready(Err(err)) => panic!("unexpected failed result for forward: {:?}", err), + _ => panic!("future is not ready"), + } + + // We only care that OMS got called (i.e the Receiver received something) + assert!(!oms_rx.is_empty()); + } +} diff --git a/base_layer/p2p/src/services/domain_deserializer.rs b/base_layer/p2p/src/services/domain_deserializer.rs new file mode 100644 index 0000000000..11f9046445 --- /dev/null +++ b/base_layer/p2p/src/services/domain_deserializer.rs @@ -0,0 +1,111 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use futures::{Future, Poll}; +use serde::export::PhantomData; +use tari_comms::{ + domain_subscriber::MessageInfo, + message::{InboundMessage, MessageError}, +}; +use tari_utilities::message_format::MessageFormat; +use tokio_threadpool::{blocking, BlockingError}; + +/// Future which asynchonously attempts to deserialize InboundMessage into +/// a `(MessageInfo, T)` tuple where T is [MessageFormat]. +pub struct DomainMessageDeserializer { + message: Option, + _t: PhantomData, +} + +impl DomainMessageDeserializer { + /// Create a new DomainMessageDeserializer from the given InboundMessage + pub fn new(message: InboundMessage) -> Self { + Self { + message: Some(message), + _t: PhantomData, + } + } +} + +impl Future for DomainMessageDeserializer { + type Error = BlockingError; + type Item = Result<(MessageInfo, T), MessageError>; + + fn poll(&mut self) -> Poll { + let msg = self.message.take().expect("poll called twice on Deserializer"); + blocking(|| { + let deserialized: T = msg.message.deserialize_message()?; + let info = MessageInfo { + peer_source: msg.peer_source, + origin_source: msg.origin_source, + }; + Ok((info, deserialized)) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use rand::rngs::OsRng; + use tari_comms::{ + message::{Message, MessageHeader}, + peer_manager::{NodeId, PeerNodeIdentity}, + types::CommsPublicKey, + }; + use tari_crypto::keys::PublicKey; + use tokio::runtime::Runtime; + + fn create_domain_message(message_type: u8, inner_msg: T) -> InboundMessage { + let mut rng = OsRng::new().unwrap(); + let (_, pk) = CommsPublicKey::random_keypair(&mut rng); + let peer_source = PeerNodeIdentity::new(NodeId::from_key(&pk).unwrap(), pk.clone()); + let header = MessageHeader::new(message_type).unwrap(); + let msg = Message::from_message_format(header, inner_msg).unwrap(); + InboundMessage::new(peer_source, pk, msg) + } + + #[test] + fn deserialize_success() { + let mut rt = Runtime::new().unwrap(); + let domain_msg = create_domain_message(1, "wubalubadubdub".to_string()); + let fut = DomainMessageDeserializer::::new(domain_msg.clone()); + + let (info, msg) = rt.block_on(fut).unwrap().unwrap(); + assert_eq!(msg, "wubalubadubdub"); + assert_eq!(info.peer_source, domain_msg.peer_source); + assert_eq!(info.origin_source, domain_msg.origin_source); + } + + #[test] + fn deserialize_fail() { + let mut rt = Runtime::new().unwrap(); + let domain_msg = create_domain_message(1, "wubalubadubdub".to_string()); + let fut = DomainMessageDeserializer::::new(domain_msg.clone()); + + match rt.block_on(fut).unwrap() { + Ok(_) => panic!("unexpected success when deserializing to mismatched type"), + Err(MessageError::MessageFormatError(_)) => {}, + Err(err) => panic!("unexpected error when deserializing mismatched types: {:?}", err), + } + } +} diff --git a/infrastructure/merklemountainrange/src/merklenode.rs b/base_layer/p2p/src/services/liveness/error.rs similarity index 77% rename from infrastructure/merklemountainrange/src/merklenode.rs rename to base_layer/p2p/src/services/liveness/error.rs index 4063d6e5dd..79f491f2f3 100644 --- a/infrastructure/merklemountainrange/src/merklenode.rs +++ b/base_layer/p2p/src/services/liveness/error.rs @@ -20,18 +20,17 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub type ObjectHash = Vec; +use crate::services::comms_outbound::CommsOutboundServiceError; +use derive_error::Error; +use tari_comms::message::MessageError; -/// This is the MerkleNode struct. This struct represents a merkle node, -#[derive(Debug)] -pub struct MerkleNode { - pub hash: ObjectHash, - pub pruned: bool, - // todo discuss adding height here, this will make some calculations faster, but will storage larger -} - -impl MerkleNode { - pub fn new(hash: ObjectHash) -> MerkleNode { - MerkleNode { hash, pruned: false } - } +#[derive(Debug, Error)] +pub enum LivenessError { + CommsOutboundError(CommsOutboundServiceError), + /// Failed to send a pong message + SendPongFailed, + /// Failed to send a ping message + SendPingFailed, + // Occurs when a message cannot deserialize into a PingPong message + MessageError(MessageError), } diff --git a/base_layer/p2p/src/services/liveness/handler.rs b/base_layer/p2p/src/services/liveness/handler.rs new file mode 100644 index 0000000000..77967dedec --- /dev/null +++ b/base_layer/p2p/src/services/liveness/handler.rs @@ -0,0 +1,170 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::messages::PingPong; +use crate::{ + services::{ + comms_outbound::CommsOutboundHandle, + liveness::{error::LivenessError, state::LivenessState}, + }, + tari_message::{NetMessage, TariMessageType}, +}; +use futures::{ + future::{self, Either}, + Future, +}; +use std::sync::Arc; +use tari_comms::{ + domain_subscriber::MessageInfo, + message::MessageFlags, + outbound_message_service::BroadcastStrategy, + types::CommsPublicKey, +}; + +pub struct LivenessHandler { + state: Arc, + outbound_handle: CommsOutboundHandle, +} + +impl LivenessHandler { + pub fn new(state: Arc, outbound_handle: CommsOutboundHandle) -> Self { + Self { state, outbound_handle } + } + + pub fn handle_message( + &mut self, + info: MessageInfo, + msg: PingPong, + ) -> impl Future + { + match msg { + PingPong::Ping => { + let state = self.state.clone(); + state.inc_pings_received(); + Either::A(self.send_pong(info.origin_source).and_then(move |_| { + state.inc_pongs_sent(); + future::ok(()) + })) + }, + PingPong::Pong => { + self.state.inc_pongs_received(); + Either::B(future::ok(())) + }, + } + } + + fn send_pong(&mut self, dest: CommsPublicKey) -> impl Future { + self.outbound_handle + .send_message( + BroadcastStrategy::DirectPublicKey(dest), + MessageFlags::empty(), + TariMessageType::new(NetMessage::PingPong), + PingPong::Pong, + ) + .or_else(|_| future::err(LivenessError::SendPongFailed)) + .and_then(|res| match res { + Ok(_) => future::ok(()), + Err(err) => future::err(LivenessError::CommsOutboundError(err)), + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::services::comms_outbound::CommsOutboundRequest; + use rand::rngs::OsRng; + use std::sync::mpsc; + use tari_comms::peer_manager::{NodeId, PeerNodeIdentity}; + use tari_crypto::keys::PublicKey; + use tari_service_framework::transport; + use tokio::runtime::Runtime; + use tower_util::service_fn; + + fn create_dummy_message_info() -> MessageInfo { + let mut rng = OsRng::new().unwrap(); + let (_, pk) = CommsPublicKey::random_keypair(&mut rng); + let peer_source = PeerNodeIdentity::new(NodeId::from_key(&pk).unwrap(), pk.clone()); + MessageInfo { + origin_source: peer_source.public_key.clone(), + peer_source, + } + } + + #[test] + fn handle_message_ping() { + let mut rt = Runtime::new().unwrap(); + let state = Arc::new(LivenessState::new()); + let (tx, rx) = mpsc::channel(); + + let (req, res) = transport::channel(service_fn(move |req| { + // Send this out so that we can assert some things about it + tx.send(req).unwrap(); + future::ok::<_, ()>(Ok(())) + })); + + rt.spawn(res); + + let outbound_handle = CommsOutboundHandle::new(req); + + let mut handler = LivenessHandler::new(state, outbound_handle); + + let info = create_dummy_message_info(); + let fut = handler.handle_message(info.clone(), PingPong::Ping); + + let result = rt.block_on(fut); + result.unwrap(); + + assert_eq!(handler.state.pings_received(), 1); + assert_eq!(handler.state.pongs_sent(), 1); + + match rx.try_recv().unwrap() { + CommsOutboundRequest::SendMsg { broadcast_strategy, .. } => match broadcast_strategy { + BroadcastStrategy::DirectPublicKey(pk) => assert_eq!(pk, info.origin_source), + _ => panic!("unexpected broadcast strategy used"), + }, + _ => panic!("liveness service sent unexpected message to outbound handle"), + } + } + + #[test] + fn handle_message_pong() { + let mut rt = Runtime::new().unwrap(); + let state = Arc::new(LivenessState::new()); + + let (req, res) = transport::channel(service_fn(|_| future::ok::<_, ()>(Ok(())))); + + rt.spawn(res); + + let outbound_handle = CommsOutboundHandle::new(req); + + let mut handler = LivenessHandler::new(state, outbound_handle); + + let info = create_dummy_message_info(); + let fut = handler.handle_message(info, PingPong::Pong); + + let result = rt.block_on(fut); + result.unwrap(); + + assert_eq!(handler.state.pongs_received(), 1); + } +} diff --git a/base_layer/p2p/src/services/liveness/messages.rs b/base_layer/p2p/src/services/liveness/messages.rs new file mode 100644 index 0000000000..a4332d7c9b --- /dev/null +++ b/base_layer/p2p/src/services/liveness/messages.rs @@ -0,0 +1,49 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use serde::{Deserialize, Serialize}; +use tari_comms::types::CommsPublicKey; + +/// API Request enum +#[derive(Debug)] +pub enum LivenessRequest { + /// Send a ping to the given public key + SendPing(CommsPublicKey), + /// Retrieve the total number of pings received + GetPingCount, + /// Retrieve the total number of pongs received + GetPongCount, +} + +/// API Response enum +#[derive(Debug)] +pub enum LivenessResponse { + PingSent, + Count(usize), +} + +/// The PingPong message +#[derive(Debug, Serialize, Deserialize)] +pub enum PingPong { + Ping, + Pong, +} diff --git a/base_layer/p2p/src/services/liveness/mod.rs b/base_layer/p2p/src/services/liveness/mod.rs new file mode 100644 index 0000000000..f258df5b5f --- /dev/null +++ b/base_layer/p2p/src/services/liveness/mod.rs @@ -0,0 +1,162 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! # Liveness Service +//! +//! This service is responsible for sending pings to any peer as well as maintaining +//! some very basic counters for the number of ping/pongs sent and received. +//! +//! It consists of: +//! - A service handle which makes requests to the Liveness backend. Types of requests can be found in the +//! [LivenessRequest] enum. +//! - A handler for incoming [PingPong] messages. +//! +//! In future, this service may be expanded to included periodic pings to maintain +//! latency and availability statistics for peers. +//! +//! [LivenessRequest]: ./messages/enum.LivenessRequets.html +//! [PingPong]: ./messages/enum.PingPong.html + +mod error; +mod handler; +mod messages; +mod service; +mod state; + +use self::{error::LivenessError, handler::LivenessHandler, service::LivenessService, state::LivenessState}; +use crate::{ + services::{ + comms_outbound::CommsOutboundHandle, + domain_deserializer::DomainMessageDeserializer, + ServiceHandlesFuture, + ServiceName, + }, + tari_message::{NetMessage, TariMessageType}, +}; +use futures::{future, Future, Stream}; +use log::*; +use std::{fmt::Debug, sync::Arc}; +use tari_comms::{builder::CommsServices, domain_subscriber::MessageInfo}; +use tari_service_framework::{ + transport::{self, Requester}, + ServiceInitializationError, + ServiceInitializer, +}; + +pub use self::messages::{LivenessRequest, LivenessResponse, PingPong}; +use tari_comms::inbound_message_service::InboundTopicSubscriptionFactory; + +pub type LivenessHandle = Requester>; + +const LOG_TARGET: &'static str = "base_layer::p2p::services::liveness"; + +/// Initializer for the Liveness service handle and service future. +pub struct LivenessInitializer { + inbound_message_subscription_factory: Arc>, +} + +impl LivenessInitializer { + /// Create a new LivenessInitializer from comms + pub fn new(comms: Arc>) -> Self { + Self { + inbound_message_subscription_factory: comms.inbound_message_subscription_factory(), + } + } + + /// Create a new LivenessInitializer from the inbound message subscriber + #[cfg(test)] + pub fn inbound_message_subscription_factory( + inbound_message_subscription_factory: Arc>, + ) -> Self { + Self { + inbound_message_subscription_factory, + } + } + + /// Get a stream of inbound PingPong messages + fn ping_stream(&self) -> impl Stream { + self.inbound_message_subscription_factory + .get_subscription_compat(TariMessageType::new(NetMessage::PingPong)) + .and_then(|msg| { + DomainMessageDeserializer::::new(msg).or_else(|_| { + error!(target: LOG_TARGET, "thread pool shut down"); + future::err(()) + }) + }) + .filter_map(ok_or_skip_result) + } +} + +impl ServiceInitializer for LivenessInitializer { + fn initialize(self: Box, handles: ServiceHandlesFuture) -> Result<(), ServiceInitializationError> { + let liveness_service = handles.lazy_service(move |handles| { + // All handles are ready + let state = Arc::new(LivenessState::new()); + let outbound_handle = handles + .get_handle::(ServiceName::CommsOutbound) + .expect("Liveness service requires CommsOutbound service handle"); + + // Setup and start the inbound message handler + let mut handler = LivenessHandler::new(Arc::clone(&state), outbound_handle.clone()); + let inbound_handler = self.ping_stream().for_each(move |(info, msg)| { + handler.handle_message(info, msg).or_else(|err| { + error!("Error when processing message: {:?}", err); + future::err(()) + }) + }); + + tokio::spawn(inbound_handler); + + LivenessService::new(state, outbound_handle) + }); + + let (requester, responder) = transport::channel(liveness_service); + // Register handle and spawn the responder service + handles.insert(ServiceName::Liveness, requester); + tokio::spawn(responder); + + Ok(()) + } +} + +fn ok_or_skip_result(res: Result) -> Option +where E: Debug { + match res { + Ok(t) => Some(t), + Err(err) => { + tracing::error!(target: LOG_TARGET, "{:?}", err); + None + }, + } +} + +#[cfg(test)] +mod test { + #[test] + fn ok_or_skip_result() { + let res = Result::<_, ()>::Ok(()); + assert_eq!(super::ok_or_skip_result(res).unwrap(), ()); + + let res = Result::<(), _>::Err(()); + assert!(super::ok_or_skip_result(res).is_none()); + } +} diff --git a/base_layer/p2p/src/services/liveness/service.rs b/base_layer/p2p/src/services/liveness/service.rs new file mode 100644 index 0000000000..bd9fcb7976 --- /dev/null +++ b/base_layer/p2p/src/services/liveness/service.rs @@ -0,0 +1,171 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{error::LivenessError, state::LivenessState, LivenessRequest, LivenessResponse}; +use crate::{ + services::{comms_outbound::CommsOutboundHandle, liveness::messages::PingPong}, + tari_message::{NetMessage, TariMessageType}, +}; +use futures::{ + future::{self, Either}, + Future, + Poll, +}; +use std::sync::Arc; +use tari_comms::{message::MessageFlags, outbound_message_service::BroadcastStrategy, types::CommsPublicKey}; +use tower_service::Service; + +/// Service responsible for testing Liveness for Peers. +/// +/// Very basic global ping and pong counter stats are implemented. In future, +/// peer latency and availability stats will be added. +pub struct LivenessService { + state: Arc, + oms_handle: CommsOutboundHandle, +} + +impl LivenessService { + pub fn new(state: Arc, oms_handle: CommsOutboundHandle) -> Self { + Self { state, oms_handle } + } + + fn send_ping( + &mut self, + pub_key: CommsPublicKey, + ) -> impl Future, Error = ()> + { + let state = self.state.clone(); + self.oms_handle + .send_message( + BroadcastStrategy::DirectPublicKey(pub_key), + MessageFlags::empty(), + TariMessageType::new(NetMessage::PingPong), + PingPong::Ping, + ) + .and_then(move |res| { + state.inc_pings_sent(); + future::ok( + res.map(|_| LivenessResponse::PingSent) + .map_err(LivenessError::CommsOutboundError), + ) + }) + .or_else(|_| future::ok(Err(LivenessError::SendPingFailed))) + } + + fn get_ping_count(&self) -> usize { + self.state.pings_received() + } + + fn get_pong_count(&self) -> usize { + self.state.pongs_received() + } +} + +impl Service for LivenessService { + type Error = (); + type Future = impl Future; + type Response = Result; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(().into()) + } + + fn call(&mut self, req: LivenessRequest) -> Self::Future { + match req { + LivenessRequest::SendPing(pub_key) => Either::A( + self.send_ping(pub_key) + .or_else(|_| future::ok(Err(LivenessError::SendPingFailed))), + ), + LivenessRequest::GetPingCount => Either::B(future::ok(Result::<_, LivenessError>::Ok( + LivenessResponse::Count(self.get_ping_count()), + ))), + LivenessRequest::GetPongCount => Either::B(future::ok(Result::<_, LivenessError>::Ok( + LivenessResponse::Count(self.get_pong_count()), + ))), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures::Async; + use rand::rngs::OsRng; + use tari_crypto::keys::PublicKey; + use tari_service_framework::transport; + use tokio::runtime::Runtime; + use tower_util::service_fn; + + #[test] + fn get_ping_pong_count() { + let state = Arc::new(LivenessState::new()); + state.inc_pings_received(); + state.inc_pongs_received(); + state.inc_pongs_received(); + + let outbound_service = service_fn(|_| future::ok::<_, ()>(Ok(()))); + let (req, _res) = transport::channel(outbound_service); + let oms_handle = CommsOutboundHandle::new(req); + + let mut service = LivenessService::new(state, oms_handle); + + let mut fut = service.call(LivenessRequest::GetPingCount); + match fut.poll().unwrap() { + Async::Ready(Ok(LivenessResponse::Count(n))) => assert_eq!(n, 1), + _ => panic!(), + } + + let mut fut = service.call(LivenessRequest::GetPongCount); + match fut.poll().unwrap() { + Async::Ready(Ok(LivenessResponse::Count(n))) => assert_eq!(n, 2), + _ => panic!(), + } + } + + #[test] + fn send_ping() { + let mut rt = Runtime::new().unwrap(); + let state = Arc::new(LivenessState::new()); + + // This service stubs out CommsOutboundService and always returns a successful result. + // Therefore, LivenessService will behave as if it was able to send the ping + // without actually sending it. + let outbound_service = service_fn(|_| future::ok::<_, ()>(Ok(()))); + let (req, res) = transport::channel(outbound_service); + rt.spawn(res); + + let oms_handle = CommsOutboundHandle::new(req); + + let mut service = LivenessService::new(Arc::clone(&state), oms_handle); + + let mut rng = OsRng::new().unwrap(); + let (_, pk) = CommsPublicKey::random_keypair(&mut rng); + let fut = service.call(LivenessRequest::SendPing(pk)); + match rt.block_on(fut).unwrap() { + Ok(LivenessResponse::PingSent) => {}, + Ok(_) => panic!("received unexpected response from liveness service"), + Err(err) => panic!("received unexpected error from liveness service: {:?}", err), + } + + assert_eq!(state.pings_sent(), 1); + } +} diff --git a/base_layer/p2p/src/services/liveness/state.rs b/base_layer/p2p/src/services/liveness/state.rs new file mode 100644 index 0000000000..0d08f55516 --- /dev/null +++ b/base_layer/p2p/src/services/liveness/state.rs @@ -0,0 +1,127 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// State for the LivenessService. +#[derive(Default)] +pub struct LivenessState { + pings_received: AtomicUsize, + pongs_received: AtomicUsize, + + pings_sent: AtomicUsize, + pongs_sent: AtomicUsize, +} + +impl LivenessState { + pub fn new() -> Self { + Default::default() + } + + pub fn inc_pings_sent(&self) -> usize { + self.pings_sent.fetch_add(1, Ordering::Relaxed) + } + + pub fn inc_pongs_sent(&self) -> usize { + self.pongs_sent.fetch_add(1, Ordering::Relaxed) + } + + pub fn pings_sent(&self) -> usize { + self.pings_sent.load(Ordering::Relaxed) + } + + pub fn pongs_sent(&self) -> usize { + self.pongs_sent.load(Ordering::Relaxed) + } + + pub fn inc_pings_received(&self) -> usize { + self.pings_received.fetch_add(1, Ordering::Relaxed) + } + + pub fn inc_pongs_received(&self) -> usize { + self.pongs_received.fetch_add(1, Ordering::Relaxed) + } + + pub fn pings_received(&self) -> usize { + self.pings_received.load(Ordering::Relaxed) + } + + pub fn pongs_received(&self) -> usize { + self.pongs_received.load(Ordering::Relaxed) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn new() { + let state = LivenessState::new(); + assert_eq!(state.pings_received.load(Ordering::SeqCst), 0); + assert_eq!(state.pongs_received.load(Ordering::SeqCst), 0); + assert_eq!(state.pings_sent.load(Ordering::SeqCst), 0); + assert_eq!(state.pongs_sent.load(Ordering::SeqCst), 0); + } + + #[test] + fn getters() { + let state = LivenessState::new(); + state.pings_received.store(5, Ordering::SeqCst); + assert_eq!(state.pings_received(), 5); + assert_eq!(state.pongs_received(), 0); + assert_eq!(state.pings_sent(), 0); + assert_eq!(state.pongs_sent(), 0); + } + + #[test] + fn inc_pings_sent() { + let state = LivenessState::new(); + assert_eq!(state.pings_sent.load(Ordering::SeqCst), 0); + assert_eq!(state.inc_pings_sent(), 0); + assert_eq!(state.pings_sent.load(Ordering::SeqCst), 1); + } + + #[test] + fn inc_pongs_sent() { + let state = LivenessState::new(); + assert_eq!(state.pongs_sent.load(Ordering::SeqCst), 0); + assert_eq!(state.inc_pongs_sent(), 0); + assert_eq!(state.pongs_sent.load(Ordering::SeqCst), 1); + } + + #[test] + fn inc_pings_received() { + let state = LivenessState::new(); + assert_eq!(state.pings_received.load(Ordering::SeqCst), 0); + assert_eq!(state.inc_pings_received(), 0); + assert_eq!(state.pings_received.load(Ordering::SeqCst), 1); + } + + #[test] + fn inc_pongs_received() { + let state = LivenessState::new(); + assert_eq!(state.pongs_received.load(Ordering::SeqCst), 0); + assert_eq!(state.inc_pongs_received(), 0); + assert_eq!(state.pongs_received.load(Ordering::SeqCst), 1); + } +} diff --git a/infrastructure/merklemountainrange/tests/support/testobject.rs b/base_layer/p2p/src/services/mod.rs similarity index 74% rename from infrastructure/merklemountainrange/tests/support/testobject.rs rename to base_layer/p2p/src/services/mod.rs index d238136089..2154dfad4f 100644 --- a/infrastructure/merklemountainrange/tests/support/testobject.rs +++ b/base_layer/p2p/src/services/mod.rs @@ -20,26 +20,20 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use digest::Digest; -use merklemountainrange::merklenode::ObjectHash; -use tari_utilities::Hashable; +// mod initialization; +pub mod comms_outbound; +mod domain_deserializer; +pub mod liveness; +mod service_name; -pub struct TestObject { - pub id: String, - pub hasher: D, -} +use tari_service_framework::handles; -impl TestObject { - pub fn new(id: String) -> TestObject { - let hasher = D::new(); - TestObject { id, hasher } - } -} +pub use self::service_name::ServiceName; -impl Hashable for TestObject { - fn hash(&self) -> ObjectHash { - let mut hash = D::new(); - hash.input(self.id.as_bytes()); - hash.result().to_vec() - } -} +/// ServiceHandles collection +pub type ServiceHandles = handles::ServiceHandles; +/// ServiceHandles future. +/// +/// This future wraps a ServiceHandles collection and will resolve to the handles +/// collection once `notify_ready` is called. +pub type ServiceHandlesFuture = handles::ServiceHandlesFuture; diff --git a/base_layer/p2p/src/services/service_name.rs b/base_layer/p2p/src/services/service_name.rs new file mode 100644 index 0000000000..a561471013 --- /dev/null +++ b/base_layer/p2p/src/services/service_name.rs @@ -0,0 +1,30 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// Used to name services and retrieve service handles +#[derive(Eq, PartialEq, Hash)] +pub enum ServiceName { + /// Outbound comms service + CommsOutbound, + /// Liveness service + Liveness, +} diff --git a/base_layer/p2p/src/sync_services/error.rs b/base_layer/p2p/src/sync_services/error.rs new file mode 100644 index 0000000000..e5d041f4bd --- /dev/null +++ b/base_layer/p2p/src/sync_services/error.rs @@ -0,0 +1,48 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; +use serde::export::fmt::Debug; +use tari_comms::{builder::CommsServicesError, domain_subscriber::DomainSubscriberError}; + +#[derive(Debug, Error)] +pub enum ServiceError { + #[error(msg_embedded, non_std, no_from)] + ServiceInitializationFailed(String), + CommsServicesError(CommsServicesError), + /// Timeout waiting for service threads to complete + JoinTimedOut, + /// Failed to send shut + ShutdownSendFailed, + #[error(msg_embedded, non_std, no_from)] + InternalServiceError(String), + /// Unable to get sole ownership of comms services. Another thread still has a handle. + CommsServiceOwnershipError, + DomainSubscriberError(DomainSubscriberError), +} + +impl ServiceError { + pub fn internal_service_error() -> impl Fn(E) -> Self + where E: Debug { + |err| ServiceError::InternalServiceError(format!("Internal service error: {:?}", err)) + } +} diff --git a/base_layer/p2p/src/sync_services/executor.rs b/base_layer/p2p/src/sync_services/executor.rs new file mode 100644 index 0000000000..08dc9c7345 --- /dev/null +++ b/base_layer/p2p/src/sync_services/executor.rs @@ -0,0 +1,301 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{error::ServiceError, registry::ServiceRegistry}; +use crate::tari_message::TariMessageType; +use log::*; +use std::{ + sync::{Arc, Mutex}, + thread, + time::Duration, +}; +use tari_comms::{ + builder::CommsServices, + outbound_message_service::outbound_message_service::OutboundMessageService, + peer_manager::{NodeIdentity, PeerManager}, +}; +use threadpool::ThreadPool; + +use crossbeam_channel as channel; +use crossbeam_channel::{Receiver, RecvTimeoutError, Sender}; + +use tari_comms::{connection::InprocAddress, inbound_message_service::InboundTopicSubscriptionFactory}; +const LOG_TARGET: &str = "base_layer::p2p::services"; + +/// Control messages for services +pub enum ServiceControlMessage { + /// Service should shut down + Shutdown, +} + +/// This is responsible for creating and managing the thread pool for +/// services that should be executed. +pub struct ServiceExecutor { + thread_pool: Mutex, + senders: Vec>, +} + +impl ServiceExecutor { + /// Execute the services contained in the given [ServiceRegistry]. + pub fn execute(comms_services: &CommsServices, registry: ServiceRegistry) -> Self { + let thread_pool = threadpool::Builder::new() + .thread_name("DomainServices".to_string()) + .num_threads(registry.num_services()) + .thread_stack_size(1_000_000) + .build(); + + let mut senders = Vec::new(); + + for mut service in registry.services.into_iter() { + let (sender, receiver) = channel::unbounded(); + senders.push(sender); + + let service_context = ServiceContext { + oms: comms_services.outbound_message_service(), + ims_message_sink_address: comms_services.connection_manager().get_message_sink_address().clone(), + peer_manager: comms_services.peer_manager(), + node_identity: comms_services.node_identity(), + receiver, + inbound_message_subscription_factory: comms_services.inbound_message_subscription_factory(), + }; + + thread_pool.execute(move || { + info!(target: LOG_TARGET, "Starting service {}", service.get_name()); + + match service.execute(service_context) { + Ok(_) => { + info!( + target: LOG_TARGET, + "Service '{}' has successfully shut down", + service.get_name(), + ); + }, + Err(err) => { + error!( + target: LOG_TARGET, + "Service '{}' has exited with an error: {:?}", + service.get_name(), + err + ); + }, + } + }); + } + + Self { + thread_pool: Mutex::new(thread_pool), + senders, + } + } + + /// Send a [ServiceControlMessage::Shutdown] message to all services. + pub fn shutdown(&self) -> Result<(), ServiceError> { + let mut failed = false; + for sender in &self.senders { + if sender.send(ServiceControlMessage::Shutdown).is_err() { + failed = true; + } + } + + // TODO: Wait for services to exit and then shutdown the comms + // self.comms_services + // .shutdown() + // .map_err(ServiceError::CommsServicesError)?; + + if failed { + Err(ServiceError::ShutdownSendFailed) + } else { + Ok(()) + } + } + + /// Join on all threads in the thread pool until they all exit or a given timeout is reached. + pub fn join_timeout(self, timeout: Duration) -> Result<(), ServiceError> { + let (tx, rx) = channel::unbounded(); + let thread_pool = self.thread_pool; + thread::spawn(move || { + acquire_lock!(thread_pool).join(); + let _ = tx.send(()); + }); + + rx.recv_timeout(timeout).map_err(|_| ServiceError::JoinTimedOut)?; + + Ok(()) + } +} + +/// The context object given to each service. This allows the service to receive [ServiceControlMessage]s, +/// access the outbound message service and create [DomainConnector]s to receive comms messages of +/// a particular [TariMessageType]. +pub struct ServiceContext { + oms: Arc, + ims_message_sink_address: InprocAddress, + peer_manager: Arc, + node_identity: Arc, + receiver: Receiver, + inbound_message_subscription_factory: Arc>, +} + +impl ServiceContext { + /// Attempt to retrieve a control message. Returns `Some(ServiceControlMessage)` if there + /// is a message on the channel or `None` if the channel is empty and the timeout is reached. + pub fn get_control_message(&self, timeout: Duration) -> Option { + match self.receiver.recv_timeout(timeout) { + Ok(msg) => Some(msg), + // Sender has disconnected (dropped) so return a shutdown signal + // This should never happen in normal operation + Err(RecvTimeoutError::Disconnected) => Some(ServiceControlMessage::Shutdown), + Err(RecvTimeoutError::Timeout) => None, + } + } + + /// Retrieve and `Arc` of the outbound message service. Used for sending outbound messages. + pub fn outbound_message_service(&self) -> Arc { + Arc::clone(&self.oms) + } + + /// Retrieve and `Arc` of the PeerManager. Used for managing peers. + pub fn peer_manager(&self) -> Arc { + Arc::clone(&self.peer_manager) + } + + /// Retrieve and `Arc` of the NodeIdentity. Used for managing the current Nodes Identity. + pub fn node_identity(&self) -> Arc { + Arc::clone(&self.node_identity) + } + + /// Retrieve a reference to the message sink address of the InboundMessageService + pub fn ims_message_sink_address(&self) -> &InprocAddress { + &self.ims_message_sink_address + } + + pub fn inbound_message_subscription_factory(&self) -> Arc> { + Arc::clone(&self.inbound_message_subscription_factory) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{sync_services::Service, tari_message::NetMessage}; + use rand::rngs::OsRng; + use std::{path::PathBuf, sync::RwLock}; + use tari_comms::{peer_manager::NodeIdentity, CommsBuilder}; + use tari_storage::{ + lmdb_store::{LMDBBuilder, LMDBError, LMDBStore}, + LMDBWrapper, + }; + + #[derive(Clone)] + struct AddWordService(Arc>, &'static str); + + impl Service for AddWordService { + fn get_name(&self) -> String { + "tick service".to_string() + } + + fn get_message_types(&self) -> Vec { + vec![NetMessage::PingPong.into()] + } + + fn execute(&mut self, context: ServiceContext) -> Result<(), ServiceError> { + let mut added_word = false; + loop { + if !added_word { + let mut lock = self.0.write().unwrap(); + *lock = format!("{} {}", *lock, self.1); + added_word = true; + } + if let Some(msg) = context.get_control_message(Duration::from_millis(1000)) { + match msg { + ServiceControlMessage::Shutdown => break, + } + } + } + + Ok(()) + } + } + + fn get_path(name: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name); + path.to_str().unwrap().to_string() + } + + fn init_datastore(name: &str) -> Result { + let path = get_path(name); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + LMDBBuilder::new() + .set_path(&path) + .set_environment_size(10) + .set_max_number_of_databases(2) + .add_database(name, lmdb_zero::db::CREATE) + .build() + } + + fn clean_up_datastore(name: &str) { + std::fs::remove_dir_all(get_path(name)).unwrap(); + } + + #[test] + fn execute() { + let node_identity = + NodeIdentity::random(&mut OsRng::new().unwrap(), "127.0.0.1:9000".parse().unwrap()).unwrap(); + + let state = Arc::new(RwLock::new("Hello".to_string())); + let service = AddWordService(state.clone(), "Tari"); + let registry = ServiceRegistry::new().register(service); + + let database_name = "executor_execute"; // Note: every test should have unique database + let datastore = init_datastore(database_name).unwrap(); + let peer_database = datastore.get_handle(database_name).unwrap(); + let peer_database = LMDBWrapper::new(Arc::new(peer_database)); + + let comms_services = CommsBuilder::new() + .with_node_identity(node_identity) + .with_peer_storage(peer_database) + .build() + .unwrap() + .start() + .map(Arc::new) + .unwrap(); + + let services = ServiceExecutor::execute(&comms_services, registry); + + services.shutdown().unwrap(); + services.join_timeout(Duration::from_millis(100)).unwrap(); + let comms = Arc::try_unwrap(comms_services) + .map_err(|_| ServiceError::CommsServiceOwnershipError) + .unwrap(); + + comms.shutdown().unwrap(); + + { + let lock = acquire_read_lock!(state); + assert_eq!(*lock, "Hello Tari"); + } + + clean_up_datastore(database_name); + } +} diff --git a/base_layer/p2p/src/sync_services/mod.rs b/base_layer/p2p/src/sync_services/mod.rs new file mode 100644 index 0000000000..2e8668f354 --- /dev/null +++ b/base_layer/p2p/src/sync_services/mod.rs @@ -0,0 +1,33 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod executor; +mod registry; +mod service; + +pub use self::{ + error::ServiceError, + executor::{ServiceContext, ServiceControlMessage, ServiceExecutor}, + registry::ServiceRegistry, + service::{Service, ServiceApiWrapper, DEFAULT_API_TIMEOUT_MS}, +}; diff --git a/base_layer/p2p/src/sync_services/registry.rs b/base_layer/p2p/src/sync_services/registry.rs new file mode 100644 index 0000000000..5ce96d4d3f --- /dev/null +++ b/base_layer/p2p/src/sync_services/registry.rs @@ -0,0 +1,85 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::service::Service; + +/// This is a container for services. Services can be registered here +/// and given to the [ServiceExecutor] for execution. This also +/// builds [CommsRoutes] for the given services. +#[derive(Default)] +pub struct ServiceRegistry { + pub(super) services: Vec>, +} + +impl ServiceRegistry { + /// Create a new [ServiceRegistry]. + pub fn new() -> Self { + Self { services: Vec::new() } + } + + /// Register a [Service] + pub fn register(mut self, service: S) -> Self + where S: Service + 'static { + self.services.push(Box::new(service)); + self + } + + /// Retrieves the number of services registered. + pub fn num_services(&self) -> usize { + self.services.len() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + sync_services::{ServiceContext, ServiceError}, + tari_message::{NetMessage, TariMessageType}, + }; + + struct DummyService; + + impl Service for DummyService { + fn get_name(&self) -> String { + "dummy".to_string() + } + + fn get_message_types(&self) -> Vec { + vec![NetMessage::PingPong.into()] + } + + fn execute(&mut self, _context: ServiceContext) -> Result<(), ServiceError> { + // do nothing + Ok(()) + } + } + + #[test] + fn num_services() { + let mut registry = ServiceRegistry::new(); + assert_eq!(registry.num_services(), 0); + let service = DummyService {}; + registry = registry.register(service); + assert_eq!(registry.num_services(), 1); + } +} diff --git a/base_layer/p2p/src/sync_services/service.rs b/base_layer/p2p/src/sync_services/service.rs new file mode 100644 index 0000000000..34dc54c7b2 --- /dev/null +++ b/base_layer/p2p/src/sync_services/service.rs @@ -0,0 +1,76 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::executor::ServiceContext; +use crate::{sync_services::ServiceError, tari_message::TariMessageType}; +use crossbeam_channel as channel; +use std::{sync::Arc, time::Duration}; + +/// This trait should be implemented for services +pub trait Service: Send + Sync { + /// A 'friendly' name used for logging purposes + fn get_name(&self) -> String; + /// Returns the message types this service requires. These will be subscribed to. + fn get_message_types(&self) -> Vec; + /// The entry point of the service. This will be executed in a dedicated thread. + /// The service should use `context.create_connector(message_type)` to create a `DomainConnector` + /// for the registered message types returned from `Service::get_message_types`. + /// This should contain a loop which reads control messages (`context.get_control_message`) + /// and connector messages and processes them. + fn execute(&mut self, context: ServiceContext) -> Result<(), ServiceError>; +} + +/// Default duration that a API 'client' will wait for a response from the service before returning a timeout error +pub const DEFAULT_API_TIMEOUT_MS: u64 = 3000; + +/// Thin convenience wrapper for any service api +pub struct ServiceApiWrapper { + api: Arc, + receiver: channel::Receiver, + sender: channel::Sender, +} + +impl ServiceApiWrapper { + /// Create a new service API + pub fn new(receiver: channel::Receiver, sender: channel::Sender, api: Arc) -> Self { + Self { api, receiver, sender } + } + + /// Send a reply to the calling API + pub fn send_reply(&self, msg: Res) -> Result<(), channel::SendError> { + self.sender.send(msg) + } + + /// Attempt to receive a service API message + pub fn recv_timeout(&self, timeout: Duration) -> Result, channel::RecvTimeoutError> { + match self.receiver.recv_timeout(timeout) { + Ok(msg) => Ok(Some(msg)), + Err(channel::RecvTimeoutError::Timeout) => Ok(None), + Err(err) => Err(err), + } + } + + /// Return the API + pub fn get_api(&self) -> Arc { + self.api.clone() + } +} diff --git a/base_layer/p2p/src/tari_message.rs b/base_layer/p2p/src/tari_message.rs new file mode 100644 index 0000000000..a92193dbad --- /dev/null +++ b/base_layer/p2p/src/tari_message.rs @@ -0,0 +1,144 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use serde::{Deserialize, Serialize}; + +/// Reduce repetitive boilerplate by defining a `is_xxx_message() -> bool` function for each class of message +macro_rules! is_type { + ($m:ident, $f:ident) => { + pub fn $f(&self) -> bool { + self.0 >= $m::START_RANGE && self.0 <= $m::END_RANGE + } + } +} + +/// A tari message type is an immutable 8-bit unsigned integer indicating the type of message being received or sent +/// over the network. Details are in +/// [RFC-0172](https://rfc.tari.com/RFC-0172_PeerToPeerMessagingProtocol.html#messagetype). +#[derive(Serialize, Deserialize, Eq, PartialEq, Hash, Clone, Debug)] +pub struct TariMessageType(u8); + +#[allow(non_snake_case, non_upper_case_globals)] +pub mod NetMessage { + pub(super) const START_RANGE: u8 = 1; + pub(super) const END_RANGE: u8 = 5; // Can be extended to 32 + /// DHT network join, and discovery of nodes and peers + pub const Join: u8 = 1; + pub const Discover: u8 = 2; + /// Message sent for PingPong and liveness checks + pub const PingPong: u8 = 3; + /// Messages sent for Store-and-forward functionality + pub const RetrieveMessages: u8 = 4; + pub const StoredMessages: u8 = 5; +} + +#[allow(non_snake_case, non_upper_case_globals)] +pub mod PeerMessage { + pub(super) const START_RANGE: u8 = 33; + pub(super) const END_RANGE: u8 = 33; // Can be extended to 64 + pub const Connect: u8 = 33; +} + +#[allow(non_snake_case, non_upper_case_globals)] +pub mod BlockchainMessage { + pub(super) const START_RANGE: u8 = 65; + pub(super) const END_RANGE: u8 = 67; // Can be extended to 96 + pub const NewBlock: u8 = 65; + pub const Transaction: u8 = 66; + pub const TransactionReply: u8 = 67; +} + +#[allow(non_snake_case, non_upper_case_globals)] +pub mod ValidatorNodeMessage { + pub(super) const START_RANGE: u8 = 97; + pub(super) const END_RANGE: u8 = 97; // Can be extended to 224 + pub const Instruction: u8 = 97; +} + +#[allow(non_snake_case, non_upper_case_globals)] +pub mod ExtendedMessage { + pub(super) const START_RANGE: u8 = 225; + pub(super) const END_RANGE: u8 = 226; // Can be extended to 255 + pub const Text: u8 = 225; + pub const TextAck: u8 = 226; +} + +impl TariMessageType { + is_type!(NetMessage, is_net_message); + + is_type!(PeerMessage, is_peer_message); + + is_type!(BlockchainMessage, is_blockchain_message); + + is_type!(ValidatorNodeMessage, is_vn_message); + + is_type!(ExtendedMessage, is_extended_message); + + pub fn new(value: u8) -> TariMessageType { + TariMessageType(value) + } + + pub fn value(&self) -> u8 { + self.0 + } + + pub fn is_known_message(&self) -> bool { + self.is_net_message() || self.is_peer_message() || self.is_blockchain_message() || self.is_vn_message() + } +} + +impl From for TariMessageType { + fn from(v: u8) -> Self { + TariMessageType::new(v) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn message_type_definition() { + // When constructing messages, we want to use human-readable definitions as defined in RFC-0172 + let t = TariMessageType::new(PeerMessage::Connect); + assert_eq!(t.value(), 33); + let t = TariMessageType::new(ValidatorNodeMessage::Instruction); + assert_eq!(t.value(), 97); + let t = TariMessageType::new(BlockchainMessage::NewBlock); + assert_eq!(t.value(), 65); + } + + #[test] + fn create_message() { + // When reading from the wire, the message type will be a byte value + let t = TariMessageType::from(2); + assert_eq!(t.value(), NetMessage::Discover); + assert!(t.is_net_message()); + assert!(t.is_known_message()); + } + + #[test] + fn unknown_message_type() { + let t = TariMessageType::from(30); + assert_eq!(t.is_known_message(), false); + } +} diff --git a/base_layer/p2p/tests/data/.gitkeep b/base_layer/p2p/tests/data/.gitkeep new file mode 100644 index 0000000000..79e790c1e5 --- /dev/null +++ b/base_layer/p2p/tests/data/.gitkeep @@ -0,0 +1 @@ +Temp folder for LMDB database files \ No newline at end of file diff --git a/base_layer/p2p/tests/dht/mod.rs b/base_layer/p2p/tests/dht/mod.rs new file mode 100644 index 0000000000..b08895cba7 --- /dev/null +++ b/base_layer/p2p/tests/dht/mod.rs @@ -0,0 +1,311 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// NOTE: These tests use ports 11113 to 11122 +use crate::support::random_string; +use rand::rngs::OsRng; +use std::{sync::Arc, thread, time::Duration}; +use tari_comms::{ + builder::CommsServices, + connection::NetAddress, + connection_manager::PeerConnectionConfig, + control_service::ControlServiceConfig, + message::NodeDestination, + peer_manager::{peer_storage::PeerStorage, NodeIdentity, Peer, PeerManager}, + types::CommsDatabase, + CommsBuilder, +}; +use tari_p2p::{ + dht_service::{DHTService, DHTServiceApi}, + sync_services::{ServiceExecutor, ServiceRegistry}, + tari_message::TariMessageType, +}; +use tari_storage::{LMDBWrapper, lmdb_store::LMDBBuilder}; +use tempdir::TempDir; + +fn new_node_identity(control_service_address: NetAddress) -> NodeIdentity { + NodeIdentity::random(&mut OsRng::new().unwrap(), control_service_address).unwrap() +} + +fn create_peer_storage(tmpdir: &TempDir, database_name: &str, peers: Vec) -> CommsDatabase { + let datastore = LMDBBuilder::new() + .set_path(tmpdir.path().to_str().unwrap()) + .set_environment_size(10) + .set_max_number_of_databases(1) + .add_database(database_name, lmdb_zero::db::CREATE) + .build() + .unwrap(); + + let peer_database = datastore.get_handle(database_name).unwrap(); + let peer_database = LMDBWrapper::new(Arc::new(peer_database)); + let mut storage = PeerStorage::new(peer_database).unwrap(); + for peer in peers { + storage.add_peer(peer).unwrap(); + } + + storage.into_datastore() +} + +fn setup_dht_service( + node_identity: NodeIdentity, + peer_storage: CommsDatabase, +) -> (ServiceExecutor, Arc, Arc>) +{ + let control_service_address = node_identity.control_service_address().unwrap(); + let dht_service = DHTService::new(); + let dht_api = dht_service.get_api(); + + let services = ServiceRegistry::new().register(dht_service); + let comms = CommsBuilder::new() + .with_routes(services.build_comms_routes()) + .with_node_identity(node_identity) + .with_peer_storage(peer_storage) + .configure_peer_connections(PeerConnectionConfig { + host: "127.0.0.1".parse().unwrap(), + ..Default::default() + }) + .configure_control_service(ControlServiceConfig { + socks_proxy_address: None, + listener_address: control_service_address, + requested_connection_timeout: Duration::from_millis(5000), + }) + .build() + .unwrap() + .start() + .map(Arc::new) + .unwrap(); + + (ServiceExecutor::execute(&comms, services), dht_api, comms) +} + +fn pause() { + thread::sleep(Duration::from_millis(3000)); +} + +#[test] +#[allow(non_snake_case)] +fn test_dht_join_propagation() { + // Create 3 nodes where only Node B knows A and C, but A and C want to talk to each other + let node_A_identity = new_node_identity("127.0.0.1:11113".parse().unwrap()); + let node_B_identity = new_node_identity("127.0.0.1:11114".parse().unwrap()); + let node_C_identity = new_node_identity("127.0.0.1:11115".parse().unwrap()); + + // Setup Node A + let node_A_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_A_database_name = "node_A"; + let (node_A_services, node_A_dht_service_api, _comms_A) = setup_dht_service( + node_A_identity.clone(), + create_peer_storage(&node_A_tmpdir, node_A_database_name, vec![node_B_identity + .clone() + .into()]), + ); + // Setup Node B + let node_B_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_B_database_name = "node_B"; + let (node_B_services, _node_B_dht_service_api, _comms_B) = setup_dht_service( + node_B_identity.clone(), + create_peer_storage(&node_B_tmpdir, node_B_database_name, vec![ + node_A_identity.clone().into(), + node_C_identity.clone().into(), + ]), + ); + // Setup Node C + let node_C_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_C_database_name = "node_C"; + let (node_C_services, _node_C_dht_service_api, _comms_C) = setup_dht_service( + node_C_identity.clone(), + create_peer_storage(&node_C_tmpdir, node_C_database_name, vec![node_B_identity + .clone() + .into()]), + ); + + // Send a join request from Node A, through B to C. As all Nodes are in the same network region, once Node C + // receives the join request from Node A, it will send a direct join request back to A. + pause(); + assert!(node_A_dht_service_api.send_join().is_ok()); + + pause(); + node_A_services.shutdown().unwrap(); + node_B_services.shutdown().unwrap(); + node_C_services.shutdown().unwrap(); + + // Restore PeerStorage of Node A and Node C and check that they are aware of each other + pause(); + let node_A_peer_manager = + PeerManager::new(create_peer_storage(&node_A_tmpdir, node_A_database_name, vec![])).unwrap(); + let node_C_peer_manager = + PeerManager::new(create_peer_storage(&node_C_tmpdir, node_C_database_name, vec![])).unwrap(); + assert!(node_C_peer_manager + .exists(&node_A_identity.identity.public_key) + .unwrap()); + assert!(node_A_peer_manager + .exists(&node_C_identity.identity.public_key) + .unwrap()); +} + +#[test] +#[allow(non_snake_case)] +fn test_dht_discover_propagation() { + // Create 3 nodes where only Node B knows A and C, but A and C want to talk to each other + let node_A_identity = new_node_identity("127.0.0.1:11116".parse().unwrap()); + let node_B_identity = new_node_identity("127.0.0.1:11117".parse().unwrap()); + let node_C_identity = new_node_identity("127.0.0.1:11118".parse().unwrap()); + let node_D_identity = new_node_identity("127.0.0.1:11119".parse().unwrap()); + + // Setup Node A + let node_A_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_A_database_name = "node_A"; + let (node_A_services, node_A_dht_service_api, _comms_A) = setup_dht_service( + node_A_identity.clone(), + create_peer_storage(&node_A_tmpdir, node_A_database_name, vec![node_B_identity + .clone() + .into()]), + ); + // Setup Node B + let node_B_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_B_database_name = "node_B"; + let (node_B_services, _node_B_dht_service_api, _comms_B) = setup_dht_service( + node_B_identity.clone(), + create_peer_storage(&node_B_tmpdir, node_B_database_name, vec![ + node_A_identity.clone().into(), + node_C_identity.clone().into(), + ]), + ); + // Setup Node C + let node_C_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_C_database_name = "node_C"; + let (node_C_services, _node_C_dht_service_api, _comms_C) = setup_dht_service( + node_C_identity.clone(), + create_peer_storage(&node_C_tmpdir, node_C_database_name, vec![ + node_B_identity.clone().into(), + node_D_identity.clone().into(), + ]), + ); + // Setup Node D + let node_D_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_D_database_name = "node_D"; + let (node_D_services, _node_D_dht_service_api, _comms_D) = setup_dht_service( + node_D_identity.clone(), + create_peer_storage(&node_D_tmpdir, node_D_database_name, vec![node_C_identity + .clone() + .into()]), + ); + + // Send a discover request from Node A, through B and C, to D. Once Node D + // receives the discover request from Node A, it will send a direct join request back to A. + pause(); + assert!(node_A_dht_service_api + .send_discover( + node_D_identity.identity.public_key.clone(), + None, + NodeDestination::Unknown + ) + .is_ok()); + + pause(); + node_A_services.shutdown().unwrap(); + node_B_services.shutdown().unwrap(); + node_C_services.shutdown().unwrap(); + node_D_services.shutdown().unwrap(); + + // Restore PeerStorage of Node A and Node D and check that they are aware of each other + pause(); + let node_A_peer_manager = + PeerManager::new(create_peer_storage(&node_A_tmpdir, node_A_database_name, vec![])).unwrap(); + let node_D_peer_manager = + PeerManager::new(create_peer_storage(&node_D_tmpdir, node_D_database_name, vec![])).unwrap(); + assert!(node_A_peer_manager + .exists(&node_D_identity.identity.public_key) + .unwrap()); + assert!(node_D_peer_manager + .exists(&node_A_identity.identity.public_key) + .unwrap()); +} + +#[test] +#[allow(non_snake_case)] +fn test_dht_join_on_service_start() { + // Create 2 nodes where Node A has a old control_service_address for Node B + let node_A_identity = new_node_identity("127.0.0.1:11120".parse().unwrap()); + let outdated_node_B_identity = new_node_identity("127.0.0.1:11121".parse().unwrap()); + let node_B_identity = outdated_node_B_identity.clone(); // new_node_identity("127.0.0.1:11122".parse().unwrap()); + node_B_identity + .set_control_service_address("127.0.0.1:11121".parse().unwrap()) + .unwrap(); + + // Setup Node A + let node_A_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_A_database_name = "node_A"; + let (node_A_services, _node_A_dht_service_api, comms_A) = setup_dht_service( + node_A_identity.clone(), + create_peer_storage(&node_A_tmpdir, node_A_database_name, vec![outdated_node_B_identity + .clone() + .into()]), + ); + // Wait for Node A to startup + pause(); + + // Setup Node B + let node_B_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_B_database_name = "node_B"; + let (node_B_services, _node_B_dht_service_api, _comms_B) = setup_dht_service( + node_B_identity.clone(), + create_peer_storage(&node_B_tmpdir, node_B_database_name, vec![node_A_identity + .clone() + .into()]), + ); + // Node A and B are aware of each other on startup but Node A has outdated information for Node B + // As Node B comes online it will send a join request to all its neighbouring peers + + pause(); + // The NetAddress of Node A has changed, the DHT Service will detect the change and inform the neighbouring peers + // using a join request + let node_A_updated_net_address: NetAddress = "127.0.0.1:11122".parse().unwrap(); + comms_A + .node_identity() + .set_control_service_address(node_A_updated_net_address.clone()) + .unwrap(); + + pause(); + node_A_services.shutdown().unwrap(); + node_B_services.shutdown().unwrap(); + + // Restore PeerStorage of Node A and B + pause(); + let node_A_peer_manager = + PeerManager::new(create_peer_storage(&node_A_tmpdir, node_A_database_name, vec![])).unwrap(); + let node_B_peer_manager = + PeerManager::new(create_peer_storage(&node_B_tmpdir, node_B_database_name, vec![])).unwrap(); + // Check that Node A is aware of the updated NetAddress of Node B + let mut peer = node_A_peer_manager + .find_with_public_key(&node_B_identity.identity.public_key) + .unwrap(); + assert!(peer + .addresses + .find_address_mut(&node_B_identity.control_service_address().unwrap()) + .is_ok()); + // Check that Node B is aware of the updated NetAddress of Node A + let mut peer = node_B_peer_manager + .find_with_public_key(&node_A_identity.identity.public_key) + .unwrap(); + assert!(peer.addresses.find_address_mut(&node_A_updated_net_address).is_ok()); +} diff --git a/base_layer/p2p/tests/mod.rs b/base_layer/p2p/tests/mod.rs new file mode 100644 index 0000000000..6c1867d607 --- /dev/null +++ b/base_layer/p2p/tests/mod.rs @@ -0,0 +1,27 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// TODO Put this back in after Futures Comms stack refactor +// mod dht; +// mod saf; +mod ping_pong; +mod support; diff --git a/base_layer/p2p/tests/ping_pong/mod.rs b/base_layer/p2p/tests/ping_pong/mod.rs new file mode 100644 index 0000000000..d8d9034bef --- /dev/null +++ b/base_layer/p2p/tests/ping_pong/mod.rs @@ -0,0 +1,140 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// NOTE: This test uses ports 11111 and 11112 + +use crate::support::{assert_change, random_string}; +use rand::rngs::OsRng; +use std::{sync::Arc, time::Duration}; +use tari_comms::{ + builder::CommsServices, + connection::NetAddress, + connection_manager::PeerConnectionConfig, + control_service::ControlServiceConfig, + peer_manager::{peer_storage::PeerStorage, NodeIdentity, Peer}, + types::CommsDatabase, + CommsBuilder, +}; +use tari_p2p::{ + ping_pong::{PingPongService, PingPongServiceApi}, + sync_services::{ServiceExecutor, ServiceRegistry}, + tari_message::TariMessageType, +}; +use tari_storage::{lmdb_store::LMDBBuilder, LMDBWrapper}; +use tempdir::TempDir; + +fn new_node_identity(control_service_address: NetAddress) -> NodeIdentity { + NodeIdentity::random(&mut OsRng::new().unwrap(), control_service_address).unwrap() +} + +fn create_peer_storage(tmpdir: &TempDir, database_name: &str, peers: Vec) -> CommsDatabase { + let datastore = LMDBBuilder::new() + .set_path(tmpdir.path().to_str().unwrap()) + .set_environment_size(10) + .set_max_number_of_databases(1) + .add_database(database_name, lmdb_zero::db::CREATE) + .build() + .unwrap(); + + let peer_database = datastore.get_handle(database_name).unwrap(); + let peer_database = LMDBWrapper::new(Arc::new(peer_database)); + let mut storage = PeerStorage::new(peer_database).unwrap(); + for peer in peers { + storage.add_peer(peer).unwrap(); + } + + storage.into_datastore() +} + +fn setup_ping_pong_service( + node_identity: NodeIdentity, + peer_storage: CommsDatabase, +) -> ( + ServiceExecutor, + Arc, + Arc>, +) +{ + let ping_pong = PingPongService::new(); + let pingpong_api = ping_pong.get_api(); + + let services = ServiceRegistry::new().register(ping_pong); + let comms = CommsBuilder::new() + .with_node_identity(node_identity.clone()) + .with_peer_storage(peer_storage) + .configure_peer_connections(PeerConnectionConfig { + host: "127.0.0.1".parse().unwrap(), + ..Default::default() + }) + .configure_control_service(ControlServiceConfig { + socks_proxy_address: None, + listener_address: node_identity.control_service_address().unwrap(), + requested_connection_timeout: Duration::from_millis(5000), + }) + .build() + .unwrap() + .start() + .map(Arc::new) + .unwrap(); + + (ServiceExecutor::execute(&comms, services), pingpong_api, comms) +} + +#[test] +#[allow(non_snake_case)] +fn end_to_end() { + let node_A_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + + let node_B_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + + let node_A_identity = new_node_identity("127.0.0.1:11111".parse().unwrap()); + let node_B_identity = new_node_identity("127.0.0.1:11112".parse().unwrap()); + + let (node_A_services, node_A_pingpong, _comms_A) = setup_ping_pong_service( + node_A_identity.clone(), + create_peer_storage(&node_A_tmpdir, "node_A", vec![node_B_identity.clone().into()]), + ); + + let (node_B_services, node_B_pingpong, _comms_B) = setup_ping_pong_service( + node_B_identity.clone(), + create_peer_storage(&node_B_tmpdir, "node_B", vec![node_A_identity.clone().into()]), + ); + + // Ping node B + node_A_pingpong + .ping(node_B_identity.identity.public_key.clone()) + .unwrap(); + + assert_change(|| node_B_pingpong.ping_count().unwrap(), 1, 20); + assert_change(|| node_A_pingpong.pong_count().unwrap(), 1, 20); + + // Ping node A + node_B_pingpong + .ping(node_A_identity.identity.public_key.clone()) + .unwrap(); + + assert_change(|| node_B_pingpong.pong_count().unwrap(), 1, 20); + assert_change(|| node_A_pingpong.ping_count().unwrap(), 1, 20); + + node_A_services.shutdown().unwrap(); + node_B_services.shutdown().unwrap(); +} diff --git a/base_layer/p2p/tests/saf/mod.rs b/base_layer/p2p/tests/saf/mod.rs new file mode 100644 index 0000000000..f258b19c51 --- /dev/null +++ b/base_layer/p2p/tests/saf/mod.rs @@ -0,0 +1,178 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// NOTE: These tests use ports 11123 to 11125 +use crate::support::random_string; +use rand::rngs::OsRng; +use std::{convert::TryInto, sync::Arc, thread, time::Duration}; +use tari_comms::{ + builder::CommsServices, + connection::NetAddress, + connection_manager::PeerConnectionConfig, + control_service::ControlServiceConfig, + message::{Frame, Message, MessageEnvelope, MessageFlags, NodeDestination}, + peer_manager::{peer_storage::PeerStorage, NodeIdentity, Peer, PeerManager}, + types::CommsDatabase, + CommsBuilder, +}; +use tari_p2p::{ + dht_service::{DHTService, DHTServiceApi, DiscoverMessage}, + saf_service::{SAFService, SAFServiceApi}, + sync_services::{ServiceExecutor, ServiceRegistry}, + tari_message::TariMessageType, +}; +use tari_storage::{LMDBWrapper, lmdb_store::LMDBBuilder}; +use tari_utilities::message_format::MessageFormat; +use tempdir::TempDir; + +fn new_node_identity(control_service_address: NetAddress) -> NodeIdentity { + NodeIdentity::random(&mut OsRng::new().unwrap(), control_service_address).unwrap() +} + +fn create_peer_storage(tmpdir: &TempDir, database_name: &str, peers: Vec) -> CommsDatabase { + let datastore = LMDBBuilder::new() + .set_path(tmpdir.path().to_str().unwrap()) + .set_environment_size(10) + .set_max_number_of_databases(1) + .add_database(database_name, lmdb_zero::db::CREATE) + .build() + .unwrap(); + + let peer_database = datastore.get_handle(database_name).unwrap(); + let peer_database = LMDBWrapper::new(Arc::new(peer_database)); + let mut storage = PeerStorage::new(peer_database).unwrap(); + for peer in peers { + storage.add_peer(peer).unwrap(); + } + + storage.into_datastore() +} + +fn setup_services( + node_identity: NodeIdentity, + peer_storage: CommsDatabase, +) -> ( + ServiceExecutor, + Arc, + Arc, + Arc>, +) +{ + let control_service_address = node_identity.control_service_address().unwrap(); + let saf_service = SAFService::new(); + let saf_api = saf_service.get_api(); + let dht_service = DHTService::new(); + let dht_api = dht_service.get_api(); + + let services = ServiceRegistry::new().register(saf_service).register(dht_service); + let comms = CommsBuilder::new() + .with_routes(services.build_comms_routes()) + .with_node_identity(node_identity) + .with_peer_storage(peer_storage) + .configure_peer_connections(PeerConnectionConfig { + host: "127.0.0.1".parse().unwrap(), + ..Default::default() + }) + .configure_control_service(ControlServiceConfig { + socks_proxy_address: None, + listener_address: control_service_address, + requested_connection_timeout: Duration::from_millis(5000), + }) + .build() + .unwrap() + .start() + .map(Arc::new) + .unwrap(); + + (ServiceExecutor::execute(&comms, services), saf_api, dht_api, comms) +} + +fn pause() { + thread::sleep(Duration::from_millis(3000)); +} + +#[test] +#[allow(non_snake_case)] +fn test_saf_store() { + // Create 3 nodes where only Node B knows A and C, but A wants to talk to Node C. Node A and C are not online at the + // same time. + let node_A_identity = new_node_identity("127.0.0.1:11123".parse().unwrap()); + let node_B_identity = new_node_identity("127.0.0.1:11124".parse().unwrap()); + let node_C_identity = new_node_identity("127.0.0.1:11125".parse().unwrap()); + + // Setup Node B + let node_B_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_B_database_name = "node_B"; + let (node_B_services, node_B_saf_service_api, _node_B_dht_service_api, _comms_B) = setup_services( + node_B_identity.clone(), + create_peer_storage(&node_B_tmpdir, node_B_database_name, vec![ + node_A_identity.clone().into(), + node_C_identity.clone().into(), + ]), + ); + + // Node A is sending a discovery message to Node C through Node B, but Node C is offline + // TODO: The comms stack of Node B should automatically store the forwarded message, this is not yet implemented. + // This storage behaviour is mimicked by manually storing the message from Node A in Node B. + let discover_msg: Message = DiscoverMessage { + node_id: node_A_identity.identity.node_id.clone(), + net_address: vec![node_A_identity.control_service_address().unwrap()], + } + .try_into() + .unwrap(); + let message_envelope_body: Frame = discover_msg.to_binary().unwrap(); + let message_envelope = MessageEnvelope::construct( + &node_A_identity, + node_C_identity.identity.public_key.clone(), + NodeDestination::Unknown, + message_envelope_body.clone(), + MessageFlags::ENCRYPTED, + ) + .unwrap(); + node_B_saf_service_api.store(message_envelope).unwrap(); // This should happen automatically when node B tries to forward the message + + pause(); + + // Node C comes online + let node_C_tmpdir = TempDir::new(random_string(8).as_str()).unwrap(); + let node_C_database_name = "node_C"; + let (node_C_services, node_C_saf_service_api, _node_C_dht_service_api, _comms_C) = setup_services( + node_C_identity.clone(), + create_peer_storage(&node_C_tmpdir, node_C_database_name, vec![node_B_identity + .clone() + .into()]), + ); + // Retrieve messages from Node B + node_C_saf_service_api.retrieve(None).unwrap(); + + pause(); + node_B_services.shutdown().unwrap(); + node_C_services.shutdown().unwrap(); + + // Restore PeerStorage of Node C and check that it is aware of Node A + pause(); + let node_C_peer_manager = + PeerManager::new(create_peer_storage(&node_C_tmpdir, node_C_database_name, vec![])).unwrap(); + assert!(node_C_peer_manager + .exists(&node_A_identity.identity.public_key) + .unwrap()); +} diff --git a/base_layer/p2p/tests/services/liveness.rs b/base_layer/p2p/tests/services/liveness.rs new file mode 100644 index 0000000000..0b40f1da34 --- /dev/null +++ b/base_layer/p2p/tests/services/liveness.rs @@ -0,0 +1,96 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support::TestCommsOutboundInitializer; +use futures::Sink; +use rand::rngs::OsRng; +use std::{ + sync::{mpsc, Arc}, + time::Duration, +}; +use tari_comms::{ + message::{DomainMessageContext, Message, MessageHeader}, + peer_manager::{NodeId, PeerNodeIdentity}, + pub_sub_channel::{pubsub_channel, TopicPayload}, + types::CommsPublicKey, +}; +use tari_crypto::keys::PublicKey; +use tari_p2p::{ + executor::StackBuilder, + services::{ + comms_outbound::CommsOutboundRequest, + liveness::{LivenessHandle, LivenessInitializer, LivenessRequest, LivenessResponse, PingPong}, + ServiceName, + }, + tari_message::{NetMessage, TariMessageType}, +}; +use tari_utilities::message_format::MessageFormat; +use tokio::runtime::Runtime; +use tower_service::Service; + +fn create_domain_message(message_type: TariMessageType, inner_msg: T) -> DomainMessageContext { + let mut rng = OsRng::new().unwrap(); + let (_, pk) = CommsPublicKey::random_keypair(&mut rng); + let peer_source = PeerNodeIdentity::new(NodeId::from_key(&pk).unwrap(), pk.clone()); + let header = MessageHeader::new(message_type).unwrap(); + let msg = Message::from_message_format(header, inner_msg).unwrap(); + DomainMessageContext::new(peer_source, pk, msg) +} + +/// Receive a Ping message and query the PingCount +#[test] +fn send_ping_query_count() { + let mut rt = Runtime::new().unwrap(); + let (mut publisher, subscriber) = pubsub_channel(2); + let (tx, rx) = mpsc::channel(); + + // Setup the stack + let stack = StackBuilder::new() + .add_initializer(LivenessInitializer::from_inbound_message_subscriber(Arc::new( + subscriber, + ))) + .add_initializer(TestCommsOutboundInitializer::new(tx)); + let handles = rt.block_on(stack.finish()).unwrap(); + + // Publish a Ping message + let msg = create_domain_message(TariMessageType::new(NetMessage::PingPong), PingPong::Ping); + let payload = TopicPayload::new(TariMessageType::new(NetMessage::PingPong), msg); + assert!(publisher.start_send(payload).unwrap().is_ready()); + + // Check that the CommsOutbound service received a SendMsg request + let outbound_req = rx.recv_timeout(Duration::from_millis(100)).unwrap(); + match outbound_req { + CommsOutboundRequest::SendMsg { .. } => {}, + _ => panic!("Unexpected request sent to comms outbound service"), + } + + // Query the ping count using the Liveness service handle + let mut liveness_handle = handles.get_handle::(ServiceName::Liveness).unwrap(); + let resp = rt + .block_on(liveness_handle.call(LivenessRequest::GetPingCount)) + .unwrap(); + + match resp.unwrap() { + LivenessResponse::Count(n) => assert_eq!(n, 1), + _ => panic!("unexpected response from liveness service"), + } +} diff --git a/infrastructure/merklemountainrange/tests/unit/mod.rs b/base_layer/p2p/tests/services/mod.rs similarity index 99% rename from infrastructure/merklemountainrange/tests/unit/mod.rs rename to base_layer/p2p/tests/services/mod.rs index 3599e682b2..d4870c341d 100644 --- a/infrastructure/merklemountainrange/tests/unit/mod.rs +++ b/base_layer/p2p/tests/services/mod.rs @@ -20,4 +20,4 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -mod mmr; +mod liveness; diff --git a/base_layer/p2p/tests/support/comms_outbound.rs b/base_layer/p2p/tests/support/comms_outbound.rs new file mode 100644 index 0000000000..a35b64499c --- /dev/null +++ b/base_layer/p2p/tests/support/comms_outbound.rs @@ -0,0 +1,56 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use futures::future; +use std::sync::mpsc; +use tari_p2p::{ + executor::{transport, ServiceInitializationError, ServiceInitializer}, + services::{ + comms_outbound::{CommsOutboundHandle, CommsOutboundRequest}, + ServiceHandlesFuture, + ServiceName, + }, +}; +use tower_util::service_fn; + +pub struct TestCommsOutboundInitializer { + sender: Option>, +} + +impl TestCommsOutboundInitializer { + pub fn new(sender: mpsc::Sender) -> Self { + Self { sender: Some(sender) } + } +} + +impl ServiceInitializer for TestCommsOutboundInitializer { + fn initialize(mut self: Box, handles: ServiceHandlesFuture) -> Result<(), ServiceInitializationError> { + let sender = self.sender.take().expect("cannot be None"); + let (oms_requester, oms_responder) = transport::channel(service_fn(move |req| { + sender.send(req).unwrap(); + future::ok::<_, ()>(Ok(())) + })); + tokio::spawn(oms_responder); + handles.insert(ServiceName::CommsOutbound, CommsOutboundHandle::new(oms_requester)); + Ok(()) + } +} diff --git a/base_layer/p2p/tests/support/mod.rs b/base_layer/p2p/tests/support/mod.rs new file mode 100644 index 0000000000..24f21cd025 --- /dev/null +++ b/base_layer/p2p/tests/support/mod.rs @@ -0,0 +1,55 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use rand::{distributions::Alphanumeric, rngs::OsRng, Rng}; +use std::{fmt::Debug, iter, thread, time::Duration}; + +pub fn random_string(len: usize) -> String { + let mut rng = OsRng::new().unwrap(); + iter::repeat(()).map(|_| rng.sample(Alphanumeric)).take(len).collect() +} + +pub fn assert_change(func: F, to: T, poll_count: usize) +where + F: Fn() -> T, + T: Eq + Debug, +{ + let mut i = 0; + loop { + let new_val = func(); + if new_val == to { + break; + } + + i += 1; + if i >= poll_count { + panic!( + "Value did not change to {:?} within {}ms (last value: {:?})", + to, + poll_count * 100, + new_val, + ); + } + + thread::sleep(Duration::from_millis(100)); + } +} diff --git a/base_layer/service_framework/Cargo.toml b/base_layer/service_framework/Cargo.toml new file mode 100644 index 0000000000..d74a50b7f0 --- /dev/null +++ b/base_layer/service_framework/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tari_service_framework" +version = "0.0.5" +authors = ["The Tari Development Community"] +repository = "https://github.com/tari-project/tari" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = "0.1.28" +tower-service = "0.2.0" +derive-error = "0.0.4" + +[dev-dependencies] +tokio-mock-task = "0.1.1" +tower-util = "0.1.0" +tokio = "0.1.22" diff --git a/base_layer/service_framework/src/handles/future.rs b/base_layer/service_framework/src/handles/future.rs new file mode 100644 index 0000000000..2fff75b053 --- /dev/null +++ b/base_layer/service_framework/src/handles/future.rs @@ -0,0 +1,132 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{LazyService, ServiceHandles}; +use futures::{task::AtomicTask, Async, Future, Poll}; +use std::{ + any::Any, + hash::Hash, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +/// Future which resolves to `ServiceHandles` once it is signaled to +/// do so. +pub struct ServiceHandlesFuture { + handles: Arc>, + is_ready: Arc, + task: Arc, +} + +impl Clone for ServiceHandlesFuture { + fn clone(&self) -> Self { + Self { + handles: Arc::clone(&self.handles), + is_ready: Arc::clone(&self.is_ready), + task: Arc::clone(&self.task), + } + } +} + +impl ServiceHandlesFuture +where N: Eq + Hash +{ + /// Create a new ServiceHandlesFuture with empty handles + pub fn new() -> Self { + Self { + handles: Arc::new(ServiceHandles::new()), + is_ready: Arc::new(AtomicBool::new(false)), + task: Arc::new(AtomicTask::new()), + } + } + + /// Insert a service handle with the given name + pub fn insert(&self, service_name: N, value: impl Any + Send + Sync) { + self.handles.insert(service_name, value); + } + + /// Retrieve a handle and downcast it to return type and return a copy, otherwise None is returned + pub fn get_handle(&self, service_name: N) -> Option + where V: Clone + 'static { + self.handles.get_handle(service_name) + } + + /// Call the given function with the final handles once this future is ready (`notify_ready` is called). + pub fn lazy_service(&self, service_fn: F) -> LazyService + where F: FnOnce(Arc>) -> S { + LazyService::new(self.clone(), service_fn) + } + + /// Notify that all handles are collected and the task should resolve + pub fn notify_ready(&self) { + self.is_ready.store(true, Ordering::SeqCst); + self.task.notify(); + } +} + +impl Future for ServiceHandlesFuture { + type Error = (); + type Item = Arc>; + + fn poll(&mut self) -> Poll { + if self.is_ready.load(Ordering::SeqCst) { + Ok(Async::Ready(Arc::clone(&self.handles))) + } else { + self.task.register(); + Ok(Async::NotReady) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use tokio_mock_task::MockTask; + + #[test] + fn insert_get() { + #[derive(Clone)] + struct TestHandle; + let handles = ServiceHandlesFuture::new(); + handles.insert(1, TestHandle); + handles.get_handle::(1).unwrap(); + assert!(handles.get_handle::<()>(1).is_none()); + assert!(handles.get_handle::<()>(2).is_none()); + } + + #[test] + fn notify_ready() { + let mut task = MockTask::new(); + task.enter(|| { + let mut handles = ServiceHandlesFuture::<()>::new(); + let mut clone = handles.clone(); + + assert!(handles.poll().unwrap().is_not_ready()); + assert!(clone.poll().unwrap().is_not_ready()); + handles.notify_ready(); + assert!(handles.poll().unwrap().is_ready()); + assert!(clone.poll().unwrap().is_ready()); + }) + } +} diff --git a/base_layer/service_framework/src/handles/lazy_service.rs b/base_layer/service_framework/src/handles/lazy_service.rs new file mode 100644 index 0000000000..9731077ab1 --- /dev/null +++ b/base_layer/service_framework/src/handles/lazy_service.rs @@ -0,0 +1,150 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use futures::{Future, Poll}; +use tower_service::Service; + +/// LazyService state +enum State { + Pending, + Ready(S), +} + +/// LazyService +/// +/// Implements the `tower_service::Service` trait. The `poll_ready` function will poll +/// the given future. Once that future is ready, the resulting value is passed into the +/// given function which must return a service. Subsequent calls to `poll_ready` and `call` +/// are delegated to that service. +/// +/// This is used by the `lazy_service` combinator in `ServiceHandlesFuture`. +pub struct LazyService { + future: F, + service_fn: Option, + state: State, +} + +impl LazyService { + /// Create a new LazyService + pub fn new(future: F, service_fn: TFn) -> Self { + Self { + future, + service_fn: Some(service_fn), + state: State::Pending, + } + } +} + +impl Service for LazyService +where + F: Future, + TFn: FnOnce(F::Item) -> S, + S: Service, +{ + type Error = S::Error; + type Future = S::Future; + type Response = S::Response; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + loop { + match self.state { + State::Pending => { + let item = try_ready!(self.future.poll()); + let service_fn = self + .service_fn + .take() + .expect("service_fn cannot be None in Pending state"); + self.state = State::Ready((service_fn)(item)); + }, + State::Ready(ref mut service) => { + return service.poll_ready(); + }, + } + } + } + + fn call(&mut self, req: TReq) -> Self::Future { + match self.state { + State::Pending => panic!("`Service::call` called before `Service::poll_ready` was ready"), + State::Ready(ref mut service) => service.call(req), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures::{ + future::{self, poll_fn}, + Async, + }; + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }; + use tower_util::service_fn; + + fn mock_fut(flag: Arc) -> impl Future { + poll_fn::<_, (), _>(move || { + if flag.load(Ordering::SeqCst) { + Ok(().into()) + } else { + Ok(Async::NotReady) + } + }) + } + + #[test] + fn ready_after_handles() { + let flag = Arc::new(AtomicBool::new(false)); + let fut = mock_fut(flag.clone()); + + let mut service = LazyService::new(fut, |_: ()| service_fn(|_: ()| future::ok::<_, ()>(()))); + + assert!(service.poll_ready().unwrap().is_not_ready()); + + flag.store(true, Ordering::SeqCst); + + assert!(service.poll_ready().unwrap().is_ready()); + } + + #[test] + fn call_after_ready() { + let flag = Arc::new(AtomicBool::new(true)); + let fut = mock_fut(flag.clone()); + let mut service = LazyService::new(fut, |_: ()| service_fn(|_: ()| future::ok::<_, ()>(()))); + + assert!(service.poll_ready().unwrap().is_ready()); + let mut fut = service.call(()); + assert!(fut.poll().unwrap().is_ready()); + } + + #[test] + #[should_panic] + fn call_before_ready() { + let flag = Arc::new(AtomicBool::new(false)); + let fut = mock_fut(flag.clone()); + let mut service = LazyService::new(fut, |_: ()| service_fn(|_: ()| future::ok::<_, ()>(()))); + assert!(service.poll_ready().unwrap().is_not_ready()); + let _ = service.call(()); + } +} diff --git a/base_layer/service_framework/src/handles/mod.rs b/base_layer/service_framework/src/handles/mod.rs new file mode 100644 index 0000000000..af06541c05 --- /dev/null +++ b/base_layer/service_framework/src/handles/mod.rs @@ -0,0 +1,90 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{any::Any, collections::HashMap, hash::Hash, sync::Mutex}; + +mod future; +mod lazy_service; + +pub use self::{future::ServiceHandlesFuture, lazy_service::LazyService}; + +/// This macro unlocks a Mutex or RwLock. If the lock is +/// poisoned (i.e. panic while unlocked) the last value +/// before the panic is used. +macro_rules! acquire_lock { + ($e:expr, $m:ident) => { + match $e.$m() { + Ok(lock) => lock, + Err(poisoned) => poisoned.into_inner(), + } + }; + ($e:expr) => { + acquire_lock!($e, lock) + }; +} + +/// Simple collection for named handles +pub struct ServiceHandles { + handles: Mutex>>, +} + +impl ServiceHandles +where N: Eq + Hash +{ + /// Create a new ServiceHandles + pub fn new() -> Self { + Self { + handles: Default::default(), + } + } + + /// Add a named ServiceHandle + pub fn insert(&self, service_name: N, value: impl Any + Send + Sync) { + acquire_lock!(self.handles).insert(service_name, Box::new(value)); + } + + /// Get a ServiceHandle and downcast it to a type `V`. If the item + /// does not exist or the downcast fails, `None` is returned. + pub fn get_handle(&self, service_name: N) -> Option + where V: Clone + 'static { + acquire_lock!(self.handles) + .get(&service_name) + .and_then(|b| b.downcast_ref::()) + .map(Clone::clone) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn service_handles_insert_get() { + #[derive(Clone)] + struct TestHandle; + let handles = ServiceHandles::new(); + handles.insert(1, TestHandle); + handles.get_handle::(1).unwrap(); + assert!(handles.get_handle::<()>(1).is_none()); + assert!(handles.get_handle::<()>(2).is_none()); + } +} diff --git a/base_layer/service_framework/src/lib.rs b/base_layer/service_framework/src/lib.rs new file mode 100644 index 0000000000..47e0d50268 --- /dev/null +++ b/base_layer/service_framework/src/lib.rs @@ -0,0 +1,33 @@ +//! Service framework +//! +//! This module contains the building blocks for async services. +//! +//! It consists of the following modules: +//! +//! - `builder`: contains the `MakeServicePair` trait which should be implemented by a service builder and the +//! `StackBuilder` struct which is responsible for building the service and making service _handles_ available to all +//! the other services. Handles are any object which is able to control a service in some way. Most commonly the +//! handle will be a `transport::Requester`. +//! +//! - `handles`: struct for collecting named handles for services. The `StackBuilder` uses this to make all handles +//! available to services. +//! +//! - `transport`: This allows messages to be reliably send/received to/from services. A `Requester`/`Responder` pair is +//! created using the `transport::channel` function which takes an impl of `tower_service::Service` as it's first +//! parameter. A `Requester` implements `tower_service::Service` and is used to send requests which return a Future +//! which resolves to a response. The `Requester` uses a `oneshot` channel allow responses to be sent back. A +//! `Responder` receives a `(request, oneshot::Sender)` tuple, calls the given tower service with that request and +//! sends the result on the `oneshot::Sender`. The `Responder` handles many requests simultaneously. + +// Used to eliminate the need for boxing futures in many cases. +// Tracking issue: https://github.com/rust-lang/rust/issues/63063 +#![feature(type_alias_impl_trait)] + +#[macro_use] +extern crate futures; + +pub mod handles; +mod stack; +pub mod transport; + +pub use self::stack::{ServiceInitializationError, ServiceInitializer, StackBuilder}; diff --git a/base_layer/service_framework/src/stack.rs b/base_layer/service_framework/src/stack.rs new file mode 100644 index 0000000000..1fb58b50cd --- /dev/null +++ b/base_layer/service_framework/src/stack.rs @@ -0,0 +1,140 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::handles::{ServiceHandles, ServiceHandlesFuture}; +use derive_error::Error; +use futures::{ + future::{self, Either}, + Future, +}; +use std::{hash::Hash, sync::Arc}; + +#[derive(Debug, Error)] +pub enum ServiceInitializationError { + #[error(msg_embedded, non_std, no_from)] + InvariantError(String), +} + +/// Builder trait for creating a service/handle pair. +/// The `StackBuilder` builds impls of this trait. +pub trait ServiceInitializer { + fn initialize(self: Box, handles: ServiceHandlesFuture) -> Result<(), ServiceInitializationError>; +} + +/// Implementation of MakeServicePair for any function taking a ServiceHandle and returning a (Handle, Future) pair. +impl ServiceInitializer for TFunc +where + N: Eq + Hash, + TFunc: FnOnce(ServiceHandlesFuture) -> Result<(), ServiceInitializationError>, +{ + fn initialize(self: Box, handles: ServiceHandlesFuture) -> Result<(), ServiceInitializationError> { + (self)(handles) + } +} + +/// Responsible for building and collecting handles and (usually long-running) service futures. +/// This can be converted into a future which resolves once all contained service futures are complete +/// by using the `IntoFuture` implementation. +pub struct StackBuilder { + initializers: Vec + Send>>, +} + +impl StackBuilder +where N: Eq + Hash +{ + pub fn new() -> Self { + Self { + initializers: Vec::new(), + } + } + + pub fn add_initializer(mut self, initializer: impl ServiceInitializer + Send + 'static) -> Self { + self.initializers.push(Box::new(initializer)); + self + } + + pub fn finish(self) -> impl Future>, Error = ServiceInitializationError> { + future::lazy(move || { + let handles = ServiceHandlesFuture::new(); + + for init in self.initializers.into_iter() { + if let Err(err) = init.initialize(handles.clone()) { + return Either::B(future::err(err)); + } + } + + handles.notify_ready(); + + Either::A(handles.map_err(|_| { + ServiceInitializationError::InvariantError("ServiceHandlesFuture cannot fail".to_string()) + })) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures::{future::poll_fn, Async}; + use std::sync::atomic::{AtomicBool, Ordering}; + + #[test] + fn service_stack_new() { + let state = Arc::new(AtomicBool::new(false)); + let state_inner = Arc::clone(&state.clone()); + let service_initializer = |handles: ServiceHandlesFuture<&'static str>| { + handles.insert("test-service", "Fake Handle"); + + let fut = poll_fn(move || { + // Test that this futures own handle is available + let fake_handle = handles.get_handle::<&str>(&"test-service").unwrap(); + assert_eq!(fake_handle, "Fake Handle"); + let not_found = handles.get_handle::<&str>(&"not-found"); + assert!(not_found.is_none()); + + // Any panics above won't fail the test so a marker bool is set + // if there are no panics. + // catch_unwind could be used but then the UnwindSafe trait bound + // needs to be added. TODO: handle panics in service poll functions + state_inner.store(true, Ordering::Release); + Ok(Async::Ready(())) + }); + + tokio::spawn(fut); + Ok(()) + }; + + tokio::run( + StackBuilder::new() + .add_initializer(service_initializer) + .finish() + .map(|_| ()) + .or_else(|err| { + panic!("{:?}", err); + #[allow(unreachable_code)] + future::err(()) + }), + ); + + assert!(state.load(Ordering::Acquire)) + } +} diff --git a/base_layer/service_framework/src/transport.rs b/base_layer/service_framework/src/transport.rs new file mode 100644 index 0000000000..fb71df7d93 --- /dev/null +++ b/base_layer/service_framework/src/transport.rs @@ -0,0 +1,439 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; +use futures::{ + stream::FuturesUnordered, + sync::{mpsc, oneshot}, + Async, + Future, + Poll, + Stream, +}; +use std::marker::PhantomData; +use tower_service::Service; + +/// Create a new Requester/Responder pair which wraps and calls the given service +pub fn channel(service: S) -> (Requester, Responder) +where S: Service { + let (tx, rx) = mpsc::unbounded(); + (Requester::new(tx), Responder::new(rx, service)) +} + +/// Receiver for a (Request, Reply) tuple, where Reply is a oneshot::Sender +pub type Rx = mpsc::UnboundedReceiver<(TReq, oneshot::Sender)>; +/// Sender for a (Request, Reply) tuple, where Reply is a oneshot::Sender +pub type Tx = mpsc::UnboundedSender<(TReq, oneshot::Sender)>; + +/// Requester is sends requests on a given `Tx` sender and returns a +/// AwaitResponseFuture which will resolve to the generic `TRes`. +/// +/// This should be used to make requests which require a response. +/// +/// This implements `tower_service::Service`, therefore the `poll_ready` and `call` +/// methods should be used to make a request. +pub struct Requester { + /// Used to send the request + tx: Tx, +} + +impl Requester { + /// Create a new Requester + pub fn new(tx: Tx) -> Self { + Self { tx } + } +} + +impl Clone for Requester { + fn clone(&self) -> Self { + Self { tx: self.tx.clone() } + } +} + +impl Service for Requester { + type Error = AwaitResponseError; + type Future = AwaitResponseFuture; + type Response = TRes; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(().into()) + } + + fn call(&mut self, request: TReq) -> Self::Future { + let (tx, rx) = oneshot::channel(); + + if self.tx.unbounded_send((request, tx)).is_ok() { + AwaitResponseFuture::new(rx) + } else { + // We're not able to send (rx closed) so return a future which resolves to + // a ChannelClosed error + AwaitResponseFuture::closed() + } + } +} + +#[derive(Debug, Error, Eq, PartialEq)] +pub enum AwaitResponseError { + /// Request was canceled + Canceled, + /// The response channel has closed + ChannelClosed, +} + +/// Response future for Results received over a given oneshot channel Receiver. +pub struct AwaitResponseFuture { + rx: Option>, +} + +impl AwaitResponseFuture { + /// Create a new AwaitResponseFuture + pub fn new(rx: oneshot::Receiver) -> Self { + Self { rx: Some(rx) } + } + + /// Create a closed AwaitResponseFuture. If this is polled + /// an RequestorError::ChannelClosed error is returned. + pub fn closed() -> Self { + Self { rx: None } + } +} + +impl Future for AwaitResponseFuture { + type Error = AwaitResponseError; + type Item = T; + + fn poll(&mut self) -> Poll { + match self.rx { + Some(ref mut rx) => rx.poll().map_err(|_| AwaitResponseError::Canceled), + None => Err(AwaitResponseError::ChannelClosed), + } + } +} + +/// This wraps an inner future and sends the result on a oneshot sender +pub struct ResponseFuture { + inner: F, + tx: Option>, + _err: PhantomData, +} + +impl ResponseFuture { + /// Create a new ResponderFuture from a Future and a oneshot Sender + pub fn new(inner: F, tx: oneshot::Sender) -> Self { + Self { + inner, + tx: Some(tx), + _err: PhantomData, + } + } +} + +impl Future for ResponseFuture +where F: Future +{ + type Error = E; + type Item = (); + + fn poll(&mut self) -> Poll { + match self + .tx + .as_mut() + .expect("ResponderFuture cannot be polled after inner future is ready") + .poll_cancel() + { + Err(_) | Ok(Async::Ready(_)) => { + // The receiver will not receive a response, + // so let's abandon processing the inner future + return Ok(().into()); + }, + _ => {}, + } + + // Progress on the inner future + let res = try_ready!(self.inner.poll()); + + let tx = self.tx.take().expect("cannot happen (ResponderFuture)"); + // Send the response + // If we get an error here, the receiver cancelled/closed so discard the Result + // TODO: Add tracing logs + let _ = tx.send(res); + Ok(().into()) + } +} + +/// Future that calls a given Service with requests that are received from a mpsc Receiver +/// and sends the response back on the requests oneshot channel. +/// +/// As requests come through the futures resulting from Service::call is added to a pending queue +/// for concurrent processing. +pub struct Responder +where S: Service +{ + service: S, + rx: Rx, + in_flight: usize, + pending: FuturesUnordered>, +} + +impl Responder +where S: Service +{ + /// Create a new Responder + pub fn new(rx: Rx, service: S) -> Self { + Self { + rx, + service, + in_flight: 0, + pending: FuturesUnordered::new(), + } + } +} + +impl Future for Responder +where S: Service +{ + type Error = (); + type Item = (); + + fn poll(&mut self) -> Poll { + loop { + if !self.pending.is_empty() { + loop { + // Make progress on the pending futures + match self.pending.poll() { + // Continue polling the pending futures until empty or NotReady + Ok(Async::Ready(Some(_))) => { + self.in_flight -= 1; + continue; + }, + Err(_) => { + // Service error occurred + // TODO: Deal with this error + self.in_flight -= 1; + continue; + }, + _ => break, + } + } + } + + // Check the service is ready + // TODO: Log an error returned from a service or deal with it in some way + match self.service.poll_ready().map_err(|_| ())? { + Async::Ready(_) => { + // Receive any new requests + match self.rx.poll().expect("poll error not possible for unbounded receiver") { + Async::Ready(Some((req, tx))) => { + // Call the service and add the resultant future to the pending queue + let fut = ResponseFuture::new(self.service.call(req), tx); + self.in_flight += 1; + self.pending.push(fut); + }, + // Stream has closed, so we're done + Async::Ready(None) => { + return Ok(Async::Ready(())); + }, + Async::NotReady => { + return Ok(Async::NotReady); + }, + } + }, + Async::NotReady => return Ok(Async::NotReady), + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures::{ + future::{self, Either}, + Async, + Stream, + }; + use std::{fmt::Debug, iter::repeat_with}; + use tokio_mock_task::MockTask; + use tower_util::service_fn; + + #[test] + fn await_response_future_new() { + let (tx, rx) = oneshot::channel::>(); + tx.send(Ok(())).unwrap(); + let mut fut = AwaitResponseFuture::new(rx); + match fut.poll().unwrap() { + Async::Ready(res) => assert!(res.is_ok()), + _ => panic!("expected future to be ready"), + } + } + + #[test] + fn await_response_future_closed() { + let mut fut = AwaitResponseFuture::<()>::closed(); + assert_eq!(fut.poll().unwrap_err(), AwaitResponseError::ChannelClosed); + } + + fn reply(mut rx: Rx>, msg: Result) + where + TResp: Debug, + TErr: Debug, + { + match rx.poll().unwrap() { + Async::Ready(Some((_, tx))) => { + tx.send(msg).unwrap(); + }, + _ => panic!("expected future to be ready"), + } + } + + #[test] + fn requestor_call() { + // task::current() used by unbounded channel + let mut task = MockTask::new(); + task.enter(|| { + let (tx, rx) = mpsc::unbounded(); + let mut requestor = Requester::<_, _>::new(tx); + + let mut fut = requestor.call("PING"); + assert!(fut.poll().unwrap().is_not_ready()); + reply::<_, _, ()>(rx, Ok("PONG")); + match fut.poll().unwrap() { + Async::Ready(Ok(msg)) => assert_eq!(msg, "PONG"), + _ => panic!("Unexpected poll result"), + } + }); + } + + #[test] + fn requestor_channel_closed() { + let (mut requestor, responder) = super::channel(service_fn(|_: ()| future::ok::<_, ()>(()))); + drop(responder); + + let mut fut = requestor.call(()); + assert_eq!(fut.poll().unwrap_err(), AwaitResponseError::ChannelClosed); + } + + #[test] + fn channel_request_response() { + let mut task = MockTask::new(); + task.enter(|| { + let (mut requestor, mut responder) = super::channel(service_fn(|_| future::ok::<_, ()>("PONG"))); + + let mut fut = requestor.call("PING"); + // Allow responder to receive the request and respond + let _ = responder.poll(); + match fut.poll().unwrap() { + Async::Ready(msg) => assert_eq!(msg, "PONG"), + Async::NotReady => panic!("expected future to be Ready"), + } + }); + } + + #[test] + fn channel_responder_inflight_out_of_order() { + let mut task = MockTask::new(); + task.enter(|| { + let (tx, rx) = oneshot::channel(); + struct EchoService(Option>); + impl Service for EchoService { + type Error = (); + type Future = impl Future; + type Response = String; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(().into()) + } + + fn call(&mut self, msg: String) -> Self::Future { + if let Some(rx) = self.0.take() { + Either::A(rx.map(|_| msg).map_err(|_| ())) + } else { + // Called more than once, return a future which resolves immediately + Either::B(future::ok(msg)) + } + } + } + + let service = EchoService(Some(rx)); + let (mut requestor, mut responder) = super::channel(service); + + // Make concurrent requests to the service + let mut fut1 = requestor.call("first".to_string()); + let mut fut2 = requestor.call("second".to_string()); + assert_eq!(responder.in_flight, 0); + + // When Responder is polled it will: + // Receive all the requests, + // call the service and then, + // poll pending futures (that is, response is sent to fut2) + responder.poll().unwrap(); + assert!(fut1.poll().unwrap().is_not_ready()); + match fut2.poll().unwrap() { + Async::Ready(v) => assert_eq!(v, "second"), + _ => panic!(), + } + assert_eq!(responder.in_flight, 1); + + // Signal the first call to be Ready so that the result is sent to fut1 + tx.send(()).unwrap(); + // Progress on the pending futures (i.e response for fut1) + responder.poll().unwrap(); + + match fut1.poll().unwrap() { + Async::Ready(v) => assert_eq!(v, "first"), + _ => panic!(), + } + }); + } + + #[test] + fn channel_responder_inflight() { + let mut task = MockTask::new(); + task.enter(|| { + let service = service_fn(|rx: oneshot::Receiver<()>| rx); + let (mut requestor, mut responder) = super::channel(service); + + // Make 100 concurrent requests + let (txs, futs): (Vec<_>, Vec<_>) = repeat_with(|| { + let (tx, rx) = oneshot::channel(); + (tx, requestor.call(rx)) + }) + .take(100) + .unzip(); + + // Call the service and collect the futures + responder.poll().unwrap(); + // Check that all are in-flight + assert_eq!(responder.in_flight, 100); + // Send the ready signal + for tx in txs.into_iter() { + tx.send(()).unwrap(); + } + // Ensure progress is made on tasks + responder.poll().unwrap(); + // Ensure we have no more unresolved requests + assert_eq!(responder.in_flight, 0); + // Check that all futures have completed + assert!(futs.into_iter().all(|mut f| f.poll().unwrap().is_ready())); + }); + } +} diff --git a/base_layer/wallet/Cargo.toml b/base_layer/wallet/Cargo.toml new file mode 100644 index 0000000000..44254d8702 --- /dev/null +++ b/base_layer/wallet/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tari_wallet" +version = "0.0.5" +edition = "2018" + +[dependencies] +tari_core = { path = "../core", version="^0.0"} +tari_crypto = { path = "../../infrastructure/crypto", version = "^0.0" } +tari_utilities = { path = "../../infrastructure/tari_util", version = "^0.0"} +tari_comms = { path = "../../comms", version = "^0.0"} +tari_p2p = {path = "../p2p", version = "^0.0"} +tari_key_manager = {path = "../keymanager", version = "^0.0"} +chrono = { version = "0.4.6", features = ["serde"]} +derive-error = "0.0.4" +digest = "0.8.0" +serde = {version = "1.0.89", features = ["derive"] } +crossbeam-channel = "0.3.8" +log = "0.4.6" +lmdb-zero = "0.4.4" +tari_storage = { version = "^0.0", path = "../../infrastructure/storage"} +diesel_migrations = "1.4" +diesel = {version="1.4", features = ["sqlite", "serde_json", "chrono"]} +rand = "0.5.5" + +[dev-dependencies] +simple_logger = "1.3.0" + diff --git a/base_layer/wallet/diesel.toml b/base_layer/wallet/diesel.toml new file mode 100644 index 0000000000..92267c829f --- /dev/null +++ b/base_layer/wallet/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" diff --git a/base_layer/wallet/migrations/2019-06-26-130555_initial/down.sql b/base_layer/wallet/migrations/2019-06-26-130555_initial/down.sql new file mode 100644 index 0000000000..9b7e8d3da1 --- /dev/null +++ b/base_layer/wallet/migrations/2019-06-26-130555_initial/down.sql @@ -0,0 +1,4 @@ +DROP TABLE IF EXISTS sent_messages; +DROP TABLE IF EXISTS received_messages; +DROP TABLE IF EXISTS contacts; +DROP TABLE IF EXISTS settings; \ No newline at end of file diff --git a/base_layer/wallet/migrations/2019-06-26-130555_initial/up.sql b/base_layer/wallet/migrations/2019-06-26-130555_initial/up.sql new file mode 100644 index 0000000000..23a1ed10ab --- /dev/null +++ b/base_layer/wallet/migrations/2019-06-26-130555_initial/up.sql @@ -0,0 +1,28 @@ +CREATE TABLE sent_messages ( + id TEXT PRIMARY KEY NOT NULL, + source_pub_key TEXT NOT NULL, + dest_pub_key TEXT NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME NOT NULL, + acknowledged INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY(dest_pub_key) REFERENCES contacts(pub_key) +); + +CREATE TABLE received_messages ( + id BLOB PRIMARY KEY NOT NULL, + source_pub_key TEXT NOT NULL, + dest_pub_key TEXT NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME NOT NULL +); + +CREATE TABLE contacts ( + pub_key TEXT PRIMARY KEY NOT NULL UNIQUE, + screen_name TEXT NOT NULL, + address TEXT NOT NULL +); + +CREATE TABLE settings ( + pub_key TEXT PRIMARY KEY NOT NULL, + screen_name TEXT NOT NULL +) \ No newline at end of file diff --git a/base_layer/wallet/src/lib.rs b/base_layer/wallet/src/lib.rs new file mode 100644 index 0000000000..f541eadca3 --- /dev/null +++ b/base_layer/wallet/src/lib.rs @@ -0,0 +1,17 @@ +#![feature(drain_filter)] + +#[macro_use] +extern crate diesel; +#[macro_use] +extern crate diesel_migrations; + +#[macro_use] +mod macros; +pub mod output_manager_service; +pub mod schema; +pub mod text_message_service; +pub mod transaction_service; +pub mod types; +pub mod wallet; + +pub use wallet::Wallet; diff --git a/base_layer/wallet/src/macros.rs b/base_layer/wallet/src/macros.rs new file mode 100644 index 0000000000..0be261e386 --- /dev/null +++ b/base_layer/wallet/src/macros.rs @@ -0,0 +1,33 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +macro_rules! acquire_lock { + ($e:expr, $m:ident) => { + match $e.$m() { + Ok(lock) => lock, + Err(poisoned) => poisoned.into_inner(), + } + }; + ($e:expr) => { + acquire_lock!($e, lock) + }; +} diff --git a/base_layer/wallet/src/output_manager_service/error.rs b/base_layer/wallet/src/output_manager_service/error.rs new file mode 100644 index 0000000000..8498d06638 --- /dev/null +++ b/base_layer/wallet/src/output_manager_service/error.rs @@ -0,0 +1,47 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; +use tari_core::transaction_protocol::TransactionProtocolError; +use tari_utilities::ByteArrayError; + +#[derive(Debug, Error, PartialEq)] +pub enum OutputManagerError { + #[error(msg_embedded, no_from, non_std)] + BuildError(String), + ByteArrayError(ByteArrayError), + TransactionProtocolError(TransactionProtocolError), + /// If an pending transaction does not exist to be confirmed + PendingTransactionNotFound, + /// Not all the transaction inputs and outputs are present to be confirmed + IncompleteTransaction, + /// Not enough funds to fulfill transaction + NotEnoughFunds, + /// Output already exists + DuplicateOutput, + /// Error sending a message to the public API + ApiSendFailed, + /// Error receiving a message from the publcic API + ApiReceiveFailed, + /// API returned something unexpected. + UnexpectedApiResponse, +} diff --git a/base_layer/blockchain/src/lib.rs b/base_layer/wallet/src/output_manager_service/mod.rs similarity index 94% rename from base_layer/blockchain/src/lib.rs rename to base_layer/wallet/src/output_manager_service/mod.rs index 88612fd10c..65e8e13e5f 100644 --- a/base_layer/blockchain/src/lib.rs +++ b/base_layer/wallet/src/output_manager_service/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2018 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -20,7 +20,5 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub mod blockchainstate; -pub mod chain; pub mod error; -pub mod store; +pub mod output_manager_service; diff --git a/base_layer/wallet/src/output_manager_service/output_manager_service.rs b/base_layer/wallet/src/output_manager_service/output_manager_service.rs new file mode 100644 index 0000000000..c83512dca9 --- /dev/null +++ b/base_layer/wallet/src/output_manager_service/output_manager_service.rs @@ -0,0 +1,676 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + output_manager_service::error::OutputManagerError, + types::{HashDigest, KeyDigest, TransactionRng}, +}; +use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; +use crossbeam_channel as channel; +use std::{collections::HashMap, sync::Mutex, time::Duration}; + +use log::*; +use std::sync::Arc; +use tari_core::{ + fee::Fee, + tari_amount::MicroTari, + transaction::{OutputFeatures, TransactionInput, TransactionOutput, UnblindedOutput}, + types::{PrivateKey, COMMITMENT_FACTORY, PROVER}, + SenderTransactionProtocol, +}; +use tari_crypto::keys::SecretKey; +use tari_key_manager::keymanager::KeyManager; +use tari_p2p::{ + sync_services::{ + Service, + ServiceApiWrapper, + ServiceContext, + ServiceControlMessage, + ServiceError, + DEFAULT_API_TIMEOUT_MS, + }, + tari_message::TariMessageType, +}; + +const LOG_TARGET: &'static str = "base_layer::wallet::output_manager_service"; + +/// This service will manage a wallet's available outputs and the key manager that produces the keys for these outputs. +/// The service will assemble transactions to be sent from the wallets available outputs and provide keys to receive +/// outputs. When the outputs are detected on the blockchain the Transaction service will call this Service to confirm +/// them to be moved to the spent and unspent output lists respectively. +pub struct OutputManagerService { + key_manager: Mutex>, + unspent_outputs: Vec, + spent_outputs: Vec, + pending_transactions: HashMap, + api: ServiceApiWrapper, +} + +impl OutputManagerService { + pub fn new(master_key: PrivateKey, branch_seed: String, primary_key_index: usize) -> OutputManagerService { + OutputManagerService { + key_manager: Mutex::new(KeyManager::::from( + master_key, + branch_seed, + primary_key_index, + )), + unspent_outputs: Vec::new(), + spent_outputs: Vec::new(), + pending_transactions: HashMap::new(), + api: Self::setup_api(), + } + } + + /// Return this service's API + pub fn get_api(&self) -> Arc { + self.api.get_api() + } + + fn setup_api() -> ServiceApiWrapper { + let (api_sender, service_receiver) = channel::bounded(0); + let (service_sender, api_receiver) = channel::bounded(0); + + let api = Arc::new(OutputManagerServiceApi::new(api_sender, api_receiver)); + ServiceApiWrapper::new(service_receiver, service_sender, api) + } + + /// Add an unblinded output to the unspent outputs list + pub fn add_output(&mut self, output: UnblindedOutput) -> Result<(), OutputManagerError> { + // Check it is not already present in the various output sets + if self.contains_output(&output) { + return Err(OutputManagerError::DuplicateOutput); + } + + self.unspent_outputs.push(output); + + Ok(()) + } + + pub fn get_balance(&self) -> MicroTari { + self.unspent_outputs + .iter() + .fold(MicroTari::from(0), |acc, x| acc + x.value) + } + + /// Request a spending key to be used to accept a transaction from a sender. + pub fn get_recipient_spending_key( + &mut self, + tx_id: u64, + amount: MicroTari, + ) -> Result + { + let mut km = acquire_lock!(self.key_manager); + + let key = km.next_key()?.k; + + self.pending_transactions.insert(tx_id, PendingTransactionOutputs { + tx_id, + outputs_to_be_spent: Vec::new(), + outputs_to_be_received: vec![UnblindedOutput { + value: amount, + spending_key: key.clone(), + features: OutputFeatures::default(), + }], + timestamp: Utc::now().naive_utc(), + }); + + Ok(key) + } + + /// Confirm the reception of an expect transaction output. This will be called by the Transaction Service when it + /// detects the output on the blockchain + pub fn confirm_received_transaction_output( + &mut self, + tx_id: u64, + received_output: &TransactionOutput, + ) -> Result<(), OutputManagerError> + { + let pending_transaction = self + .pending_transactions + .get_mut(&tx_id) + .ok_or(OutputManagerError::PendingTransactionNotFound)?; + + // Assumption: We are only allowing a single output per receiver in the current transaction protocols. + if pending_transaction.outputs_to_be_received.len() != 1 || + pending_transaction.outputs_to_be_received[0] + .as_transaction_input(&COMMITMENT_FACTORY, OutputFeatures::default()) + .commitment != + received_output.commitment + { + return Err(OutputManagerError::IncompleteTransaction); + } + + self.unspent_outputs + .append(&mut pending_transaction.outputs_to_be_received); + let _ = self.pending_transactions.remove(&tx_id); + Ok(()) + } + + /// Prepare a Sender Transaction Protocol for the amount and fee_per_gram specified. If required a change output + /// will be produced. + pub fn prepare_transaction_to_send( + &mut self, + amount: MicroTari, + fee_per_gram: MicroTari, + lock_height: Option, + ) -> Result + { + let mut rng = TransactionRng::new().unwrap(); + let outputs = self.select_outputs(amount, fee_per_gram, UTXOSelectionStrategy::Smallest)?; + let total = outputs.iter().fold(MicroTari::from(0), |acc, x| acc + x.value); + + let offset = PrivateKey::random(&mut rng); + let nonce = PrivateKey::random(&mut rng); + + let mut builder = SenderTransactionProtocol::builder(1); + builder + .with_lock_height(lock_height.unwrap_or(0)) + .with_fee_per_gram(fee_per_gram) + .with_offset(offset.clone()) + .with_private_nonce(nonce.clone()) + .with_amount(0, amount); + + for uo in outputs.iter() { + builder.with_input( + uo.as_transaction_input(&COMMITMENT_FACTORY, OutputFeatures::default()), + uo.clone(), + ); + } + + let fee_without_change = Fee::calculate(fee_per_gram, outputs.len(), 1); + let mut change_key: Option = None; + // If the input values > the amount to be sent + fees_without_change then we will need to include a change + // output + if total > amount + fee_without_change { + let mut km = acquire_lock!(self.key_manager); + let key = km.next_key()?.k; + change_key = Some(key.clone()); + builder.with_change_secret(key); + } + + let stp = builder + .build::(&PROVER, &COMMITMENT_FACTORY) + .map_err(|e| OutputManagerError::BuildError(e.message))?; + + // The Transaction Protocol built successfully so we will pull the unspent outputs out of the unspent list and + // store them until the transaction times out OR is confirmed + let outputs_to_be_spent = self + .unspent_outputs + .drain_filter(|uo| outputs.iter().any(|o| uo.spending_key == o.spending_key)) + .collect(); + + let mut pending_transaction = PendingTransactionOutputs { + tx_id: stp.get_tx_id()?, + outputs_to_be_spent, + outputs_to_be_received: Vec::new(), + timestamp: Utc::now().naive_utc(), + }; + + // If a change output was created add it to the pending_outputs list. + if let Some(key) = change_key { + pending_transaction.outputs_to_be_received.push(UnblindedOutput { + value: stp.get_amount_to_self()?, + spending_key: key, + features: OutputFeatures::default(), + }) + } + + self.pending_transactions + .insert(pending_transaction.tx_id, pending_transaction); + + Ok(stp) + } + + /// Confirm that a received or sent transaction and its outputs have been detected on the base chain. This will + /// usually be called by the Transaction Service which monitors the base chain. + pub fn confirm_sent_transaction( + &mut self, + tx_id: u64, + spent_outputs: &Vec, + received_outputs: &Vec, + ) -> Result<(), OutputManagerError> + { + let pending_transaction = self + .pending_transactions + .get_mut(&tx_id) + .ok_or(OutputManagerError::PendingTransactionNotFound)?; + + // Check that the set of TransactionInputs and TransactionOutputs provided contain all the spent and received + // outputs in the PendingTransaction + // Assumption: There will only be ONE extra output which belongs to the receiver + if spent_outputs.len() != pending_transaction.outputs_to_be_spent.len() || + !pending_transaction.outputs_to_be_spent.iter().fold(true, |acc, i| { + acc && spent_outputs.iter().any(|o| { + o.commitment == + i.as_transaction_input(&COMMITMENT_FACTORY, OutputFeatures::default()) + .commitment + }) + }) || + received_outputs.len() - 1 != pending_transaction.outputs_to_be_received.len() || + !pending_transaction.outputs_to_be_received.iter().fold(true, |acc, i| { + acc && received_outputs.iter().any(|o| { + o.commitment == + i.as_transaction_input(&COMMITMENT_FACTORY, OutputFeatures::default()) + .commitment + }) + }) + { + return Err(OutputManagerError::IncompleteTransaction); + } + + self.unspent_outputs + .append(&mut pending_transaction.outputs_to_be_received); + self.spent_outputs.append(&mut pending_transaction.outputs_to_be_spent); + let _ = self.pending_transactions.remove(&tx_id); + + Ok(()) + } + + /// Cancel a pending transaction and place the encumbered outputs back into the unspent pool + pub fn cancel_transaction(&mut self, tx_id: u64) -> Result<(), OutputManagerError> { + let pending_transaction = self + .pending_transactions + .get_mut(&tx_id) + .ok_or(OutputManagerError::PendingTransactionNotFound)?; + + self.unspent_outputs + .append(&mut pending_transaction.outputs_to_be_spent); + + Ok(()) + } + + /// Go through the pending transaction and if any have existed longer than the specified duration, cancel them + pub fn timeout_pending_transactions(&mut self, period: ChronoDuration) -> Result<(), OutputManagerError> { + let mut transactions_to_be_cancelled = Vec::new(); + for (tx_id, pt) in self.pending_transactions.iter() { + if pt.timestamp + period < Utc::now().naive_utc() { + transactions_to_be_cancelled.push(tx_id.clone()); + } + } + + for t in transactions_to_be_cancelled { + self.cancel_transaction(t.clone())? + } + + Ok(()) + } + + /// Select which outputs to use to send a transaction of the specified amount. Use the specified selection strategy + /// to choose the outputs + fn select_outputs( + &mut self, + amount: MicroTari, + fee_per_gram: MicroTari, + strategy: UTXOSelectionStrategy, + ) -> Result, OutputManagerError> + { + let mut outputs = Vec::new(); + let mut total = MicroTari::from(0); + let mut fee_without_change = MicroTari::from(0); + let mut fee_with_change = MicroTari::from(0); + + match strategy { + UTXOSelectionStrategy::Smallest => { + self.unspent_outputs.sort(); + for o in self.unspent_outputs.iter() { + outputs.push(o.clone()); + total += o.value.clone(); + // I am assuming that the only output will be the payment output and change if required + fee_without_change = Fee::calculate(fee_per_gram, outputs.len(), 1); + fee_with_change = Fee::calculate(fee_per_gram, outputs.len(), 2); + + if total == amount + fee_without_change || total >= amount + fee_with_change { + break; + } + } + }, + } + + if (total != amount + fee_without_change) && (total < amount + fee_with_change) { + return Err(OutputManagerError::NotEnoughFunds); + } + + Ok(outputs) + } + + pub fn pending_transactions(&self) -> &HashMap { + &self.pending_transactions + } + + pub fn spent_outputs(&self) -> &Vec { + &self.spent_outputs + } + + pub fn unspent_outputs(&self) -> &Vec { + &self.unspent_outputs + } + + /// Utility function to determine if an output exists in the spent, unspent or pending output sets + pub fn contains_output(&self, output: &UnblindedOutput) -> bool { + self.unspent_outputs + .iter() + .any(|o| o.value == output.value && o.spending_key == output.spending_key) || + self.spent_outputs + .iter() + .any(|o| o.value == output.value && o.spending_key == output.spending_key) || + self.pending_transactions.values().fold(false, |acc, pt| { + acc || pt + .outputs_to_be_spent + .iter() + .chain(pt.outputs_to_be_received.iter()) + .any(|o| o.value == output.value && o.spending_key == output.spending_key) + }) + } + + /// This handler is called when the Service executor loops receives an API request + fn handle_api_message(&mut self, msg: OutputManagerApiRequest) -> Result<(), ServiceError> { + debug!(target: LOG_TARGET, "[{}] Received API message", self.get_name(),); + let resp = match msg { + OutputManagerApiRequest::AddOutput(uo) => { + self.add_output(uo).map(|_| OutputManagerApiResponse::OutputAdded) + }, + OutputManagerApiRequest::GetBalance => Ok(OutputManagerApiResponse::Balance(self.get_balance())), + OutputManagerApiRequest::GetRecipientKey((tx_id, amount)) => self + .get_recipient_spending_key(tx_id, amount) + .map(|k| OutputManagerApiResponse::RecipientKeyGenerated(k)), + OutputManagerApiRequest::PrepareToSendTransaction((amount, fee_per_gram, lock_height)) => self + .prepare_transaction_to_send(amount, fee_per_gram, lock_height) + .map(|stp| OutputManagerApiResponse::TransactionToSend(stp)), + OutputManagerApiRequest::ConfirmReceivedOutput((tx_id, output)) => self + .confirm_received_transaction_output(tx_id, &output) + .map(|_| OutputManagerApiResponse::OutputConfirmed), + OutputManagerApiRequest::ConfirmSentTransaction((tx_id, spent_outputs, received_outputs)) => self + .confirm_sent_transaction(tx_id, &spent_outputs, &received_outputs) + .map(|_| OutputManagerApiResponse::TransactionConfirmed), + OutputManagerApiRequest::CancelTransaction(tx_id) => self + .cancel_transaction(tx_id) + .map(|_| OutputManagerApiResponse::TransactionCancelled), + OutputManagerApiRequest::TimeoutTransactions(period) => self + .timeout_pending_transactions(period) + .map(|_| OutputManagerApiResponse::TransactionsTimedOut), + }; + debug!(target: LOG_TARGET, "[{}] Replying to API", self.get_name()); + self.api + .send_reply(resp) + .map_err(ServiceError::internal_service_error()) + } +} + +/// Holds the outputs that have been selected for a given pending transaction waiting for confirmation +pub struct PendingTransactionOutputs { + pub tx_id: u64, + pub outputs_to_be_spent: Vec, + pub outputs_to_be_received: Vec, + pub timestamp: NaiveDateTime, +} + +/// Different UTXO selection strategies for choosing which UTXO's are used to fulfill a transaction +/// TODO Investigate and implement more optimal strategies +pub enum UTXOSelectionStrategy { + // Start from the smallest UTXOs and work your way up until the amount is covered. Main benefit is removing small + // UTXOs from the blockchain, con is that it costs more in fees + Smallest, +} + +/// The Domain Service trait implementation for the TestMessageService +impl Service for OutputManagerService { + fn get_name(&self) -> String { + "Output Manager service".to_string() + } + + fn get_message_types(&self) -> Vec { + Vec::new() + } + + /// Function called by the Service Executor in its own thread. This function polls for both API request and Comms + /// layer messages from the Message Broker + fn execute(&mut self, context: ServiceContext) -> Result<(), ServiceError> { + debug!(target: LOG_TARGET, "Starting Output Manager Service executor"); + loop { + if let Some(msg) = context.get_control_message(Duration::from_millis(5)) { + match msg { + ServiceControlMessage::Shutdown => break, + } + } else { + } + if let Some(msg) = self + .api + .recv_timeout(Duration::from_millis(50)) + .map_err(ServiceError::internal_service_error())? + { + self.handle_api_message(msg)?; + } + } + + Ok(()) + } +} + +/// API Request enum +#[derive(Debug)] +pub enum OutputManagerApiRequest { + GetBalance, + AddOutput(UnblindedOutput), + GetRecipientKey((u64, MicroTari)), + ConfirmReceivedOutput((u64, TransactionOutput)), + ConfirmSentTransaction((u64, Vec, Vec)), + PrepareToSendTransaction((MicroTari, MicroTari, Option)), + CancelTransaction(u64), + TimeoutTransactions(ChronoDuration), +} + +/// API Reply enum +#[derive(Debug)] +pub enum OutputManagerApiResponse { + Balance(MicroTari), + OutputAdded, + RecipientKeyGenerated(PrivateKey), + OutputConfirmed, + TransactionConfirmed, + TransactionToSend(SenderTransactionProtocol), + TransactionCancelled, + TransactionsTimedOut, +} + +/// Result for all API requests +pub type OutputManagerApiResult = Result; + +/// The Output Manager service public API that other services and application will use to interact with this service. +/// The requests and responses are transmitted via channels into the Service Executor thread where this service is +/// running +pub struct OutputManagerServiceApi { + sender: channel::Sender, + receiver: channel::Receiver, + mutex: Mutex<()>, + timeout: Duration, +} + +impl OutputManagerServiceApi { + fn new( + sender: channel::Sender, + receiver: channel::Receiver, + ) -> Self + { + Self { + sender, + receiver, + mutex: Mutex::new(()), + timeout: Duration::from_millis(DEFAULT_API_TIMEOUT_MS), + } + } + + pub fn add_output(&self, output: UnblindedOutput) -> Result<(), OutputManagerError> { + self.send_recv(OutputManagerApiRequest::AddOutput(output)) + .and_then(|resp| match resp { + OutputManagerApiResponse::OutputAdded => Ok(()), + _ => Err(OutputManagerError::UnexpectedApiResponse), + }) + } + + pub fn get_balance(&self) -> Result { + self.send_recv(OutputManagerApiRequest::GetBalance) + .and_then(|resp| match resp { + OutputManagerApiResponse::Balance(b) => Ok(b), + _ => Err(OutputManagerError::UnexpectedApiResponse), + }) + } + + pub fn get_recipient_spending_key(&self, tx_id: u64, amount: MicroTari) -> Result { + self.send_recv(OutputManagerApiRequest::GetRecipientKey((tx_id, amount))) + .and_then(|resp| match resp { + OutputManagerApiResponse::RecipientKeyGenerated(k) => Ok(k), + _ => Err(OutputManagerError::UnexpectedApiResponse), + }) + } + + pub fn prepare_transaction_to_send( + &self, + amount: MicroTari, + fee_per_gram: MicroTari, + lock_height: Option, + ) -> Result + { + self.send_recv(OutputManagerApiRequest::PrepareToSendTransaction(( + amount, + fee_per_gram, + lock_height, + ))) + .and_then(|resp| match resp { + OutputManagerApiResponse::TransactionToSend(stp) => Ok(stp), + _ => Err(OutputManagerError::UnexpectedApiResponse), + }) + } + + pub fn confirm_received_output(&self, tx_id: u64, output: TransactionOutput) -> Result<(), OutputManagerError> { + self.send_recv(OutputManagerApiRequest::ConfirmReceivedOutput((tx_id, output))) + .and_then(|resp| match resp { + OutputManagerApiResponse::OutputConfirmed => Ok(()), + _ => Err(OutputManagerError::UnexpectedApiResponse), + }) + } + + pub fn confirm_sent_transaction( + &self, + tx_id: u64, + spent_outputs: Vec, + received_outputs: Vec, + ) -> Result<(), OutputManagerError> + { + self.send_recv(OutputManagerApiRequest::ConfirmSentTransaction(( + tx_id, + spent_outputs, + received_outputs, + ))) + .and_then(|resp| match resp { + OutputManagerApiResponse::TransactionConfirmed => Ok(()), + _ => Err(OutputManagerError::UnexpectedApiResponse), + }) + } + + pub fn cancel_transaction(&self, tx_id: u64) -> Result<(), OutputManagerError> { + self.send_recv(OutputManagerApiRequest::CancelTransaction(tx_id)) + .and_then(|resp| match resp { + OutputManagerApiResponse::TransactionCancelled => Ok(()), + _ => Err(OutputManagerError::UnexpectedApiResponse), + }) + } + + pub fn timeout_transactions(&self, period: ChronoDuration) -> Result<(), OutputManagerError> { + self.send_recv(OutputManagerApiRequest::TimeoutTransactions(period)) + .and_then(|resp| match resp { + OutputManagerApiResponse::TransactionsTimedOut => Ok(()), + _ => Err(OutputManagerError::UnexpectedApiResponse), + }) + } + + fn send_recv(&self, msg: OutputManagerApiRequest) -> OutputManagerApiResult { + self.lock(|| -> OutputManagerApiResult { + self.sender.send(msg).map_err(|_| OutputManagerError::ApiSendFailed)?; + self.receiver + .recv_timeout(self.timeout.clone()) + .map_err(|_| OutputManagerError::ApiReceiveFailed)? + }) + } + + fn lock(&self, func: F) -> T + where F: FnOnce() -> T { + let lock = acquire_lock!(self.mutex); + let res = func(); + drop(lock); + res + } +} + +#[cfg(test)] +mod test { + use crate::output_manager_service::output_manager_service::{OutputManagerService, PendingTransactionOutputs}; + use chrono::Utc; + use rand::{CryptoRng, Rng, RngCore}; + use tari_core::{ + tari_amount::MicroTari, + transaction::UnblindedOutput, + types::{PrivateKey, PublicKey}, + }; + use tari_crypto::keys::{PublicKey as PublicKeyTrait, SecretKey}; + + fn make_output(rng: &mut R, val: MicroTari) -> UnblindedOutput { + let key = PrivateKey::random(rng); + UnblindedOutput::new(val, key, None) + } + + #[test] + fn test_contains_output_function() { + let mut rng = rand::OsRng::new().unwrap(); + let (secret_key, _public_key) = PublicKey::random_keypair(&mut rng); + + let mut oms = OutputManagerService::new(secret_key, "".to_string(), 0); + let mut balance = MicroTari::from(0); + for _i in 0..3 { + let uo = make_output(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + balance += uo.value.clone(); + oms.add_output(uo).unwrap(); + } + + let uo1 = make_output(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + + assert!(!oms.contains_output(&uo1)); + oms.add_output(uo1.clone()).unwrap(); + assert!(oms.contains_output(&uo1)); + + let uo2 = make_output(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + assert!(!oms.contains_output(&uo2)); + oms.spent_outputs.push(uo2.clone()); + assert!(oms.contains_output(&uo2)); + + let uo3 = make_output(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + assert!(!oms.contains_output(&uo3)); + oms.pending_transactions.insert(1, PendingTransactionOutputs { + tx_id: 1, + outputs_to_be_received: vec![uo3.clone()], + outputs_to_be_spent: Vec::new(), + timestamp: Utc::now().naive_utc(), + }); + assert!(oms.contains_output(&uo3)); + + assert_eq!(uo1.value + balance, oms.get_balance()); + } +} diff --git a/base_layer/wallet/src/schema.rs b/base_layer/wallet/src/schema.rs new file mode 100644 index 0000000000..c2917b6169 --- /dev/null +++ b/base_layer/wallet/src/schema.rs @@ -0,0 +1,39 @@ +table! { + contacts (pub_key) { + pub_key -> Text, + screen_name -> Text, + address -> Text, + } +} + +table! { + received_messages (id) { + id -> Binary, + source_pub_key -> Text, + dest_pub_key -> Text, + message -> Text, + timestamp -> Timestamp, + } +} + +table! { + sent_messages (id) { + id -> Text, + source_pub_key -> Text, + dest_pub_key -> Text, + message -> Text, + timestamp -> Timestamp, + acknowledged -> Integer, + } +} + +table! { + settings (pub_key) { + pub_key -> Text, + screen_name -> Text, + } +} + +joinable!(sent_messages -> contacts (dest_pub_key)); + +allow_tables_to_appear_in_same_query!(contacts, received_messages, sent_messages, settings,); diff --git a/base_layer/wallet/src/text_message_service/error.rs b/base_layer/wallet/src/text_message_service/error.rs new file mode 100644 index 0000000000..1e8d59007e --- /dev/null +++ b/base_layer/wallet/src/text_message_service/error.rs @@ -0,0 +1,67 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; +use diesel::result::{ConnectionError as DieselConnectionError, Error as DieselError}; +use tari_comms::{ + builder::CommsServicesError, + connection::NetAddressError, + dispatcher::DispatchError, + message::MessageError, + outbound_message_service::OutboundError, +}; +use tari_p2p::sync_services::ServiceError; +use tari_utilities::{hex::HexError, message_format::MessageFormatError}; + +#[derive(Debug, Error)] +pub enum TextMessageError { + MessageFormatError(MessageFormatError), + DispatchError(DispatchError), + MessageError(MessageError), + OutboundError(OutboundError), + ServiceError(ServiceError), + CommsServicesError(CommsServicesError), + HexError(HexError), + DatabaseError(DieselError), + NetAddressError(NetAddressError), + DatabaseConnectionError(DieselConnectionError), + /// If a received TextMessageAck doesn't matching any pending messages + MessageNotFound, + /// Failed to send from API + ApiSendFailed, + /// Failed to receive in API from service + ApiReceiveFailed, + /// The Outbound Message Service is not initialized + OMSNotInitialized, + /// The Comms service stack is not initialized + CommsNotInitialized, + /// Received an unexpected API response + UnexpectedApiResponse, + /// Contact not found + ContactNotFound, + /// Contact already exists + ContactAlreadyExists, + /// There was an error updating a row in the database + DatabaseUpdateError, + /// Error retrieving settings + SettingsReadError, +} diff --git a/base_layer/wallet/src/text_message_service/mod.rs b/base_layer/wallet/src/text_message_service/mod.rs new file mode 100644 index 0000000000..48e3067c7a --- /dev/null +++ b/base_layer/wallet/src/text_message_service/mod.rs @@ -0,0 +1,28 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod model; +mod service; + +pub use model::{Contact, ReceivedTextMessage, SentTextMessage, UpdateContact}; +pub use service::{TextMessageApiResponse, TextMessageService, TextMessageServiceApi, TextMessages}; diff --git a/base_layer/wallet/src/text_message_service/model.rs b/base_layer/wallet/src/text_message_service/model.rs new file mode 100644 index 0000000000..8e1607d3db --- /dev/null +++ b/base_layer/wallet/src/text_message_service/model.rs @@ -0,0 +1,730 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use chrono::{NaiveDateTime, Utc}; +use tari_comms::types::CommsPublicKey; + +use crate::{ + schema::{contacts, received_messages, sent_messages, settings}, + text_message_service::error::TextMessageError, + types::HashDigest, +}; + +use diesel::{dsl::count, prelude::*, query_dsl::RunQueryDsl, result::Error as DieselError, SqliteConnection}; +use digest::Digest; +use serde::{Deserialize, Serialize}; +use std::{ + cmp::Ordering, + convert::{TryFrom, TryInto}, +}; +use tari_comms::{ + connection::NetAddress, + message::{Message, MessageError}, +}; +use tari_p2p::tari_message::{ExtendedMessage, TariMessageType}; +use tari_utilities::{ + byte_array::ByteArray, + hex::{from_hex, Hex}, +}; + +/// This function generates a unique ID hash for a Text Message from the message components and an index integer +/// +/// `index`: This value should be incremented for every message sent to the same destination. This ensures that if you +/// send a duplicate message to the same destination that the ID hashes will be unique +pub fn generate_id( + source_pub_key: &CommsPublicKey, + dest_pub_key: &CommsPublicKey, + message: &String, + timestamp: &NaiveDateTime, + index: usize, +) -> Vec +{ + D::new() + .chain(source_pub_key.as_bytes()) + .chain(dest_pub_key.as_bytes()) + .chain(message.as_bytes()) + .chain(timestamp.to_string()) + .chain(index.to_le_bytes()) + .result() + .to_vec() +} + +/// Represents a single Text Message to be sent that includes an acknowledged field +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SentTextMessage { + pub id: Vec, + pub source_pub_key: CommsPublicKey, + pub dest_pub_key: CommsPublicKey, + pub message: String, + pub timestamp: NaiveDateTime, + pub acknowledged: bool, +} + +/// The Native Sql version of the SentTextMessage model +#[derive(Insertable, Queryable)] +#[table_name = "sent_messages"] +struct SentTextMessageSql { + pub id: String, + pub source_pub_key: String, + pub dest_pub_key: String, + pub message: String, + pub timestamp: NaiveDateTime, + pub acknowledged: i32, +} + +impl SentTextMessage { + /// Creates a new instance of a TextMessage to be sent + /// `source_pub_key`: The current node's pub_key (sender) + /// `dest_pub_key`: Recipient's pub key + /// `message`: The message to be sent + /// `index`: An index of how many messages have been sent to this recipient in order to ensure unique IDs. + pub fn new( + source_pub_key: CommsPublicKey, + dest_pub_key: CommsPublicKey, + message: String, + index: Option, + ) -> SentTextMessage + { + let timestamp = Utc::now().naive_utc(); + let id = generate_id::(&source_pub_key, &dest_pub_key, &message, ×tamp, index.unwrap_or(0)); + SentTextMessage { + id, + source_pub_key, + dest_pub_key, + message, + timestamp, + acknowledged: false, + } + } + + pub fn commit(&self, conn: &SqliteConnection) -> Result<(), TextMessageError> { + diesel::insert_into(sent_messages::table) + .values(SentTextMessageSql::from(self.clone())) + .execute(conn)?; + Ok(()) + } + + pub fn find(id: &Vec, conn: &SqliteConnection) -> Result { + SentTextMessage::try_from( + sent_messages::table + .filter(sent_messages::id.eq(id.to_hex())) + .first::(conn)?, + ) + } + + pub fn find_by_dest_pub_key( + dest_pub_key: &CommsPublicKey, + conn: &SqliteConnection, + ) -> Result, TextMessageError> + { + let messages = sent_messages::table + .filter(sent_messages::dest_pub_key.eq(dest_pub_key.to_hex())) + .order_by(sent_messages::timestamp) + .load::(conn)?; + let mut result: Vec = Vec::new(); + + for m in messages { + result.push(SentTextMessage::try_from(m)?); + } + + Ok(result) + } + + pub fn index(conn: &SqliteConnection) -> Result, TextMessageError> { + let messages = sent_messages::table.load::(conn)?; + let mut result: Vec = Vec::new(); + + for m in messages { + result.push(SentTextMessage::try_from(m)?); + } + + Ok(result) + } + + pub fn count_by_dest_pub_key( + dest_pub_key: &CommsPublicKey, + conn: &SqliteConnection, + ) -> Result + { + Ok(sent_messages::table + .filter(sent_messages::dest_pub_key.eq(dest_pub_key.to_hex())) + .select(count(sent_messages::dest_pub_key)) + .first(conn)?) + } + + pub fn mark_sent_message_ack(id: Vec, conn: &SqliteConnection) -> Result<(), TextMessageError> { + let num_updated = diesel::update(sent_messages::table.filter(sent_messages::id.eq(&id.to_hex()))) + .set(UpdateAckSentTextMessage { + acknowledged: Some(1i32), + }) + .execute(conn)?; + + if num_updated == 0 { + return Err(TextMessageError::DatabaseUpdateError); + } + + Ok(()) + } +} + +impl From for SentTextMessageSql { + fn from(msg: SentTextMessage) -> SentTextMessageSql { + SentTextMessageSql { + id: msg.id.to_hex(), + source_pub_key: msg.source_pub_key.to_hex(), + dest_pub_key: msg.dest_pub_key.to_hex(), + message: msg.message, + timestamp: msg.timestamp, + acknowledged: msg.acknowledged as i32, + } + } +} + +impl TryFrom for SentTextMessage { + type Error = TextMessageError; + + fn try_from(msg: SentTextMessageSql) -> Result { + Ok(SentTextMessage { + id: from_hex(msg.id.as_str())?, + source_pub_key: CommsPublicKey::from_hex(msg.source_pub_key.as_str())?, + dest_pub_key: CommsPublicKey::from_hex(msg.dest_pub_key.as_str())?, + message: msg.message, + timestamp: msg.timestamp, + acknowledged: msg.acknowledged != 0, + }) + } +} + +impl TryInto for SentTextMessage { + type Error = MessageError; + + fn try_into(self) -> Result { + (TariMessageType::new(ExtendedMessage::Text), self).try_into() + } +} + +/// The changeset to mark a SentTextMessage as acknowledged +#[derive(AsChangeset)] +#[table_name = "sent_messages"] +pub struct UpdateAckSentTextMessage { + pub acknowledged: Option, +} + +/// Represents a single received Text Message +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct ReceivedTextMessage { + pub id: Vec, + pub source_pub_key: CommsPublicKey, + pub dest_pub_key: CommsPublicKey, + pub message: String, + pub timestamp: NaiveDateTime, +} + +/// The Native Sql version of the TextMessage model +#[derive(Queryable, Insertable)] +#[table_name = "received_messages"] +struct ReceivedTextMessageSql { + pub id: Vec, + pub source_pub_key: String, + pub dest_pub_key: String, + pub message: String, + pub timestamp: NaiveDateTime, +} + +impl ReceivedTextMessage { + // Does not require new as these will only ever be received + pub fn commit(&self, conn: &SqliteConnection) -> Result<(), TextMessageError> { + diesel::insert_into(received_messages::table) + .values(ReceivedTextMessageSql::from(self.clone())) + .execute(conn)?; + Ok(()) + } + + pub fn index(conn: &SqliteConnection) -> Result, TextMessageError> { + let messages = received_messages::table.load::(conn)?; + let mut result: Vec = Vec::new(); + + for m in messages { + result.push(ReceivedTextMessage::try_from(m)?); + } + + Ok(result) + } + + pub fn find(id: &Vec, conn: &SqliteConnection) -> Result { + ReceivedTextMessage::try_from( + received_messages::table + .filter(received_messages::id.eq(id)) + .first::(conn)?, + ) + } + + pub fn find_by_source_pub_key( + source_pub_key: &CommsPublicKey, + conn: &SqliteConnection, + ) -> Result, TextMessageError> + { + let messages = received_messages::table + .filter(received_messages::source_pub_key.eq(source_pub_key.to_hex())) + .order_by(received_messages::timestamp) + .load::(conn)?; + let mut result: Vec = Vec::new(); + + for m in messages { + result.push(ReceivedTextMessage::try_from(m)?); + } + + Ok(result) + } +} + +impl From for ReceivedTextMessageSql { + fn from(msg: ReceivedTextMessage) -> ReceivedTextMessageSql { + ReceivedTextMessageSql { + id: msg.id, + source_pub_key: msg.source_pub_key.to_hex(), + dest_pub_key: msg.dest_pub_key.to_hex(), + message: msg.message, + timestamp: msg.timestamp, + } + } +} + +impl TryFrom for ReceivedTextMessage { + type Error = TextMessageError; + + fn try_from(msg: ReceivedTextMessageSql) -> Result { + Ok(ReceivedTextMessage { + id: msg.id, + source_pub_key: CommsPublicKey::from_hex(msg.source_pub_key.as_str())?, + dest_pub_key: CommsPublicKey::from_hex(msg.dest_pub_key.as_str())?, + message: msg.message, + timestamp: msg.timestamp, + }) + } +} + +impl From for SentTextMessage { + fn from(t: ReceivedTextMessage) -> SentTextMessage { + SentTextMessage { + id: t.id, + source_pub_key: t.source_pub_key, + dest_pub_key: t.dest_pub_key, + message: t.message, + timestamp: t.timestamp, + acknowledged: false, + } + } +} + +impl From for ReceivedTextMessage { + fn from(t: SentTextMessage) -> ReceivedTextMessage { + ReceivedTextMessage { + id: t.id, + source_pub_key: t.source_pub_key, + dest_pub_key: t.dest_pub_key, + message: t.message, + timestamp: t.timestamp, + } + } +} + +impl TryInto for ReceivedTextMessage { + type Error = MessageError; + + fn try_into(self) -> Result { + (TariMessageType::new(ExtendedMessage::Text), self).try_into() + } +} + +impl PartialOrd for ReceivedTextMessage { + /// Orders OutboundMessage from least to most time remaining from being scheduled + fn partial_cmp(&self, other: &Self) -> Option { + self.timestamp.partial_cmp(&other.timestamp) + } +} + +impl Ord for ReceivedTextMessage { + /// Orders OutboundMessage from least to most time remaining from being scheduled + fn cmp(&self, other: &Self) -> Ordering { + self.timestamp.cmp(&other.timestamp) + } +} + +/// A message service contact +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct Contact { + pub screen_name: String, + pub pub_key: CommsPublicKey, + pub address: NetAddress, +} + +/// The Native Sql version of the Contact model +#[derive(Queryable, Insertable)] +#[table_name = "contacts"] +struct ContactSql { + pub pub_key: String, + pub screen_name: String, + pub address: String, +} + +impl Contact { + pub fn new(screen_name: String, pub_key: CommsPublicKey, address: NetAddress) -> Contact { + Contact { + screen_name, + pub_key, + address, + } + } + + pub fn commit(&self, conn: &SqliteConnection) -> Result<(), TextMessageError> { + diesel::insert_into(contacts::table) + .values(ContactSql::from(self.clone())) + .execute(conn)?; + Ok(()) + } + + pub fn index(conn: &SqliteConnection) -> Result, TextMessageError> { + let contacts = contacts::table.load::(conn)?; + let mut result: Vec = Vec::new(); + + for c in contacts { + result.push(Contact::try_from(c)?); + } + + Ok(result) + } + + pub fn find(pub_key: &CommsPublicKey, conn: &SqliteConnection) -> Result { + Ok(Contact::try_from( + contacts::table + .filter(contacts::pub_key.eq(pub_key.to_hex())) + .first::(conn)?, + )?) + } + + pub fn update(self, updated_contact: UpdateContact, conn: &SqliteConnection) -> Result { + let num_updated = diesel::update(contacts::table.filter(contacts::pub_key.eq(&self.pub_key.to_hex()))) + .set(UpdateContactSql::from(updated_contact)) + .execute(conn)?; + + if num_updated == 0 { + return Err(TextMessageError::DatabaseUpdateError); + } + + Ok(Contact::find(&self.pub_key, conn)?) + } + + pub fn delete(self, conn: &SqliteConnection) -> Result<(), TextMessageError> { + let num_deleted = + diesel::delete(contacts::table.filter(contacts::pub_key.eq(&self.pub_key.to_hex()))).execute(conn)?; + if num_deleted == 0 { + return Err(TextMessageError::ContactNotFound); + } + Ok(()) + } +} + +impl From for ContactSql { + fn from(c: Contact) -> ContactSql { + ContactSql { + screen_name: c.screen_name, + pub_key: c.pub_key.to_hex(), + address: format!("{}", c.address), + } + } +} + +impl TryFrom for Contact { + type Error = TextMessageError; + + fn try_from(c: ContactSql) -> Result { + Ok(Contact { + screen_name: c.screen_name, + pub_key: CommsPublicKey::from_hex(c.pub_key.as_str())?, + address: c.address.parse()?, + }) + } +} + +/// The updatable fields of message contact +#[derive(Clone, Debug, PartialEq)] +pub struct UpdateContact { + pub screen_name: Option, + pub address: Option, +} + +/// The Native Sql version of the UpdateContact model +#[derive(AsChangeset)] +#[table_name = "contacts"] +struct UpdateContactSql { + pub screen_name: Option, + pub address: Option, +} + +impl From for UpdateContactSql { + fn from(c: UpdateContact) -> UpdateContactSql { + UpdateContactSql { + screen_name: c.screen_name, + address: c.address.map(|a| format!("{}", a)), + } + } +} + +/// Struct to hold the current settings for the +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TextMessageSettings { + pub pub_key: CommsPublicKey, + pub screen_name: String, +} + +#[derive(Debug, Queryable, Insertable)] +#[table_name = "settings"] +pub struct TextMessageSettingsSql { + pub_key: String, + screen_name: String, +} + +impl TextMessageSettings { + pub fn new(screen_name: String, pub_key: CommsPublicKey) -> TextMessageSettings { + TextMessageSettings { screen_name, pub_key } + } + + pub fn commit(&self, conn: &SqliteConnection) -> Result<(), TextMessageError> { + conn.transaction::<_, DieselError, _>(|| { + // There should only be one row in this table (until we support revisions) so first clean out the table + diesel::delete(settings::table).execute(conn)?; + + // And then insert + diesel::insert_into(settings::table) + .values(TextMessageSettingsSql::from(self.clone())) + .execute(conn)?; + + Ok(()) + })?; + + Ok(()) + } + + pub fn read(conn: &SqliteConnection) -> Result { + let read_settings = settings::table.load::(conn)?; + + let mut result: Vec = Vec::new(); + + for rs in read_settings { + result.push(TextMessageSettings::try_from(rs)?); + } + + if result.len() != 1 { + return Err(TextMessageError::SettingsReadError); + } + + Ok(result.remove(0)) + } +} + +impl From for TextMessageSettingsSql { + fn from(c: TextMessageSettings) -> TextMessageSettingsSql { + TextMessageSettingsSql { + screen_name: c.screen_name, + pub_key: c.pub_key.to_hex(), + } + } +} + +impl TryFrom for TextMessageSettings { + type Error = TextMessageError; + + fn try_from(c: TextMessageSettingsSql) -> Result { + Ok(TextMessageSettings { + screen_name: c.screen_name, + pub_key: CommsPublicKey::from_hex(c.pub_key.as_str())?, + }) + } +} + +#[cfg(test)] +mod test { + use crate::text_message_service::{ + model::{SentTextMessage, TextMessageSettings}, + Contact, + ReceivedTextMessage, + UpdateContact, + }; + use chrono::Utc; + use diesel::{Connection, SqliteConnection}; + use std::path::PathBuf; + use tari_comms::types::CommsPublicKey; + use tari_crypto::keys::PublicKey; + + fn get_path(name: Option<&str>) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name.unwrap_or("")); + path.to_str().unwrap().to_string() + } + + fn clean_up(name: &str) { + if std::fs::metadata(get_path(Some(name))).is_ok() { + std::fs::remove_file(get_path(Some(name))).unwrap(); + } + } + + fn init(name: &str) { + clean_up(name); + let path = get_path(None); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + } + + #[test] + fn db_model_tests() { + let mut rng = rand::OsRng::new().unwrap(); + let (_secret_key1, public_key1) = CommsPublicKey::random_keypair(&mut rng); + let (_secret_key2, public_key2) = CommsPublicKey::random_keypair(&mut rng); + let (_secret_key3, public_key3) = CommsPublicKey::random_keypair(&mut rng); + let (_secret_key4, public_key4) = CommsPublicKey::random_keypair(&mut rng); + + let db_name = "test.sqlite3"; + let db_path = get_path(Some(db_name)); + init(db_name); + + embed_migrations!("./migrations"); + let conn = SqliteConnection::establish(&db_path).unwrap_or_else(|_| panic!("Error connecting to {}", db_path)); + conn.execute("PRAGMA foreign_keys = ON").unwrap(); + + embedded_migrations::run_with_output(&conn, &mut std::io::stdout()).expect("Migration failed"); + + let _settings1 = TextMessageSettings::new("Bob".to_string(), public_key1.clone()).commit(&conn); + let read_settings1 = TextMessageSettings::read(&conn).unwrap(); + assert_eq!(read_settings1.screen_name, "Bob".to_string()); + let _settings2 = TextMessageSettings::new("Ed".to_string(), public_key1.clone()).commit(&conn); + let read_settings2 = TextMessageSettings::read(&conn).unwrap(); + assert_eq!(read_settings2.screen_name, "Ed".to_string()); + + let contact1 = Contact::new( + "Alice".to_string(), + public_key2.clone(), + "127.0.0.1:45532".parse().unwrap(), + ); + + contact1.commit(&conn).unwrap(); + + let contact2 = Contact::new( + "Bob".to_string(), + public_key3.clone(), + "127.0.0.1:45532".parse().unwrap(), + ); + + contact2.commit(&conn).unwrap(); + + let contact3 = Contact::new( + "Carol".to_string(), + public_key4.clone(), + "127.0.0.1:45537".parse().unwrap(), + ); + assert!(contact3.clone().delete(&conn).is_err()); + contact3.commit(&conn).unwrap(); + + let contacts = Contact::index(&conn).unwrap(); + + assert_eq!(contacts, vec![contact1.clone(), contact2.clone(), contact3.clone()]); + + let update = UpdateContact { + screen_name: Some("Carol".to_string()), + address: None, + }; + + let contact1 = contact1.update(update, &conn).unwrap(); + + let contacts = Contact::index(&conn).unwrap(); + + assert_eq!(contacts, vec![contact1.clone(), contact2.clone(), contact3.clone()]); + assert_eq!(contact2, Contact::find(&contact2.pub_key.clone(), &conn).unwrap()); + + contact3.delete(&conn).unwrap(); + let contacts = Contact::index(&conn).unwrap(); + + assert_eq!(contacts, vec![contact1.clone(), contact2.clone()]); + + assert!( + SentTextMessage::new(public_key1.clone(), public_key1.clone(), "Test1".to_string(), Some(0)) + .commit(&conn) + .is_err() + ); + + let sent_msg1 = SentTextMessage::new(public_key1.clone(), public_key2.clone(), "Test1".to_string(), Some(0)); + sent_msg1.commit(&conn).unwrap(); + let sent_msg2 = SentTextMessage::new(public_key1.clone(), public_key3.clone(), "Test2".to_string(), Some(0)); + sent_msg2.commit(&conn).unwrap(); + let sent_msg3 = SentTextMessage::new(public_key1.clone(), public_key3.clone(), "Test3".to_string(), Some(0)); + sent_msg3.commit(&conn).unwrap(); + + let sent_msgs = SentTextMessage::index(&conn).unwrap(); + assert_eq!(sent_msgs, vec![sent_msg1.clone(), sent_msg2.clone(), sent_msg3.clone()]); + let find1 = SentTextMessage::find(&sent_msg1.id, &conn).unwrap(); + assert_eq!(find1, sent_msg1); + let find2 = SentTextMessage::find_by_dest_pub_key(&public_key3.clone(), &conn).unwrap(); + assert_eq!(find2, vec![sent_msg2.clone(), sent_msg3.clone()]); + + let count = SentTextMessage::count_by_dest_pub_key(&public_key3.clone(), &conn).unwrap(); + assert_eq!(count, 2); + + assert!(SentTextMessage::mark_sent_message_ack(vec![2u8; 32], &conn).is_err()); + SentTextMessage::mark_sent_message_ack(sent_msg1.clone().id, &conn).unwrap(); + let find3 = SentTextMessage::find(&sent_msg1.id, &conn).unwrap(); + assert!(find3.acknowledged); + + let recv_msg1 = ReceivedTextMessage { + id: vec![1u8; 32], + source_pub_key: public_key1.clone(), + dest_pub_key: public_key2.clone(), + message: "recv1".to_string(), + timestamp: Utc::now().naive_utc(), + }; + recv_msg1.commit(&conn).unwrap(); + let recv_msg2 = ReceivedTextMessage { + id: vec![2u8; 32], + source_pub_key: public_key2.clone(), + dest_pub_key: public_key3.clone(), + message: "recv2".to_string(), + timestamp: Utc::now().naive_utc(), + }; + recv_msg2.commit(&conn).unwrap(); + let recv_msg3 = ReceivedTextMessage { + id: vec![3u8; 32], + source_pub_key: public_key2.clone(), + dest_pub_key: public_key3.clone(), + message: "recv3".to_string(), + timestamp: Utc::now().naive_utc(), + }; + recv_msg3.commit(&conn).unwrap(); + + let recv_msgs = ReceivedTextMessage::index(&conn).unwrap(); + assert_eq!(recv_msgs, vec![recv_msg1.clone(), recv_msg2.clone(), recv_msg3.clone()]); + let find1 = ReceivedTextMessage::find(&recv_msg1.id, &conn).unwrap(); + assert_eq!(find1, recv_msg1); + let find2 = ReceivedTextMessage::find_by_source_pub_key(&public_key2.clone(), &conn).unwrap(); + assert_eq!(find2, vec![recv_msg2, recv_msg3]); + + clean_up(db_name); + } +} diff --git a/base_layer/wallet/src/text_message_service/service.rs b/base_layer/wallet/src/text_message_service/service.rs new file mode 100644 index 0000000000..48b5774f8b --- /dev/null +++ b/base_layer/wallet/src/text_message_service/service.rs @@ -0,0 +1,664 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::text_message_service::{ + error::TextMessageError, + model::{ReceivedTextMessage, SentTextMessage}, + Contact, + UpdateContact, +}; +use crossbeam_channel as channel; +use diesel::{connection::Connection, SqliteConnection}; +use log::*; +use serde::{Deserialize, Serialize}; +use std::{ + convert::TryInto, + sync::{Arc, Mutex}, + time::Duration, +}; +use tari_comms::{ + domain_subscriber::{MessageInfo, SyncDomainSubscription}, + message::{Message, MessageError, MessageFlags}, + outbound_message_service::{outbound_message_service::OutboundMessageService, BroadcastStrategy}, + types::CommsPublicKey, +}; +use tari_p2p::{ + ping_pong::PingPong, + sync_services::{ + Service, + ServiceApiWrapper, + ServiceContext, + ServiceControlMessage, + ServiceError, + DEFAULT_API_TIMEOUT_MS, + }, + tari_message::{ExtendedMessage, TariMessageType}, +}; + +const LOG_TARGET: &'static str = "base_layer::wallet::text_messsage_service"; + +/// Represents an Acknowledgement of receiving a Text Message +#[derive(Debug, Serialize, Deserialize)] +pub struct TextMessageAck { + id: Vec, +} + +impl TryInto for TextMessageAck { + type Error = MessageError; + + fn try_into(self) -> Result { + Ok((TariMessageType::new(ExtendedMessage::TextAck), self).try_into()?) + } +} + +/// The TextMessageService manages the local node's text messages. It keeps track of sent messages that require an Ack +/// (pending messages), Ack'ed sent messages and received messages. +pub struct TextMessageService { + pub_key: CommsPublicKey, + screen_name: Option, + oms: Option>, + api: ServiceApiWrapper, + database_path: String, +} + +impl TextMessageService { + pub fn new(pub_key: CommsPublicKey, database_path: String) -> TextMessageService { + TextMessageService { + pub_key, + screen_name: None, + oms: None, + api: Self::setup_api(), + database_path, + } + } + + /// Return this service's API + pub fn get_api(&self) -> Arc { + self.api.get_api() + } + + fn setup_api() -> ServiceApiWrapper { + let (api_sender, service_receiver) = channel::bounded(0); + let (service_sender, api_receiver) = channel::bounded(0); + + let api = Arc::new(TextMessageServiceApi::new(api_sender, api_receiver)); + ServiceApiWrapper::new(service_receiver, service_sender, api) + } + + /// Send a text message to the specified node using the provided OMS + fn send_text_message( + &mut self, + dest_pub_key: CommsPublicKey, + message: String, + conn: &SqliteConnection, + ) -> Result<(), TextMessageError> + { + let oms = self.oms.clone().ok_or(TextMessageError::OMSNotInitialized)?; + + let count = SentTextMessage::count_by_dest_pub_key(&dest_pub_key.clone(), conn)?; + + let text_message = SentTextMessage::new(self.pub_key.clone(), dest_pub_key, message, Some(count as usize)); + + oms.send_message( + BroadcastStrategy::DirectPublicKey(text_message.dest_pub_key.clone()), + MessageFlags::ENCRYPTED, + text_message.clone(), + )?; + + text_message.commit(conn)?; + + trace!(target: LOG_TARGET, "Text Message Sent to {}", text_message.dest_pub_key); + + Ok(()) + } + + /// Process an incoming text message + fn receive_text_message( + &mut self, + info: MessageInfo, + message: ReceivedTextMessage, + conn: &SqliteConnection, + ) -> Result<(), TextMessageError> + { + let oms = self.oms.clone().ok_or(TextMessageError::OMSNotInitialized)?; + + trace!( + target: LOG_TARGET, + "Text Message received with ID: {:?} from {} with message: {:?}", + message.id.clone(), + message.source_pub_key, + message.message.clone() + ); + + let text_message_ack = TextMessageAck { id: message.clone().id }; + oms.send_message( + BroadcastStrategy::DirectPublicKey(info.origin_source), + MessageFlags::ENCRYPTED, + text_message_ack, + )?; + + message.commit(conn)?; + + Ok(()) + } + + /// Process an incoming text message Ack + fn receive_text_message_ack( + &mut self, + message_ack: TextMessageAck, + conn: &SqliteConnection, + ) -> Result<(), TextMessageError> + { + debug!( + target: LOG_TARGET, + "Text Message Ack received with ID: {:?}", + message_ack.id.clone(), + ); + SentTextMessage::mark_sent_message_ack(message_ack.id.clone(), conn)?; + + Ok(()) + } + + /// Return a copy of the current lists of messages + fn get_current_messages(&self, conn: &SqliteConnection) -> Result { + Ok(TextMessages { + sent_messages: SentTextMessage::index(conn)?, + received_messages: ReceivedTextMessage::index(conn)?, + }) + } + + fn get_current_messages_by_pub_key( + &self, + pub_key: CommsPublicKey, + conn: &SqliteConnection, + ) -> Result + { + Ok(TextMessages { + sent_messages: SentTextMessage::find_by_dest_pub_key(&pub_key, conn)?, + received_messages: ReceivedTextMessage::find_by_source_pub_key(&pub_key, conn)?, + }) + } + + pub fn get_pub_key(&self) -> CommsPublicKey { + self.pub_key.clone() + } + + pub fn set_pub_key(&mut self, pub_key: CommsPublicKey) { + self.pub_key = pub_key; + } + + pub fn get_screen_name(&self) -> Option { + self.screen_name.clone() + } + + pub fn set_screen_name(&mut self, screen_name: String) { + self.screen_name = Some(screen_name); + } + + pub fn add_contact(&mut self, contact: Contact, conn: &SqliteConnection) -> Result<(), TextMessageError> { + let found_contact = Contact::find(&contact.pub_key, conn); + if let Ok(c) = found_contact { + if c.pub_key == contact.pub_key { + return Err(TextMessageError::ContactAlreadyExists); + } + } + + contact.commit(&conn)?; + + // Send ping to the contact so that if they are online they will flush all outstanding messages for this node + let oms = self.oms.clone().ok_or(TextMessageError::OMSNotInitialized)?; + oms.send_message( + BroadcastStrategy::DirectPublicKey(contact.pub_key.clone()), + MessageFlags::empty(), + PingPong::Ping, + )?; + + trace!( + target: LOG_TARGET, + "Contact Added: Screen name: {:?} - Pub-key: {} - Address: {:?}", + contact.screen_name.clone(), + contact.pub_key.clone(), + contact.address.clone() + ); + Ok(()) + } + + pub fn remove_contact(&mut self, contact: Contact, conn: &SqliteConnection) -> Result<(), TextMessageError> { + contact.clone().delete(conn)?; + + trace!( + target: LOG_TARGET, + "Contact Deleted: Screen name: {:?} - Pub-key: {} - Address: {:?}", + contact.screen_name.clone(), + contact.pub_key.clone(), + contact.address.clone() + ); + + Ok(()) + } + + pub fn get_contacts(&self, conn: &SqliteConnection) -> Result, TextMessageError> { + Contact::index(conn) + } + + /// Updates the screen_name of a contact if an existing contact with the same pub_key is found + pub fn update_contact( + &mut self, + pub_key: CommsPublicKey, + contact_update: UpdateContact, + conn: &SqliteConnection, + ) -> Result<(), TextMessageError> + { + let contact = Contact::find(&pub_key, conn)?; + + contact.clone().update(contact_update, conn)?; + + trace!( + target: LOG_TARGET, + "Contact Updated: Screen name: {:?} - Pub-key: {} - Address: {:?}", + contact.screen_name.clone(), + contact.pub_key.clone(), + contact.address.clone() + ); + + Ok(()) + } + + /// This handler is called when the Service executor loops receives an API request + fn handle_api_message( + &mut self, + msg: TextMessageApiRequest, + connection: &SqliteConnection, + ) -> Result<(), ServiceError> + { + trace!(target: LOG_TARGET, "[{}] Received API message", self.get_name(),); + let resp = match msg { + TextMessageApiRequest::SendTextMessage((destination, message)) => self + .send_text_message(destination, message, connection) + .map(|_| TextMessageApiResponse::MessageSent), + TextMessageApiRequest::GetTextMessages => self + .get_current_messages(connection) + .map(|tm| TextMessageApiResponse::TextMessages(tm)), + TextMessageApiRequest::GetTextMessagesByPubKey(pk) => self + .get_current_messages_by_pub_key(pk, connection) + .map(|tm| TextMessageApiResponse::TextMessages(tm)), + TextMessageApiRequest::GetScreenName => Ok(TextMessageApiResponse::ScreenName(self.get_screen_name())), + TextMessageApiRequest::SetScreenName(s) => { + self.set_screen_name(s); + Ok(TextMessageApiResponse::ScreenNameSet) + }, + TextMessageApiRequest::AddContact(c) => self + .add_contact(c, connection) + .map(|_| TextMessageApiResponse::ContactAdded), + TextMessageApiRequest::RemoveContact(c) => self + .remove_contact(c, connection) + .map(|_| TextMessageApiResponse::ContactRemoved), + TextMessageApiRequest::GetContacts => self + .get_contacts(connection) + .map(|c| TextMessageApiResponse::Contacts(c)), + TextMessageApiRequest::UpdateContact((pk, c)) => self + .update_contact(pk, c, connection) + .map(|_| TextMessageApiResponse::ContactUpdated), + }; + + trace!(target: LOG_TARGET, "[{}] Replying to API", self.get_name()); + self.api + .send_reply(resp) + .map_err(ServiceError::internal_service_error()) + } + + // TODO Some sort of accessor that allows for pagination of messages +} + +/// A collection to hold a text message state +#[derive(Debug)] +pub struct TextMessages { + pub received_messages: Vec, + pub sent_messages: Vec, +} + +/// The Domain Service trait implementation for the TestMessageService +impl Service for TextMessageService { + fn get_name(&self) -> String { + "Text Message service".to_string() + } + + fn get_message_types(&self) -> Vec { + vec![ExtendedMessage::Text.into(), ExtendedMessage::TextAck.into()] + } + + /// Function called by the Service Executor in its own thread. This function polls for both API request and Comms + /// layer messages from the Message Broker + fn execute(&mut self, context: ServiceContext) -> Result<(), ServiceError> { + let mut subscription_text = SyncDomainSubscription::new( + context + .inbound_message_subscription_factory() + .get_subscription_fused(ExtendedMessage::Text.into()), + ); + let mut subscription_text_ack = SyncDomainSubscription::new( + context + .inbound_message_subscription_factory() + .get_subscription_fused(ExtendedMessage::TextAck.into()), + ); + + self.oms = Some(context.outbound_message_service()); + + // Check if the database file exists + let mut exists = false; + if std::fs::metadata(self.database_path.clone()).is_ok() { + exists = true; + } + + let connection = SqliteConnection::establish(&self.database_path) + .map_err(|e| ServiceError::ServiceInitializationFailed(format!("{}", e).to_string()))?; + + connection + .execute("PRAGMA foreign_keys = ON") + .map_err(|e| ServiceError::ServiceInitializationFailed(format!("{}", e).to_string()))?; + + if !exists { + embed_migrations!("./migrations"); + embedded_migrations::run_with_output(&connection, &mut std::io::stdout()).map_err(|e| { + ServiceError::ServiceInitializationFailed(format!("Database migration failed {}", e).to_string()) + })?; + } + + debug!(target: LOG_TARGET, "Starting Text Message Service executor"); + loop { + if let Some(msg) = context.get_control_message(Duration::from_millis(5)) { + match msg { + ServiceControlMessage::Shutdown => break, + } + } + for m in subscription_text.receive_messages()?.drain(..) { + match self.receive_text_message(m.0, m.1, &connection) { + Ok(_) => {}, + Err(err) => { + error!(target: LOG_TARGET, "Text Message service had error: {:?}", err); + }, + } + } + for m in subscription_text_ack.receive_messages()?.drain(..) { + match self.receive_text_message_ack(m.1, &connection) { + Ok(_) => {}, + Err(err) => { + error!(target: LOG_TARGET, "Text Message service had error: {:?}", err); + }, + } + } + if let Some(msg) = self + .api + .recv_timeout(Duration::from_millis(50)) + .map_err(ServiceError::internal_service_error())? + { + self.handle_api_message(msg, &connection)?; + } + } + + Ok(()) + } +} + +/// API Request enum +#[derive(Debug)] +pub enum TextMessageApiRequest { + SendTextMessage((CommsPublicKey, String)), + GetTextMessages, + GetTextMessagesByPubKey(CommsPublicKey), + SetScreenName(String), + GetScreenName, + AddContact(Contact), + RemoveContact(Contact), + GetContacts, + UpdateContact((CommsPublicKey, UpdateContact)), +} + +/// API Response enum +#[derive(Debug)] +pub enum TextMessageApiResponse { + MessageSent, + TextMessages(TextMessages), + ScreenName(Option), + ScreenNameSet, + ContactAdded, + ContactRemoved, + Contacts(Vec), + ContactUpdated, +} + +/// Result for all API requests +pub type TextMessageApiResult = Result; + +/// The TextMessage service public API that other services and application will use to interact with this service. +/// The requests and responses are transmitted via channels into the Service Executor thread where this service is +/// running +pub struct TextMessageServiceApi { + sender: channel::Sender, + receiver: channel::Receiver, + mutex: Mutex<()>, + timeout: Duration, +} + +impl TextMessageServiceApi { + fn new(sender: channel::Sender, receiver: channel::Receiver) -> Self { + Self { + sender, + receiver, + mutex: Mutex::new(()), + timeout: Duration::from_millis(DEFAULT_API_TIMEOUT_MS), + } + } + + pub fn send_text_message(&self, destination: CommsPublicKey, message: String) -> Result<(), TextMessageError> { + self.send_recv(TextMessageApiRequest::SendTextMessage((destination, message))) + .and_then(|resp| match resp { + TextMessageApiResponse::MessageSent => Ok(()), + _ => Err(TextMessageError::UnexpectedApiResponse), + }) + } + + pub fn get_text_messages(&self) -> Result { + self.send_recv(TextMessageApiRequest::GetTextMessages) + .and_then(|resp| match resp { + TextMessageApiResponse::TextMessages(msgs) => Ok(msgs), + _ => Err(TextMessageError::UnexpectedApiResponse), + }) + } + + pub fn get_text_messages_by_pub_key(&self, pub_key: CommsPublicKey) -> Result { + self.send_recv(TextMessageApiRequest::GetTextMessagesByPubKey(pub_key)) + .and_then(|resp| match resp { + TextMessageApiResponse::TextMessages(msgs) => Ok(msgs), + _ => Err(TextMessageError::UnexpectedApiResponse), + }) + } + + pub fn get_screen_name(&self) -> Result, TextMessageError> { + self.send_recv(TextMessageApiRequest::GetScreenName) + .and_then(|resp| match resp { + TextMessageApiResponse::ScreenName(s) => Ok(s), + _ => Err(TextMessageError::UnexpectedApiResponse), + }) + } + + pub fn set_screen_name(&self, screen_name: String) -> Result<(), TextMessageError> { + self.send_recv(TextMessageApiRequest::SetScreenName(screen_name)) + .and_then(|resp| match resp { + TextMessageApiResponse::ScreenNameSet => Ok(()), + _ => Err(TextMessageError::UnexpectedApiResponse), + }) + } + + pub fn add_contact(&self, contact: Contact) -> Result<(), TextMessageError> { + self.send_recv(TextMessageApiRequest::AddContact(contact)) + .and_then(|resp| match resp { + TextMessageApiResponse::ContactAdded => Ok(()), + _ => Err(TextMessageError::UnexpectedApiResponse), + }) + } + + pub fn remove_contact(&self, contact: Contact) -> Result<(), TextMessageError> { + self.send_recv(TextMessageApiRequest::RemoveContact(contact)) + .and_then(|resp| match resp { + TextMessageApiResponse::ContactRemoved => Ok(()), + _ => Err(TextMessageError::UnexpectedApiResponse), + }) + } + + pub fn get_contacts(&self) -> Result, TextMessageError> { + self.send_recv(TextMessageApiRequest::GetContacts) + .and_then(|resp| match resp { + TextMessageApiResponse::Contacts(v) => Ok(v), + _ => Err(TextMessageError::UnexpectedApiResponse), + }) + } + + pub fn update_contact(&self, pub_key: CommsPublicKey, contact: UpdateContact) -> Result<(), TextMessageError> { + self.send_recv(TextMessageApiRequest::UpdateContact((pub_key, contact))) + .and_then(|resp| match resp { + TextMessageApiResponse::ContactUpdated => Ok(()), + _ => Err(TextMessageError::UnexpectedApiResponse), + }) + } + + fn send_recv(&self, msg: TextMessageApiRequest) -> TextMessageApiResult { + self.lock(|| -> TextMessageApiResult { + self.sender.send(msg).map_err(|_| TextMessageError::ApiSendFailed)?; + self.receiver + .recv_timeout(self.timeout.clone()) + .map_err(|_| TextMessageError::ApiReceiveFailed)? + }) + } + + fn lock(&self, func: F) -> T + where F: FnOnce() -> T { + let lock = acquire_lock!(self.mutex); + let res = func(); + drop(lock); + res + } +} + +#[cfg(test)] +mod test { + use crate::{ + diesel::Connection, + text_message_service::{error::TextMessageError, Contact, TextMessageService, UpdateContact}, + }; + use diesel::{result::Error as DieselError, SqliteConnection}; + use std::path::PathBuf; + use tari_comms::types::CommsPublicKey; + use tari_crypto::keys::PublicKey; + + fn get_path(name: Option<&str>) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name.unwrap_or("")); + path.to_str().unwrap().to_string() + } + + fn clean_up(name: &str) { + if std::fs::metadata(get_path(Some(name))).is_ok() { + std::fs::remove_file(get_path(Some(name))).unwrap(); + } + } + + fn init(name: &str) { + clean_up(name); + let path = get_path(None); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + } + + #[test] + fn test_contacts_crud() { + let mut rng = rand::OsRng::new().unwrap(); + + let (_secret_key, public_key) = CommsPublicKey::random_keypair(&mut rng); + + let db_name = "test_crud.sqlite3"; + let db_path = get_path(Some(db_name)); + init(db_name); + + let conn = SqliteConnection::establish(&db_path).unwrap(); + embed_migrations!("./migrations"); + embedded_migrations::run_with_output(&conn, &mut std::io::stdout()).expect("Migration failed"); + + let mut tms = TextMessageService::new(public_key, db_path); + + let mut contacts = Vec::new(); + + let screen_names = vec![ + "Alice".to_string(), + "Bob".to_string(), + "Carol".to_string(), + "Dave".to_string(), + "Eric".to_string(), + ]; + for i in 0..5 { + let (_contact_secret_key, contact_public_key) = CommsPublicKey::random_keypair(&mut rng); + contacts.push(Contact::new( + screen_names[i].clone(), + contact_public_key, + "127.0.0.1:12345".parse().unwrap(), + )); + } + + assert_eq!(tms.get_screen_name(), None); + tms.set_screen_name("Fred".to_string()); + assert_eq!(tms.get_screen_name(), Some("Fred".to_string())); + + for c in contacts.iter() { + let _ = tms.add_contact(c.clone(), &conn); + } + + assert_eq!(tms.get_contacts(&conn).unwrap().len(), 5); + + tms.remove_contact(contacts[0].clone(), &conn).unwrap(); + + assert_eq!(tms.get_contacts(&conn).unwrap().len(), 4); + + let update_contact = UpdateContact { + screen_name: Some("Betty".to_string()), + address: Some(contacts[1].address.clone()), + }; + + tms.update_contact(contacts[1].pub_key.clone(), update_contact, &conn) + .unwrap(); + + let updated_contacts = tms.get_contacts(&conn).unwrap(); + assert_eq!(updated_contacts[0].screen_name, "Betty".to_string()); + + match tms.update_contact( + CommsPublicKey::default(), + UpdateContact { + screen_name: Some("Whatever".to_string()), + address: Some("127.0.0.1:12345".parse().unwrap()), + }, + &conn, + ) { + Err(TextMessageError::DatabaseError(DieselError::NotFound)) => assert!(true), + _ => assert!(false), + } + + clean_up(db_name); + } +} diff --git a/base_layer/wallet/src/transaction_manager.rs b/base_layer/wallet/src/transaction_manager.rs new file mode 100644 index 0000000000..9e7423e076 --- /dev/null +++ b/base_layer/wallet/src/transaction_manager.rs @@ -0,0 +1,611 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE + +use derive_error::Error; +use std::collections::HashMap; +use tari_core::{ + transaction::{KernelFeatures, OutputFeatures, Transaction}, + transaction_protocol::{ + recipient::RecipientSignedTransactionData, + sender::SenderMessage, + TransactionProtocolError, + }, + types::{CommitmentFactory, PrivateKey, RangeProofService}, + ReceiverTransactionProtocol, + SenderTransactionProtocol, +}; + +#[derive(Debug, Error, PartialEq)] +pub enum TransactionManagerError { + // Transaction protocol is not in the correct state for this operation + InvalidStateError, + // Transaction Protocol Error + TransactionProtocolError(TransactionProtocolError), + // The message being process is not recognized by the Transaction Manager + InvalidMessageTypeError, + // A message for a specific tx_id has been repeated + RepeatedMessageError, + // A recipient reply was received for a non-existent tx_id + TransactionDoesNotExistError, +} + +/// TransactionManager allows for the management of multiple inbound and outbound transaction protocols +/// which are uniquely identified by a tx_id. The TransactionManager generates and accepts the various protocol +/// messages and applies them to the appropriate protocol instances based on the tx_id. +/// The TransactionManager allows for the sending of transactions to single receivers, when the appropriate recipient +/// response is handled the transaction is completed and moved to the completed_transaction buffer. +/// The TransactionManager will accept inbound transactions and generate a reply. Received transactions will remain +/// in the pending_inbound_transactions buffer. +/// TODO Allow for inbound transactions that are detected on the blockchain to be marked as complete. +/// +/// # Fields +/// `pending_outbound_transactions` - List of transaction protocols sent by this client and waiting response from the +/// recipient +/// +/// `pending_inbound_transactions` - List of transaction protocols that have been received and responded to. +/// +/// +/// `completed_transaction` - List of sent transactions that have been responded to and are completed. +#[derive(Default)] +pub struct TransactionManager { + pending_outbound_transactions: HashMap, + pending_inbound_transactions: HashMap, + completed_transactions: HashMap, +} + +impl TransactionManager { + pub fn new() -> TransactionManager { + TransactionManager { + pending_outbound_transactions: HashMap::new(), + pending_inbound_transactions: HashMap::new(), + completed_transactions: HashMap::new(), + } + } + + /// Start to send a new transaction. + /// # Arguments + /// 'sender_transaction_protocol' - A well formed SenderTransactionProtocol ready to generate the SenderMessage. + /// + /// # Returns + /// Public SenderMessage to be transmitted to the recipient. + pub fn start_send_transaction( + &mut self, + mut sender_transaction_protocol: SenderTransactionProtocol, + ) -> Result + { + if !sender_transaction_protocol.is_single_round_message_ready() { + return Err(TransactionManagerError::InvalidStateError); + } + + let msg = sender_transaction_protocol.build_single_round_message()?; + + self.pending_outbound_transactions + .insert(msg.tx_id, sender_transaction_protocol); + + Ok(SenderMessage::Single(Box::new(msg))) + } + + /// Accept the public reply from a recipient and apply the reply to the relevant transaction protocol + /// # Arguments + /// 'recipient_reply' - The public response from a recipient with data required to complete the transaction + pub fn accept_recipient_reply( + &mut self, + recipient_reply: RecipientSignedTransactionData, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result<(), TransactionManagerError> + { + let mut marked_for_removal = None; + + for (&tx_id, stp) in self.pending_outbound_transactions.iter_mut() { + let recp_tx_id = recipient_reply.tx_id; + if stp.check_tx_id(recp_tx_id) && stp.is_collecting_single_signature() { + stp.add_single_recipient_info(recipient_reply, prover)?; + stp.finalize(KernelFeatures::empty(), prover, factory)?; + let tx = stp.get_transaction()?; + self.completed_transactions.insert(recp_tx_id, tx.clone()); + marked_for_removal = Some(tx_id); + break; + } + } + + if marked_for_removal.is_none() { + return Err(TransactionManagerError::TransactionDoesNotExistError); + } + + if let Some(tx_id) = marked_for_removal { + self.pending_outbound_transactions.remove(&tx_id); + } + + Ok(()) + } + + /// Accept a new transaction from a sender by handling a public SenderMessage + /// # Arguments + /// 'sender_message' - Message from a sender containing the setup of the transaction being sent to you + /// 'nonce' - Your chosen nonce for your signature of this transaction + /// 'spending_key' - Your chosen secret_key for this transaction + /// # Returns + /// Public reply message to be sent back to the sender. + pub fn accept_transaction( + &mut self, + sender_message: SenderMessage, + nonce: PrivateKey, + spending_key: PrivateKey, + prover: &RangeProofService, + factory: &CommitmentFactory, + ) -> Result + { + let rtp = ReceiverTransactionProtocol::new( + sender_message, + nonce, + spending_key, + OutputFeatures::empty(), + prover, + factory, + ); + let recipient_reply = rtp.get_signed_data()?.clone(); + + // Check this is not a repeat message i.e. tx_id doesn't already exist in our pending or completed transactions + if self.pending_outbound_transactions.contains_key(&recipient_reply.tx_id) { + return Err(TransactionManagerError::RepeatedMessageError); + } + + if self.pending_inbound_transactions.contains_key(&recipient_reply.tx_id) { + return Err(TransactionManagerError::RepeatedMessageError); + } + + if self.completed_transactions.contains_key(&recipient_reply.tx_id) { + return Err(TransactionManagerError::RepeatedMessageError); + } + + // Otherwise add it to our pending transaction list and return reply + self.pending_inbound_transactions.insert(recipient_reply.tx_id, rtp); + + Ok(recipient_reply) + } + + /// Returns the list of the completed transactions + pub fn get_completed_transactions(&self) -> &HashMap { + &self.completed_transactions + } + + pub fn num_pending_inbound_transactions(&self) -> usize { + self.pending_inbound_transactions.len() + } + + pub fn num_pending_outbound_transactions(&self) -> usize { + self.pending_outbound_transactions.len() + } +} + +#[cfg(test)] +mod test { + use crate::transaction_manager::{TransactionManager, TransactionManagerError}; + use rand::{CryptoRng, OsRng, Rng}; + use tari_core::{ + tari_amount::*, + transaction::{OutputFeatures, TransactionInput, UnblindedOutput}, + transaction_protocol::{sender::SenderMessage, TransactionProtocolError}, + types::{PrivateKey, PublicKey, RangeProof, COMMITMENT_FACTORY, PROVER}, + SenderTransactionProtocol, + }; + use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + common::Blake256, + keys::{PublicKey as PK, SecretKey as SK}, + }; + use tari_utilities::ByteArray; + + pub struct TestParams { + pub spend_key: PrivateKey, + pub change_key: PrivateKey, + pub offset: PrivateKey, + pub nonce: PrivateKey, + pub public_nonce: PublicKey, + } + + impl TestParams { + pub fn new(rng: &mut R) -> TestParams { + let r = PrivateKey::random(rng); + TestParams { + spend_key: PrivateKey::random(rng), + change_key: PrivateKey::random(rng), + offset: PrivateKey::random(rng), + public_nonce: PublicKey::from_secret_key(&r), + nonce: r, + } + } + } + + pub fn make_input(rng: &mut R, val: MicroTari) -> (TransactionInput, UnblindedOutput) { + let key = PrivateKey::random(rng); + let commitment = COMMITMENT_FACTORY.commit_value(&key, val.into()); + let input = TransactionInput::new(OutputFeatures::empty(), commitment); + (input, UnblindedOutput::new(val, key, None)) + } + + #[test] + fn manage_single_transaction() { + let mut rng = OsRng::new().unwrap(); + // Alice's parameters + let a = TestParams::new(&mut rng); + // Bob's parameters + let b = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(2500)); + let mut builder = SenderTransactionProtocol::builder(1); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a.offset.clone()) + .with_private_nonce(a.nonce.clone()) + .with_change_secret(a.change_key.clone()) + .with_input(utxo.clone(), input) + .with_amount(0, MicroTari(500)); + let alice_stp = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + + let mut alice_tx_manager = TransactionManager::new(); + let mut bob_tx_manager = TransactionManager::new(); + + let send_msg = alice_tx_manager.start_send_transaction(alice_stp).unwrap(); + let mut tx_id = 0; + if let SenderMessage::Single(single_round_sender_data) = send_msg.clone() { + tx_id = single_round_sender_data.tx_id; + } + + assert_eq!(alice_tx_manager.num_pending_outbound_transactions(), 1); + + let receive_msg = bob_tx_manager + .accept_transaction(send_msg, b.nonce, b.spend_key, &PROVER, &COMMITMENT_FACTORY) + .unwrap(); + + assert_eq!(bob_tx_manager.num_pending_inbound_transactions(), 1); + + alice_tx_manager + .accept_recipient_reply(receive_msg, &PROVER, &COMMITMENT_FACTORY) + .unwrap(); + + let txs = alice_tx_manager.get_completed_transactions(); + + assert_eq!(txs.len(), 1); + assert_eq!(alice_tx_manager.num_pending_outbound_transactions(), 0); + assert!(txs.contains_key(&tx_id)); + } + + #[test] + fn manage_multiple_transactions() { + let mut rng = OsRng::new().unwrap(); + // Alice, Bob and Carols various parameters + let a_send1 = TestParams::new(&mut rng); + let a_send2 = TestParams::new(&mut rng); + let a_send3 = TestParams::new(&mut rng); + let a_recv1 = TestParams::new(&mut rng); + let b_send1 = TestParams::new(&mut rng); + let b_recv1 = TestParams::new(&mut rng); + let b_recv2 = TestParams::new(&mut rng); + let c_recv1 = TestParams::new(&mut rng); + + // Initializing all the sending transaction protocols + // Alice + let (utxo_a1, input_a1) = make_input(&mut rng, MicroTari(2500)); + let mut builder_a1 = SenderTransactionProtocol::builder(1); + builder_a1 + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a_send1.offset.clone()) + .with_private_nonce(a_send1.nonce.clone()) + .with_change_secret(a_send1.change_key.clone()) + .with_input(utxo_a1.clone(), input_a1) + .with_amount(0, MicroTari(500)); + let alice_stp1 = builder_a1.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + + let (utxo_a2, input_a2) = make_input(&mut rng, MicroTari(2500)); + let mut builder_a2 = SenderTransactionProtocol::builder(1); + builder_a2 + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a_send2.offset.clone()) + .with_private_nonce(a_send2.nonce.clone()) + .with_change_secret(a_send2.change_key.clone()) + .with_input(utxo_a2.clone(), input_a2) + .with_amount(0, MicroTari(500)); + let alice_stp2 = builder_a2.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + + let (utxo_a3, input_a3) = make_input(&mut rng, MicroTari(2500)); + let mut builder_a3 = SenderTransactionProtocol::builder(1); + builder_a3 + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a_send3.offset.clone()) + .with_private_nonce(a_send3.nonce.clone()) + .with_change_secret(a_send3.change_key.clone()) + .with_input(utxo_a3.clone(), input_a3) + .with_amount(0, MicroTari(500)); + let alice_stp3 = builder_a3.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + + // Bob + let (utxo_b1, input_b1) = make_input(&mut rng, MicroTari(2500)); + let mut builder_b1 = SenderTransactionProtocol::builder(1); + builder_b1 + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(b_send1.offset.clone()) + .with_private_nonce(b_send1.nonce.clone()) + .with_change_secret(b_send1.change_key.clone()) + .with_input(utxo_b1.clone(), input_b1) + .with_amount(0, MicroTari(500)); + let bob_stp1 = builder_b1.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + + let mut alice_tx_manager = TransactionManager::new(); + let mut bob_tx_manager = TransactionManager::new(); + let mut carol_tx_manager = TransactionManager::new(); + + // Now a series of interleaved sending and receiving of transactions + let send_msg_a1 = alice_tx_manager.start_send_transaction(alice_stp1).unwrap(); + let mut alice_tx_ids = Vec::new(); + if let SenderMessage::Single(single_round_sender_data) = send_msg_a1.clone() { + alice_tx_ids.push(single_round_sender_data.tx_id); + } + assert_eq!(alice_tx_manager.num_pending_outbound_transactions(), 1); + + let send_msg_a2 = alice_tx_manager.start_send_transaction(alice_stp2).unwrap(); + if let SenderMessage::Single(single_round_sender_data) = send_msg_a2.clone() { + alice_tx_ids.push(single_round_sender_data.tx_id); + } + assert_eq!(alice_tx_manager.num_pending_outbound_transactions(), 2); + + let receive_msg_b1 = bob_tx_manager + .accept_transaction( + send_msg_a1, + b_recv1.nonce, + b_recv1.spend_key, + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + assert_eq!(bob_tx_manager.num_pending_inbound_transactions(), 1); + + let receive_msg_c1 = carol_tx_manager + .accept_transaction( + send_msg_a2, + c_recv1.nonce, + c_recv1.spend_key, + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + assert_eq!(carol_tx_manager.num_pending_inbound_transactions(), 1); + + let send_msg_b1 = bob_tx_manager.start_send_transaction(bob_stp1).unwrap(); + let mut bob_tx_ids = Vec::new(); + if let SenderMessage::Single(single_round_sender_data) = send_msg_b1.clone() { + bob_tx_ids.push(single_round_sender_data.tx_id); + } + assert_eq!(bob_tx_manager.num_pending_outbound_transactions(), 1); + + let receive_msg_a1 = alice_tx_manager + .accept_transaction( + send_msg_b1, + a_recv1.nonce, + a_recv1.spend_key, + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + assert_eq!(alice_tx_manager.num_pending_inbound_transactions(), 1); + + alice_tx_manager + .accept_recipient_reply(receive_msg_c1, &PROVER, &COMMITMENT_FACTORY) + .unwrap(); + assert_eq!(alice_tx_manager.num_pending_outbound_transactions(), 1); + assert_eq!(alice_tx_manager.get_completed_transactions().len(), 1); + + let send_msg_a3 = alice_tx_manager.start_send_transaction(alice_stp3).unwrap(); + if let SenderMessage::Single(single_round_sender_data) = send_msg_a3.clone() { + alice_tx_ids.push(single_round_sender_data.tx_id); + } + assert_eq!(alice_tx_manager.num_pending_outbound_transactions(), 2); + + let receive_msg_b2 = bob_tx_manager + .accept_transaction( + send_msg_a3, + b_recv2.nonce, + b_recv2.spend_key, + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + assert_eq!(bob_tx_manager.num_pending_inbound_transactions(), 2); + + alice_tx_manager + .accept_recipient_reply(receive_msg_b2, &PROVER, &COMMITMENT_FACTORY) + .unwrap(); + assert_eq!(alice_tx_manager.num_pending_outbound_transactions(), 1); + assert_eq!(alice_tx_manager.get_completed_transactions().len(), 2); + + bob_tx_manager + .accept_recipient_reply(receive_msg_a1, &PROVER, &COMMITMENT_FACTORY) + .unwrap(); + assert_eq!(bob_tx_manager.num_pending_outbound_transactions(), 0); + assert_eq!(bob_tx_manager.get_completed_transactions().len(), 1); + + alice_tx_manager + .accept_recipient_reply(receive_msg_b1, &PROVER, &COMMITMENT_FACTORY) + .unwrap(); + assert_eq!(alice_tx_manager.num_pending_outbound_transactions(), 0); + assert_eq!(alice_tx_manager.get_completed_transactions().len(), 3); + + for tx_id in alice_tx_ids { + assert!(alice_tx_manager.get_completed_transactions().contains_key(&tx_id)); + } + + for tx_id in bob_tx_ids { + assert!(bob_tx_manager.get_completed_transactions().contains_key(&tx_id)); + } + } + + #[test] + fn accept_repeated_tx_id() { + let mut rng = OsRng::new().unwrap(); + // Alice's parameters + let a = TestParams::new(&mut rng); + // Bob's parameters + let b = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(2500)); + let mut builder = SenderTransactionProtocol::builder(1); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a.offset.clone()) + .with_private_nonce(a.nonce.clone()) + .with_change_secret(a.change_key.clone()) + .with_input(utxo.clone(), input) + .with_amount(0, MicroTari(500)); + let alice_stp = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + + let mut alice_tx_manager = TransactionManager::new(); + let mut bob_tx_manager = TransactionManager::new(); + + let send_msg = alice_tx_manager.start_send_transaction(alice_stp).unwrap(); + + let _receive_msg = bob_tx_manager + .accept_transaction( + send_msg.clone(), + b.nonce.clone(), + b.spend_key.clone(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + + let receive_msg2 = + bob_tx_manager.accept_transaction(send_msg, b.nonce, b.spend_key, &PROVER, &COMMITMENT_FACTORY); + + assert_eq!(receive_msg2, Err(TransactionManagerError::RepeatedMessageError)); + } + + #[test] + fn accept_malformed_sender_message() { + let mut rng = OsRng::new().unwrap(); + // Bob's parameters + let b = TestParams::new(&mut rng); + let send_msg = SenderMessage::None; + let mut bob_tx_manager = TransactionManager::new(); + let receive_msg = + bob_tx_manager.accept_transaction(send_msg, b.nonce, b.spend_key, &PROVER, &COMMITMENT_FACTORY); + + assert_eq!( + receive_msg, + Err(TransactionManagerError::TransactionProtocolError( + TransactionProtocolError::InvalidStateError + )) + ); + } + + #[test] + fn accept_malformed_recipient_reply() { + let mut rng = OsRng::new().unwrap(); + // Alice's parameters + let a = TestParams::new(&mut rng); + // Bob's parameters + let b = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(2500)); + let mut builder = SenderTransactionProtocol::builder(1); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a.offset.clone()) + .with_private_nonce(a.nonce.clone()) + .with_change_secret(a.change_key.clone()) + .with_input(utxo.clone(), input) + .with_amount(0, MicroTari(500)); + let alice_stp = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + + let mut alice_tx_manager = TransactionManager::new(); + let mut bob_tx_manager = TransactionManager::new(); + + let send_msg = alice_tx_manager.start_send_transaction(alice_stp).unwrap(); + + let mut receive_msg = bob_tx_manager + .accept_transaction( + send_msg.clone(), + b.nonce.clone(), + b.spend_key.clone(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + + // Monkey with the range proof + receive_msg.output.proof = RangeProof::from_bytes(&[0u8; 32]).unwrap(); + + assert_eq!( + alice_tx_manager.accept_recipient_reply(receive_msg, &PROVER, &COMMITMENT_FACTORY), + Err(TransactionManagerError::TransactionProtocolError( + TransactionProtocolError::ValidationError("Recipient output range proof failed to verify".to_string()) + )) + ); + } + + #[test] + fn accept_recipient_reply_for_unknown_tx_id() { + let mut rng = OsRng::new().unwrap(); + // Alice's parameters + let a = TestParams::new(&mut rng); + // Bob's parameters + let b = TestParams::new(&mut rng); + let (utxo, input) = make_input(&mut rng, MicroTari(2500)); + let mut builder = SenderTransactionProtocol::builder(1); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari(20)) + .with_offset(a.offset.clone()) + .with_private_nonce(a.nonce.clone()) + .with_change_secret(a.change_key.clone()) + .with_input(utxo.clone(), input) + .with_amount(0, MicroTari(500)); + let alice_stp = builder.build::(&PROVER, &COMMITMENT_FACTORY).unwrap(); + + let mut alice_tx_manager = TransactionManager::new(); + let mut bob_tx_manager = TransactionManager::new(); + + let send_msg = alice_tx_manager.start_send_transaction(alice_stp).unwrap(); + + let mut receive_msg = bob_tx_manager + .accept_transaction( + send_msg.clone(), + b.nonce.clone(), + b.spend_key.clone(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + + receive_msg.tx_id = 0; + + assert_eq!( + alice_tx_manager.accept_recipient_reply(receive_msg, &PROVER, &COMMITMENT_FACTORY), + Err(TransactionManagerError::TransactionDoesNotExistError) + ); + } + +} diff --git a/base_layer/wallet/src/transaction_service.rs b/base_layer/wallet/src/transaction_service.rs new file mode 100644 index 0000000000..df229b99d3 --- /dev/null +++ b/base_layer/wallet/src/transaction_service.rs @@ -0,0 +1,490 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE + +use crate::{ + output_manager_service::{error::OutputManagerError, output_manager_service::OutputManagerServiceApi}, + types::TransactionRng, +}; +use crossbeam_channel as channel; +use derive_error::Error; +use log::*; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, +}; +use tari_comms::{ + domain_subscriber::SyncDomainSubscription, + message::MessageFlags, + outbound_message_service::{outbound_message_service::OutboundMessageService, BroadcastStrategy, OutboundError}, + types::CommsPublicKey, +}; +use tari_core::{ + tari_amount::MicroTari, + transaction::{KernelFeatures, OutputFeatures, Transaction}, + transaction_protocol::{ + recipient::RecipientSignedMessage, + sender::TransactionSenderMessage, + TransactionProtocolError, + }, + types::{PrivateKey, COMMITMENT_FACTORY, PROVER}, + ReceiverTransactionProtocol, + SenderTransactionProtocol, +}; +use tari_crypto::keys::SecretKey; +use tari_p2p::{ + sync_services::{ + Service, + ServiceApiWrapper, + ServiceContext, + ServiceControlMessage, + ServiceError, + DEFAULT_API_TIMEOUT_MS, + }, + tari_message::{BlockchainMessage, TariMessageType}, +}; + +const LOG_TARGET: &'static str = "base_layer::wallet::transaction_service"; + +#[derive(Debug, Error)] +pub enum TransactionServiceError { + // Transaction protocol is not in the correct state for this operation + InvalidStateError, + // Transaction Protocol Error + TransactionProtocolError(TransactionProtocolError), + // The message being process is not recognized by the Transaction Manager + InvalidMessageTypeError, + // A message for a specific tx_id has been repeated + RepeatedMessageError, + // A recipient reply was received for a non-existent tx_id + TransactionDoesNotExistError, + /// The Outbound Message Service is not initialized + OutboundMessageServiceNotInitialized, + /// Received an unexpected API response + UnexpectedApiResponse, + /// Failed to send from API + ApiSendFailed, + /// Failed to receive in API from service + ApiReceiveFailed, + OutboundError(OutboundError), + OutputManagerError(OutputManagerError), +} + +/// TransactionService allows for the management of multiple inbound and outbound transaction protocols +/// which are uniquely identified by a tx_id. The TransactionService generates and accepts the various protocol +/// messages and applies them to the appropriate protocol instances based on the tx_id. +/// The TransactionService allows for the sending of transactions to single receivers, when the appropriate recipient +/// response is handled the transaction is completed and moved to the completed_transaction buffer. +/// The TransactionService will accept inbound transactions and generate a reply. Received transactions will remain +/// in the pending_inbound_transactions buffer. +/// TODO Allow for inbound transactions that are detected on the blockchain to be marked as complete in the +/// OutputManagerService +/// TODO Detect Completed Transactions on the blockchain before marking them as completed in OutputManagerService +/// # Fields +/// `pending_outbound_transactions` - List of transaction protocols sent by this client and waiting response from the +/// recipient +/// `pending_inbound_transactions` - List of transaction protocols that have been received and responded to. +/// `completed_transaction` - List of sent transactions that have been responded to and are completed. + +pub struct TransactionService { + pending_outbound_transactions: HashMap, + pending_inbound_transactions: HashMap, + completed_transactions: HashMap, + outbound_message_service: Option>, + api: ServiceApiWrapper, + output_manager_service: Arc, +} + +impl TransactionService { + pub fn new(output_manager_service: Arc) -> TransactionService { + TransactionService { + pending_outbound_transactions: HashMap::new(), + pending_inbound_transactions: HashMap::new(), + completed_transactions: HashMap::new(), + outbound_message_service: None, + api: Self::setup_api(), + output_manager_service, + } + } + + fn setup_api() -> ServiceApiWrapper + { + let (api_sender, service_receiver) = channel::bounded(0); + let (service_sender, api_receiver) = channel::bounded(0); + + let api = Arc::new(TransactionServiceApi::new(api_sender, api_receiver)); + ServiceApiWrapper::new(service_receiver, service_sender, api) + } + + /// Return this service's API + pub fn get_api(&self) -> Arc { + self.api.get_api() + } + + /// Sends a new transaction to a recipient + /// # Arguments + /// 'dest_pubkey': The Comms pubkey of the recipient node + /// 'amount': The amount of Tari to send to the recipient + /// 'fee_per_gram': The amount of fee per transaction gram to be included in transaction + pub fn send_transaction( + &mut self, + dest_pubkey: CommsPublicKey, + amount: MicroTari, + fee_per_gram: MicroTari, + ) -> Result<(), TransactionServiceError> + { + let outbound_message_service = self + .outbound_message_service + .clone() + .ok_or(TransactionServiceError::OutboundMessageServiceNotInitialized)?; + + let mut stp = self + .output_manager_service + .prepare_transaction_to_send(amount, fee_per_gram, None)?; + + if !stp.is_single_round_message_ready() { + return Err(TransactionServiceError::InvalidStateError); + } + + let msg = stp.build_single_round_message()?; + outbound_message_service.send_message( + BroadcastStrategy::DirectPublicKey(dest_pubkey.clone()), + MessageFlags::ENCRYPTED, + TransactionSenderMessage::Single(Box::new(msg.clone())), + )?; + + self.pending_outbound_transactions.insert(msg.tx_id.clone(), stp); + + info!( + target: LOG_TARGET, + "Transaction with TX_ID = {} sent to {}", + msg.tx_id.clone(), + dest_pubkey + ); + + Ok(()) + } + + /// Accept the public reply from a recipient and apply the reply to the relevant transaction protocol + /// # Arguments + /// 'recipient_reply' - The public response from a recipient with data required to complete the transaction + pub fn accept_recipient_reply( + &mut self, + recipient_reply: RecipientSignedMessage, + ) -> Result<(), TransactionServiceError> + { + let mut marked_for_removal = None; + + for (tx_id, stp) in self.pending_outbound_transactions.iter_mut() { + let recp_tx_id = recipient_reply.tx_id.clone(); + if stp.check_tx_id(recp_tx_id) && stp.is_collecting_single_signature() { + stp.add_single_recipient_info(recipient_reply, &PROVER)?; + stp.finalize(KernelFeatures::empty(), &PROVER, &COMMITMENT_FACTORY)?; + let tx = stp.get_transaction()?; + self.completed_transactions.insert(recp_tx_id, tx.clone()); + // TODO Broadcast this to the chain + // TODO Only confirm this transaction once it is detected on chain. For now just confirming it directly. + self.output_manager_service.confirm_sent_transaction( + recp_tx_id, + tx.body.inputs.clone(), + tx.body.outputs.clone(), + )?; + + marked_for_removal = Some(tx_id.clone()); + break; + } + } + + if marked_for_removal.is_none() { + return Err(TransactionServiceError::TransactionDoesNotExistError); + } + + if let Some(tx_id) = marked_for_removal { + self.pending_outbound_transactions.remove(&tx_id); + info!( + target: LOG_TARGET, + "Transaction Recipient Reply for TX_ID = {} received", tx_id, + ); + } + + Ok(()) + } + + /// Accept a new transaction from a sender by handling a public SenderMessage. The reply is generated and sent. + /// # Arguments + /// 'source_pubkey' - The pubkey from which the message was sent and to which the reply will be sent. + /// 'sender_message' - Message from a sender containing the setup of the transaction being sent to you + pub fn accept_transaction( + &mut self, + source_pubkey: CommsPublicKey, + sender_message: TransactionSenderMessage, + ) -> Result<(), TransactionServiceError> + { + let outbound_message_service = self + .outbound_message_service + .clone() + .ok_or(TransactionServiceError::OutboundMessageServiceNotInitialized)?; + + // Currently we will only reply to a Single sender transaction protocol + if let TransactionSenderMessage::Single(data) = sender_message.clone() { + let spending_key = self + .output_manager_service + .get_recipient_spending_key(data.tx_id, data.amount)?; + let mut rng = TransactionRng::new().unwrap(); + let nonce = PrivateKey::random(&mut rng); + + let rtp = ReceiverTransactionProtocol::new( + sender_message, + nonce, + spending_key, + OutputFeatures::default(), + &PROVER, + &COMMITMENT_FACTORY, + ); + let recipient_reply = rtp.get_signed_data()?.clone(); + + // Check this is not a repeat message i.e. tx_id doesn't already exist in our pending or completed + // transactions + if self.pending_outbound_transactions.contains_key(&recipient_reply.tx_id) || + self.pending_inbound_transactions.contains_key(&recipient_reply.tx_id) || + self.completed_transactions.contains_key(&recipient_reply.tx_id) + { + return Err(TransactionServiceError::RepeatedMessageError); + } + + outbound_message_service.send_message( + BroadcastStrategy::DirectPublicKey(source_pubkey.clone()), + MessageFlags::ENCRYPTED, + recipient_reply.clone(), + )?; + + // Otherwise add it to our pending transaction list and return reply + self.pending_inbound_transactions + .insert(recipient_reply.tx_id.clone(), rtp); + + info!( + target: LOG_TARGET, + "Transaction with TX_ID = {} received from {}. Reply Sent", + recipient_reply.tx_id.clone(), + source_pubkey.clone() + ); + } + Ok(()) + } + + /// This handler is called when the Service executor loops receives an API request + fn handle_api_message(&mut self, msg: TransactionServiceApiRequest) -> Result<(), ServiceError> { + trace!(target: LOG_TARGET, "[{}] Received API message", self.get_name(),); + let resp = match msg { + TransactionServiceApiRequest::SendTransaction((dest_pubkey, amount, fee_per_gram)) => self + .send_transaction(dest_pubkey, amount, fee_per_gram) + .map(|_| TransactionServiceApiResponse::TransactionSent), + TransactionServiceApiRequest::GetPendingInboundTransactions => Ok( + TransactionServiceApiResponse::PendingInboundTransactions(self.pending_inbound_transactions.clone()), + ), + TransactionServiceApiRequest::GetPendingOutboundTransactions => Ok( + TransactionServiceApiResponse::PendingOutboundTransactions(self.pending_outbound_transactions.clone()), + ), + TransactionServiceApiRequest::GetCompletedTransactions => Ok( + TransactionServiceApiResponse::CompletedTransactions(self.completed_transactions.clone()), + ), + }; + + trace!(target: LOG_TARGET, "[{}] Replying to API", self.get_name()); + self.api + .send_reply(resp) + .map_err(ServiceError::internal_service_error()) + } +} + +/// The Domain Service trait implementation for the TestMessageService +impl Service for TransactionService { + fn get_name(&self) -> String { + "Transaction service".to_string() + } + + fn get_message_types(&self) -> Vec { + vec![ + BlockchainMessage::Transaction.into(), + BlockchainMessage::TransactionReply.into(), + ] + } + + /// Function called by the Service Executor in its own thread. This function polls for both API request and Comms + /// layer messages from the Message Broker + fn execute(&mut self, context: ServiceContext) -> Result<(), ServiceError> { + let mut subscription_transaction = SyncDomainSubscription::new( + context + .inbound_message_subscription_factory() + .get_subscription_fused(BlockchainMessage::Transaction.into()), + ); + + let mut subscription_transaction_reply = SyncDomainSubscription::new( + context + .inbound_message_subscription_factory() + .get_subscription_fused(BlockchainMessage::TransactionReply.into()), + ); + + self.outbound_message_service = Some(context.outbound_message_service()); + debug!(target: LOG_TARGET, "Starting Transaction Service executor"); + loop { + if let Some(msg) = context.get_control_message(Duration::from_millis(5)) { + match msg { + ServiceControlMessage::Shutdown => break, + } + } + + for m in subscription_transaction.receive_messages()?.drain(..) { + if let Err(e) = self.accept_transaction(m.0.origin_source.clone(), m.1) { + error!(target: LOG_TARGET, "Transaction service had error: {:?}", e); + } + } + + for m in subscription_transaction_reply.receive_messages()?.drain(..) { + if let Err(e) = self.accept_recipient_reply(m.1) { + error!(target: LOG_TARGET, "Transaction service had error: {:?}", e); + } + } + + if let Some(msg) = self + .api + .recv_timeout(Duration::from_millis(50)) + .map_err(ServiceError::internal_service_error())? + { + self.handle_api_message(msg)?; + } + } + + Ok(()) + } +} + +/// API Request enum +#[derive(Debug)] +pub enum TransactionServiceApiRequest { + GetPendingInboundTransactions, + GetPendingOutboundTransactions, + GetCompletedTransactions, + SendTransaction((CommsPublicKey, MicroTari, MicroTari)), +} + +/// API Response enum +#[derive(Debug)] +pub enum TransactionServiceApiResponse { + TransactionSent, + PendingInboundTransactions(HashMap), + PendingOutboundTransactions(HashMap), + CompletedTransactions(HashMap), +} + +/// Result for all API requests +pub type TransactionServiceApiResult = Result; + +/// The TextMessage service public API that other services and application will use to interact with this service. +/// The requests and responses are transmitted via channels into the Service Executor thread where this service is +/// running +pub struct TransactionServiceApi { + sender: channel::Sender, + receiver: channel::Receiver, + mutex: Mutex<()>, + timeout: Duration, +} + +impl TransactionServiceApi { + fn new( + sender: channel::Sender, + receiver: channel::Receiver, + ) -> Self + { + Self { + sender, + receiver, + mutex: Mutex::new(()), + timeout: Duration::from_millis(DEFAULT_API_TIMEOUT_MS), + } + } + + pub fn send_transaction( + &self, + dest_pubkey: CommsPublicKey, + amount: MicroTari, + fee_per_gram: MicroTari, + ) -> Result<(), TransactionServiceError> + { + self.send_recv(TransactionServiceApiRequest::SendTransaction(( + dest_pubkey, + amount, + fee_per_gram, + ))) + .and_then(|resp| match resp { + TransactionServiceApiResponse::TransactionSent => Ok(()), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + }) + } + + pub fn get_pending_inbound_transaction( + &self, + ) -> Result, TransactionServiceError> { + self.send_recv(TransactionServiceApiRequest::GetPendingInboundTransactions) + .and_then(|resp| match resp { + TransactionServiceApiResponse::PendingInboundTransactions(p) => Ok(p), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + }) + } + + pub fn get_pending_outbound_transaction( + &self, + ) -> Result, TransactionServiceError> { + self.send_recv(TransactionServiceApiRequest::GetPendingOutboundTransactions) + .and_then(|resp| match resp { + TransactionServiceApiResponse::PendingOutboundTransactions(p) => Ok(p), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + }) + } + + pub fn get_completed_transaction(&self) -> Result, TransactionServiceError> { + self.send_recv(TransactionServiceApiRequest::GetCompletedTransactions) + .and_then(|resp| match resp { + TransactionServiceApiResponse::CompletedTransactions(c) => Ok(c), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + }) + } + + fn send_recv(&self, msg: TransactionServiceApiRequest) -> TransactionServiceApiResult { + self.lock(|| -> TransactionServiceApiResult { + self.sender + .send(msg) + .map_err(|_| TransactionServiceError::ApiSendFailed)?; + self.receiver + .recv_timeout(self.timeout.clone()) + .map_err(|_| TransactionServiceError::ApiReceiveFailed)? + }) + } + + fn lock(&self, func: F) -> T + where F: FnOnce() -> T { + let lock = acquire_lock!(self.mutex); + let res = func(); + drop(lock); + res + } +} diff --git a/base_layer/wallet/src/types.rs b/base_layer/wallet/src/types.rs new file mode 100644 index 0000000000..ebcd1b605e --- /dev/null +++ b/base_layer/wallet/src/types.rs @@ -0,0 +1,33 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use rand::OsRng; +use tari_crypto::common::Blake256; + +/// Specify the Hash function used by the key manager +pub type KeyDigest = Blake256; + +/// Specify the Hash function used when constructing challenges during transaction building +pub type HashDigest = Blake256; + +/// Specify the Rng to use while building transactions for this wallet +pub type TransactionRng = OsRng; diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs new file mode 100644 index 0000000000..16d212a916 --- /dev/null +++ b/base_layer/wallet/src/wallet.rs @@ -0,0 +1,79 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::text_message_service::{TextMessageService, TextMessageServiceApi}; +use derive_error::Error; +use std::sync::Arc; +use tari_comms::{builder::CommsServices, types::CommsPublicKey}; +use tari_p2p::{ + initialization::{initialize_comms, CommsConfig, CommsInitializationError}, + ping_pong::{PingPongService, PingPongServiceApi}, + sync_services::{ServiceExecutor, ServiceRegistry}, + tari_message::TariMessageType, +}; + +#[derive(Debug, Error)] +pub enum WalletError { + CommsInitializationError(CommsInitializationError), +} + +#[derive(Clone)] +pub struct WalletConfig { + pub comms: CommsConfig, + pub public_key: CommsPublicKey, + pub database_path: String, +} + +/// A structure containing the config and services that a Wallet application will require. This struct will start up all +/// the services and provide the APIs that applications will use to interact with the services +pub struct Wallet { + pub ping_pong_service: Arc, + pub text_message_service: Arc, + pub comms_services: Arc>, + pub service_executor: ServiceExecutor, + pub public_key: CommsPublicKey, +} + +impl Wallet { + pub fn new(config: WalletConfig) -> Result { + let ping_pong_service = PingPongService::new(); + let ping_pong_service_api = ping_pong_service.get_api(); + + let text_message_service = TextMessageService::new(config.public_key.clone(), config.database_path.clone()); + let text_message_service_api = text_message_service.get_api(); + + let registry = ServiceRegistry::new() + .register(ping_pong_service) + .register(text_message_service); + + let comms_services = initialize_comms(config.comms.clone())?; + let service_executor = ServiceExecutor::execute(&comms_services, registry); + + Ok(Wallet { + text_message_service: text_message_service_api, + ping_pong_service: ping_pong_service_api, + comms_services, + service_executor, + public_key: config.public_key.clone(), + }) + } +} diff --git a/base_layer/wallet/tests/data/.gitkeep b/base_layer/wallet/tests/data/.gitkeep new file mode 100644 index 0000000000..79e790c1e5 --- /dev/null +++ b/base_layer/wallet/tests/data/.gitkeep @@ -0,0 +1 @@ +Temp folder for LMDB database files \ No newline at end of file diff --git a/base_layer/wallet/tests/mod.rs b/base_layer/wallet/tests/mod.rs new file mode 100644 index 0000000000..bc8e8609c8 --- /dev/null +++ b/base_layer/wallet/tests/mod.rs @@ -0,0 +1,27 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod output_manager_service; +pub mod support; +pub mod text_message_service; +pub mod transaction_service; +pub mod wallet; diff --git a/base_layer/wallet/tests/output_manager_service/mod.rs b/base_layer/wallet/tests/output_manager_service/mod.rs new file mode 100644 index 0000000000..44985eff8e --- /dev/null +++ b/base_layer/wallet/tests/output_manager_service/mod.rs @@ -0,0 +1,390 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use crate::support::{ + comms_and_services::setup_comms_services, + data::{clean_up_datastore, init_datastore}, + utils::{make_input, TestParams}, +}; +use chrono::Duration as ChronoDuration; +use log::Level; +use rand::RngCore; +use std::{thread, time::Duration}; +use tari_comms::peer_manager::NodeIdentity; +use tari_core::{ + consensus::ConsensusRules, + fee::Fee, + tari_amount::MicroTari, + transaction::{KernelFeatures, OutputFeatures, TransactionOutput, UnblindedOutput}, + transaction_protocol::single_receiver::SingleReceiverTransactionProtocol, + types::{PrivateKey, PublicKey, RangeProof, COMMITMENT_FACTORY, PROVER}, +}; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::{PublicKey as PublicKeyTrait, SecretKey}, + range_proof::RangeProofService, +}; +use tari_p2p::sync_services::{ServiceExecutor, ServiceRegistry}; +use tari_utilities::ByteArray; +use tari_wallet::output_manager_service::{error::OutputManagerError, output_manager_service::OutputManagerService}; + +#[test] +fn sending_transaction_and_confirmation() { + let mut rng = rand::OsRng::new().unwrap(); + let (secret_key, _public_key) = PublicKey::random_keypair(&mut rng); + + let mut oms = OutputManagerService::new(secret_key, "".to_string(), 0); + + let (_ti, uo) = make_input(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + oms.add_output(uo.clone()).unwrap(); + assert_eq!(oms.add_output(uo), Err(OutputManagerError::DuplicateOutput)); + let num_outputs = 20; + for _i in 0..num_outputs { + let (_ti, uo) = make_input(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + oms.add_output(uo).unwrap(); + } + + let mut stp = oms + .prepare_transaction_to_send(MicroTari::from(1000), MicroTari::from(20), None) + .unwrap(); + + let sender_tx_id = stp.get_tx_id().unwrap(); + let mut num_change = 0; + // Is there change? Unlikely not to be but the random amounts MIGHT produce a no change output situation + if stp.get_amount_to_self().unwrap() > MicroTari::from(0) { + let pt = oms.pending_transactions(); + assert_eq!(pt.len(), 1); + assert_eq!( + pt.get(&sender_tx_id).unwrap().outputs_to_be_received[0].value, + stp.get_amount_to_self().unwrap() + ); + num_change = 1; + } + + let msg = stp.build_single_round_message().unwrap(); + + let b = TestParams::new(&mut rng); + + let recv_info = SingleReceiverTransactionProtocol::create( + &msg, + b.nonce, + b.spend_key, + OutputFeatures::default(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + + stp.add_single_recipient_info(recv_info.clone(), &PROVER).unwrap(); + + stp.finalize(KernelFeatures::empty(), &PROVER, &COMMITMENT_FACTORY) + .unwrap(); + + let tx = stp.get_transaction().unwrap(); + + oms.confirm_sent_transaction(sender_tx_id, &tx.body.inputs, &tx.body.outputs) + .unwrap(); + + assert_eq!(oms.pending_transactions().len(), 0); + assert_eq!(oms.spent_outputs().len(), tx.body.inputs.len()); + assert_eq!( + oms.unspent_outputs().len(), + num_outputs + 1 - oms.spent_outputs().len() + num_change + ); +} + +#[test] +fn send_not_enough_funds() { + let mut rng = rand::OsRng::new().unwrap(); + let (secret_key, _public_key) = PublicKey::random_keypair(&mut rng); + + let mut oms = OutputManagerService::new(secret_key, "".to_string(), 0); + + let num_outputs = 20; + for _i in 0..num_outputs { + let (_ti, uo) = make_input(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + oms.add_output(uo).unwrap(); + } + + match oms.prepare_transaction_to_send(MicroTari::from(num_outputs * 2000), MicroTari::from(20), None) { + Err(OutputManagerError::NotEnoughFunds) => assert!(true), + _ => assert!(false), + } +} + +#[test] +fn send_no_change() { + let mut rng = rand::OsRng::new().unwrap(); + let (secret_key, _public_key) = PublicKey::random_keypair(&mut rng); + + let mut oms = OutputManagerService::new(secret_key, "".to_string(), 0); + let fee_per_gram = MicroTari::from(20); + let fee_without_change = Fee::calculate(fee_per_gram, 2, 1); + let key1 = PrivateKey::random(&mut rng); + let value1 = 500; + oms.add_output(UnblindedOutput::new(MicroTari::from(value1), key1, None)) + .unwrap(); + let key2 = PrivateKey::random(&mut rng); + let value2 = 800; + oms.add_output(UnblindedOutput::new(MicroTari::from(value2), key2, None)) + .unwrap(); + + let mut stp = oms + .prepare_transaction_to_send( + MicroTari::from(value1 + value2) - fee_without_change, + MicroTari::from(20), + None, + ) + .unwrap(); + + let sender_tx_id = stp.get_tx_id().unwrap(); + assert_eq!(stp.get_amount_to_self().unwrap(), MicroTari::from(0)); + assert_eq!(oms.pending_transactions().len(), 1); + + let msg = stp.build_single_round_message().unwrap(); + + let b = TestParams::new(&mut rng); + + let recv_info = SingleReceiverTransactionProtocol::create( + &msg, + b.nonce, + b.spend_key, + OutputFeatures::default(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + + stp.add_single_recipient_info(recv_info.clone(), &PROVER).unwrap(); + + stp.finalize(KernelFeatures::empty(), &PROVER, &COMMITMENT_FACTORY) + .unwrap(); + + let tx = stp.get_transaction().unwrap(); + + oms.confirm_sent_transaction(sender_tx_id, &tx.body.inputs, &tx.body.outputs) + .unwrap(); + + assert_eq!(oms.pending_transactions().len(), 0); + assert_eq!(oms.spent_outputs().len(), tx.body.inputs.len()); + assert_eq!(oms.unspent_outputs().len(), 0); +} + +#[test] +fn send_not_enough_for_change() { + let mut rng = rand::OsRng::new().unwrap(); + let (secret_key, _public_key) = PublicKey::random_keypair(&mut rng); + + let mut oms = OutputManagerService::new(secret_key, "".to_string(), 0); + let fee_per_gram = MicroTari::from(20); + let fee_without_change = Fee::calculate(fee_per_gram, 2, 1); + let key1 = PrivateKey::random(&mut rng); + let value1 = 500; + oms.add_output(UnblindedOutput::new(MicroTari::from(value1), key1, None)) + .unwrap(); + let key2 = PrivateKey::random(&mut rng); + let value2 = 800; + oms.add_output(UnblindedOutput::new(MicroTari::from(value2), key2, None)) + .unwrap(); + + match oms.prepare_transaction_to_send( + MicroTari::from(value1 + value2 + 1) - fee_without_change, + MicroTari::from(20), + None, + ) { + Err(OutputManagerError::NotEnoughFunds) => assert!(true), + _ => assert!(false), + } +} + +#[test] +fn receiving_and_confirmation() { + let mut rng = rand::OsRng::new().unwrap(); + let (secret_key, _public_key) = PublicKey::random_keypair(&mut rng); + + let mut oms = OutputManagerService::new(secret_key, "".to_string(), 0); + let value = MicroTari::from(5000); + let recv_key = oms.get_recipient_spending_key(1, value).unwrap(); + assert_eq!(oms.unspent_outputs().len(), 0); + assert_eq!(oms.pending_transactions().len(), 1); + + let commitment = COMMITMENT_FACTORY.commit(&recv_key, &value.into()); + let rr = PROVER.construct_proof(&recv_key, value.into()).unwrap(); + let output = TransactionOutput::new( + OutputFeatures::create_coinbase(0, &ConsensusRules::current()), + commitment, + RangeProof::from_bytes(&rr).unwrap(), + ); + + oms.confirm_received_transaction_output(1, &output).unwrap(); + + assert_eq!(oms.pending_transactions().len(), 0); + assert_eq!(oms.unspent_outputs().len(), 1); +} + +#[test] +fn cancel_transaction() { + let mut rng = rand::OsRng::new().unwrap(); + let (secret_key, _public_key) = PublicKey::random_keypair(&mut rng); + + let mut oms = OutputManagerService::new(secret_key, "".to_string(), 0); + + let num_outputs = 20; + for _i in 0..num_outputs { + let (_ti, uo) = make_input(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + oms.add_output(uo).unwrap(); + } + let stp = oms + .prepare_transaction_to_send(MicroTari::from(1000), MicroTari::from(20), None) + .unwrap(); + + assert_eq!( + oms.cancel_transaction(1), + Err(OutputManagerError::PendingTransactionNotFound) + ); + + oms.cancel_transaction(stp.get_tx_id().unwrap()).unwrap(); + + assert_eq!(oms.unspent_outputs().len(), num_outputs); +} + +#[test] +fn timeout_transaction() { + let mut rng = rand::OsRng::new().unwrap(); + let (secret_key, _public_key) = PublicKey::random_keypair(&mut rng); + + let mut oms = OutputManagerService::new(secret_key, "".to_string(), 0); + + let num_outputs = 20; + for _i in 0..num_outputs { + let (_ti, uo) = make_input(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + oms.add_output(uo).unwrap(); + } + let _stp = oms + .prepare_transaction_to_send(MicroTari::from(1000), MicroTari::from(20), None) + .unwrap(); + + let remaining_outputs = oms.unspent_outputs().len(); + + thread::sleep(Duration::from_millis(2)); + + oms.timeout_pending_transactions(chrono::Duration::milliseconds(10)) + .unwrap(); + + assert_eq!(oms.unspent_outputs().len(), remaining_outputs); + + oms.timeout_pending_transactions(chrono::Duration::milliseconds(1)) + .unwrap(); + + assert_eq!(oms.unspent_outputs().len(), num_outputs); +} + +#[test] +fn test_api() { + let _ = simple_logger::init_with_level(Level::Debug); + let mut rng = rand::OsRng::new().unwrap(); + let (secret_key, _public_key) = PublicKey::random_keypair(&mut rng); + + let oms = OutputManagerService::new(secret_key, "".to_string(), 0); + let api = oms.get_api(); + let services = ServiceRegistry::new().register(oms); + + // The Service Executor needs a comms stack even though the OMS doesn't use the comms stack. + let node_1_identity = NodeIdentity::random(&mut rng, "127.0.0.1:32563".parse().unwrap()).unwrap(); + let node_1_database_name = "node_1_output_manager_service_api_test"; // Note: every test should have unique database + let node_1_datastore = init_datastore(node_1_database_name).unwrap(); + let node_1_peer_database = node_1_datastore.get_handle(node_1_database_name).unwrap(); + let comms = setup_comms_services(node_1_identity.clone(), Vec::new(), node_1_peer_database); + + let executor = ServiceExecutor::execute(&comms, services); + + assert_eq!(api.get_balance().unwrap(), MicroTari::from(0)); + + let num_outputs = 20; + let mut balance = MicroTari::from(0); + for _i in 0..num_outputs { + let (_ti, uo) = make_input(&mut rng.clone(), MicroTari::from(100 + rng.next_u64() % 1000)); + balance += uo.clone().value; + api.add_output(uo).unwrap(); + } + let amount_to_send = MicroTari::from(1000); + let fee_per_gram = MicroTari::from(20); + let stp = api + .prepare_transaction_to_send(amount_to_send, fee_per_gram, None) + .unwrap(); + + assert_ne!(api.get_balance().unwrap(), balance); + api.cancel_transaction(stp.get_tx_id().unwrap()).unwrap(); + assert_eq!(api.get_balance().unwrap(), balance); + let _stp = api + .prepare_transaction_to_send(amount_to_send, fee_per_gram, None) + .unwrap(); + assert_ne!(api.get_balance().unwrap(), balance); + thread::sleep(Duration::from_millis(10)); + api.timeout_transactions(ChronoDuration::milliseconds(1)).unwrap(); + assert_eq!(api.get_balance().unwrap(), balance); + + let mut stp = api + .prepare_transaction_to_send(amount_to_send, fee_per_gram, None) + .unwrap(); + + let sender_tx_id = stp.get_tx_id().unwrap(); + let msg = stp.build_single_round_message().unwrap(); + + let b = TestParams::new(&mut rng); + + let recv_info = SingleReceiverTransactionProtocol::create( + &msg, + b.nonce, + b.spend_key, + OutputFeatures::default(), + &PROVER, + &COMMITMENT_FACTORY, + ) + .unwrap(); + + stp.add_single_recipient_info(recv_info.clone(), &PROVER).unwrap(); + + stp.finalize(KernelFeatures::empty(), &PROVER, &COMMITMENT_FACTORY) + .unwrap(); + let tx = stp.get_transaction().unwrap(); + let fee = Fee::calculate(fee_per_gram, tx.body.inputs.len(), tx.body.outputs.len()); + + api.confirm_sent_transaction(sender_tx_id, tx.body.inputs.clone(), tx.body.outputs.clone()) + .unwrap(); + + assert_eq!(api.get_balance().unwrap(), balance - amount_to_send - fee); + let balance = api.get_balance().unwrap(); + let value = MicroTari::from(5000); + let recv_key = api.get_recipient_spending_key(1, value).unwrap(); + let commitment = COMMITMENT_FACTORY.commit(&recv_key, &value.into()); + let rr = PROVER.construct_proof(&recv_key, value.into()).unwrap(); + let output = TransactionOutput::new( + OutputFeatures::create_coinbase(0, &ConsensusRules::current()), + commitment, + RangeProof::from_bytes(&rr).unwrap(), + ); + api.confirm_received_output(1, output).unwrap(); + + assert_eq!(api.get_balance().unwrap(), balance + value); + executor.shutdown().unwrap(); + clean_up_datastore(node_1_database_name); +} diff --git a/base_layer/wallet/tests/support/comms_and_services.rs b/base_layer/wallet/tests/support/comms_and_services.rs new file mode 100644 index 0000000000..32ca3c00af --- /dev/null +++ b/base_layer/wallet/tests/support/comms_and_services.rs @@ -0,0 +1,68 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{sync::Arc, time::Duration}; +use tari_comms::{ + builder::CommsServices, + connection_manager::PeerConnectionConfig, + control_service::ControlServiceConfig, + peer_manager::{NodeIdentity, Peer}, + CommsBuilder, +}; +use tari_p2p::tari_message::TariMessageType; +use tari_storage::{lmdb_store::LMDBDatabase, LMDBWrapper}; +pub fn setup_comms_services( + node_identity: NodeIdentity, + peers: Vec, + peer_database: LMDBDatabase, +) -> CommsServices +{ + let peer_database = LMDBWrapper::new(Arc::new(peer_database)); + let comms = CommsBuilder::new() + .with_node_identity(node_identity.clone()) + .with_peer_storage(peer_database) + .configure_peer_connections(PeerConnectionConfig { + host: "127.0.0.1".parse().unwrap(), + ..Default::default() + }) + .configure_control_service(ControlServiceConfig { + socks_proxy_address: None, + listener_address: node_identity.control_service_address().unwrap(), + requested_connection_timeout: Duration::from_millis(5000), + }) + .build() + .unwrap() + .start() + .unwrap(); + + for p in peers { + comms + .peer_manager() + .add_peer( + Peer::from_public_key_and_address(p.identity.public_key.clone(), p.control_service_address().unwrap()) + .unwrap(), + ) + .unwrap(); + } + + comms +} diff --git a/base_layer/wallet/tests/support/data.rs b/base_layer/wallet/tests/support/data.rs new file mode 100644 index 0000000000..9b2bcf5c57 --- /dev/null +++ b/base_layer/wallet/tests/support/data.rs @@ -0,0 +1,58 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::path::PathBuf; +use tari_storage::lmdb_store::{LMDBBuilder, LMDBError, LMDBStore}; + +pub fn get_path(name: Option<&str>) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name.unwrap_or("")); + path.to_str().unwrap().to_string() +} + +pub fn init_datastore(name: &str) -> Result { + let path = get_path(Some(name)); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + LMDBBuilder::new() + .set_path(&path) + .set_environment_size(10) + .set_max_number_of_databases(1) + .add_database(name, lmdb_zero::db::CREATE) + .build() +} + +pub fn clean_up_datastore(name: &str) { + std::fs::remove_dir_all(get_path(Some(name))).unwrap(); +} + +pub fn clean_up_sql_database(name: &str) { + if std::fs::metadata(get_path(Some(name))).is_ok() { + std::fs::remove_file(get_path(Some(name))).unwrap(); + } +} + +pub fn init_sql_database(name: &str) { + clean_up_sql_database(name); + let path = get_path(None); + let _ = std::fs::create_dir(&path).unwrap_or_default(); +} diff --git a/base_layer/wallet/tests/support/mod.rs b/base_layer/wallet/tests/support/mod.rs new file mode 100644 index 0000000000..3f8484d916 --- /dev/null +++ b/base_layer/wallet/tests/support/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod comms_and_services; +pub mod data; +pub mod utils; diff --git a/base_layer/wallet/tests/support/utils.rs b/base_layer/wallet/tests/support/utils.rs new file mode 100644 index 0000000000..4067d22b24 --- /dev/null +++ b/base_layer/wallet/tests/support/utils.rs @@ -0,0 +1,86 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use rand::{CryptoRng, Rng}; +use std::{fmt::Debug, thread, time::Duration}; +use tari_core::{ + tari_amount::MicroTari, + transaction::{OutputFeatures, TransactionInput, UnblindedOutput}, + types::{PrivateKey, PublicKey, COMMITMENT_FACTORY}, +}; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::{PublicKey as PublicKeyTrait, SecretKey as SecretKeyTrait}, +}; +pub fn assert_change(func: F, to: T, poll_count: usize) +where + F: Fn() -> T, + T: Eq + Debug, +{ + let mut i = 0; + loop { + let last_val = func(); + if last_val == to { + break; + } + + i += 1; + if i >= poll_count { + panic!( + "Value did not change to {:?} within {}ms (last value: {:?})", + to, + poll_count * 100, + last_val, + ); + } + + thread::sleep(Duration::from_millis(100)); + } +} + +pub struct TestParams { + pub spend_key: PrivateKey, + pub change_key: PrivateKey, + pub offset: PrivateKey, + pub nonce: PrivateKey, + pub public_nonce: PublicKey, +} + +impl TestParams { + pub fn new(rng: &mut R) -> TestParams { + let r = PrivateKey::random(rng); + TestParams { + spend_key: PrivateKey::random(rng), + change_key: PrivateKey::random(rng), + offset: PrivateKey::random(rng), + public_nonce: PublicKey::from_secret_key(&r), + nonce: r, + } + } +} + +pub fn make_input(rng: &mut R, val: MicroTari) -> (TransactionInput, UnblindedOutput) { + let key = PrivateKey::random(rng); + let commitment = COMMITMENT_FACTORY.commit_value(&key, val.into()); + let input = TransactionInput::new(OutputFeatures::default(), commitment); + (input, UnblindedOutput::new(val, key, None)) +} diff --git a/base_layer/wallet/tests/text_message_service/mod.rs b/base_layer/wallet/tests/text_message_service/mod.rs new file mode 100644 index 0000000000..20f3070ff0 --- /dev/null +++ b/base_layer/wallet/tests/text_message_service/mod.rs @@ -0,0 +1,220 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support::{comms_and_services::setup_comms_services, data::*, utils::assert_change}; +use std::sync::Arc; +use tari_comms::{builder::CommsServices, peer_manager::NodeIdentity}; +use tari_p2p::{ + sync_services::{ServiceExecutor, ServiceRegistry}, + tari_message::TariMessageType, +}; +use tari_storage::lmdb_store::LMDBDatabase; +use tari_wallet::text_message_service::{Contact, TextMessageService, TextMessageServiceApi}; + +pub fn setup_text_message_service( + node_identity: NodeIdentity, + peers: Vec, + peer_database: LMDBDatabase, + database_path: String, +) -> ( + ServiceExecutor, + Arc, + CommsServices, +) +{ + let tms = TextMessageService::new(node_identity.identity.public_key.clone(), database_path); + let tms_api = tms.get_api(); + + let services = ServiceRegistry::new().register(tms); + + let comms = setup_comms_services(node_identity, peers, peer_database); + + (ServiceExecutor::execute(&comms, services), tms_api, comms) +} + +#[test] +fn test_text_message_service() { + let mut rng = rand::OsRng::new().unwrap(); + + let node_1_identity = NodeIdentity::random(&mut rng, "127.0.0.1:31523".parse().unwrap()).unwrap(); + let node_2_identity = NodeIdentity::random(&mut rng, "127.0.0.1:31145".parse().unwrap()).unwrap(); + let node_3_identity = NodeIdentity::random(&mut rng, "127.0.0.1:31546".parse().unwrap()).unwrap(); + + let node_1_database_name = "node_1_test_text_message_service"; // Note: every test should have unique database + let node_1_datastore = init_datastore(node_1_database_name).unwrap(); + let node_1_peer_database = node_1_datastore.get_handle(node_1_database_name).unwrap(); + let node_2_database_name = "node_2_test_text_message_service"; // Note: every test should have unique database + let node_2_datastore = init_datastore(node_2_database_name).unwrap(); + let node_2_peer_database = node_2_datastore.get_handle(node_2_database_name).unwrap(); + let node_3_database_name = "node_3_test_text_message_service"; // Note: every test should have unique database + let node_3_datastore = init_datastore(node_3_database_name).unwrap(); + let node_3_peer_database = node_3_datastore.get_handle(node_3_database_name).unwrap(); + + let db_name1 = "test_text_message_service1.sqlite3"; + let db_path1 = get_path(Some(db_name1)); + init_sql_database(db_name1); + + let db_name2 = "test_text_message_service2.sqlite3"; + let db_path2 = get_path(Some(db_name2)); + init_sql_database(db_name2); + + let db_name3 = "test_text_message_service3.sqlite3"; + let db_path3 = get_path(Some(db_name3)); + init_sql_database(db_name3); + + let (node_1_services, node_1_tms, _comms_1) = setup_text_message_service( + node_1_identity.clone(), + vec![node_2_identity.clone(), node_3_identity.clone()], + node_1_peer_database, + db_path1, + ); + let (node_2_services, node_2_tms, _comms_2) = setup_text_message_service( + node_2_identity.clone(), + vec![node_1_identity.clone()], + node_2_peer_database, + db_path2, + ); + let (node_3_services, node_3_tms, _comms_3) = setup_text_message_service( + node_3_identity.clone(), + vec![node_1_identity.clone()], + node_3_peer_database, + db_path3, + ); + + node_1_tms + .add_contact(Contact::new( + "Bob".to_string(), + node_2_identity.identity.public_key.clone(), + node_2_identity.control_service_address().unwrap(), + )) + .unwrap(); + node_1_tms + .add_contact(Contact::new( + "Carol".to_string(), + node_3_identity.identity.public_key.clone(), + node_3_identity.control_service_address().unwrap(), + )) + .unwrap(); + + node_2_tms + .add_contact(Contact::new( + "Alice".to_string(), + node_1_identity.identity.public_key.clone(), + node_1_identity.control_service_address().unwrap(), + )) + .unwrap(); + + node_3_tms + .add_contact(Contact::new( + "Alice".to_string(), + node_1_identity.identity.public_key.clone(), + node_1_identity.control_service_address().unwrap(), + )) + .unwrap(); + + let mut node1_to_node2_sent_messages = vec!["Say Hello".to_string(), "to my little friend!".to_string()]; + + node_1_tms + .send_text_message( + node_2_identity.identity.public_key.clone(), + node1_to_node2_sent_messages[0].clone(), + ) + .unwrap(); + node_1_tms + .send_text_message(node_3_identity.identity.public_key.clone(), "Say Hello".to_string()) + .unwrap(); + + node_2_tms + .send_text_message(node_1_identity.identity.public_key.clone(), "hello?".to_string()) + .unwrap(); + node_1_tms + .send_text_message( + node_2_identity.identity.public_key.clone(), + node1_to_node2_sent_messages[1].clone(), + ) + .unwrap(); + + for i in 0..3 { + node1_to_node2_sent_messages.push(format!("Message {}", i).to_string()); + node_1_tms + .send_text_message( + node_2_identity.identity.public_key.clone(), + node1_to_node2_sent_messages[2 + i].clone(), + ) + .unwrap(); + } + for i in 0..3 { + node_2_tms + .send_text_message( + node_1_identity.identity.public_key.clone(), + format!("Message {}", i).to_string(), + ) + .unwrap(); + } + + assert_change( + || { + let msgs = node_1_tms.get_text_messages().unwrap(); + + (msgs.sent_messages.len(), msgs.received_messages.len()) + }, + (6, 4), + 50, + ); + + assert_change( + || { + let msgs = node_2_tms.get_text_messages().unwrap(); + (msgs.sent_messages.len(), msgs.received_messages.len()) + }, + (4, 5), + 50, + ); + + let node1_msgs = node_1_tms + .get_text_messages_by_pub_key(node_2_identity.identity.public_key) + .unwrap(); + + assert_eq!(node1_msgs.sent_messages.len(), node1_to_node2_sent_messages.len()); + for i in 0..node1_to_node2_sent_messages.len() { + assert_eq!(node1_msgs.sent_messages[i].message, node1_to_node2_sent_messages[i]); + } + + let node2_msgs = node_2_tms + .get_text_messages_by_pub_key(node_1_identity.identity.public_key) + .unwrap(); + + assert_eq!(node2_msgs.received_messages.len(), node1_to_node2_sent_messages.len()); + for i in 0..node1_to_node2_sent_messages.len() { + assert_eq!(node2_msgs.received_messages[i].message, node1_to_node2_sent_messages[i]); + } + + node_1_services.shutdown().unwrap(); + node_2_services.shutdown().unwrap(); + node_3_services.shutdown().unwrap(); + clean_up_datastore(node_1_database_name); + clean_up_datastore(node_2_database_name); + clean_up_datastore(node_3_database_name); + clean_up_sql_database(db_name1); + clean_up_sql_database(db_name2); + clean_up_sql_database(db_name3); +} diff --git a/base_layer/wallet/tests/transaction_service/mod.rs b/base_layer/wallet/tests/transaction_service/mod.rs new file mode 100644 index 0000000000..8aaf20a27a --- /dev/null +++ b/base_layer/wallet/tests/transaction_service/mod.rs @@ -0,0 +1,312 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use crate::support::{ + comms_and_services::setup_comms_services, + data::{clean_up_datastore, init_datastore}, + utils::assert_change, +}; +use log::Level; +use rand::{CryptoRng, OsRng, Rng}; +use std::sync::Arc; +use tari_comms::{builder::CommsServices, peer_manager::NodeIdentity}; +use tari_core::{ + tari_amount::*, + transaction::{OutputFeatures, TransactionInput, UnblindedOutput}, + transaction_protocol::recipient::RecipientState, + types::{PrivateKey, PublicKey, COMMITMENT_FACTORY}, +}; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::{PublicKey as PK, SecretKey as SK}, +}; +use tari_p2p::{ + sync_services::{ServiceExecutor, ServiceRegistry}, + tari_message::TariMessageType, +}; +use tari_storage::lmdb_store::LMDBDatabase; +use tari_wallet::{ + output_manager_service::output_manager_service::{OutputManagerService, OutputManagerServiceApi}, + transaction_service::{TransactionService, TransactionServiceApi}, +}; +pub fn setup_transaction_service( + seed_key: PrivateKey, + node_identity: NodeIdentity, + peers: Vec, + peer_database: LMDBDatabase, +) -> ( + ServiceExecutor, + Arc, + Arc, + CommsServices, +) +{ + let output_manager = OutputManagerService::new(seed_key, "".to_string(), 0); + let output_manager_api = output_manager.get_api(); + let tx_service = TransactionService::new(output_manager_api.clone()); + let tx_service_api = tx_service.get_api(); + let services = ServiceRegistry::new().register(tx_service).register(output_manager); + let comms = setup_comms_services(node_identity, peers, peer_database); + + ( + ServiceExecutor::execute(&comms, services), + tx_service_api, + output_manager_api, + comms, + ) +} +pub fn make_input(rng: &mut R, val: MicroTari) -> (TransactionInput, UnblindedOutput) { + let key = PrivateKey::random(rng); + let commitment = COMMITMENT_FACTORY.commit_value(&key, val.into()); + let input = TransactionInput::new(OutputFeatures::default(), commitment); + (input, UnblindedOutput::new(val, key, None)) +} +pub struct TestParams { + pub spend_key: PrivateKey, + pub change_key: PrivateKey, + pub offset: PrivateKey, + pub nonce: PrivateKey, + pub public_nonce: PublicKey, +} +impl TestParams { + pub fn new(rng: &mut R) -> TestParams { + let r = PrivateKey::random(rng); + TestParams { + spend_key: PrivateKey::random(rng), + change_key: PrivateKey::random(rng), + offset: PrivateKey::random(rng), + public_nonce: PublicKey::from_secret_key(&r), + nonce: r, + } + } +} + +#[test] +fn manage_single_transaction() { + let mut rng = OsRng::new().unwrap(); + // Alice's parameters + let alice_seed = PrivateKey::random(&mut rng); + let alice_node_identity = NodeIdentity::random(&mut rng, "127.0.0.1:31583".parse().unwrap()).unwrap(); + let alice_database_name = "alice_test_tx_service1"; // Note: every test should have unique database + let alice_datastore = init_datastore(alice_database_name).unwrap(); + let alice_peer_database = alice_datastore.get_handle(alice_database_name).unwrap(); + // Bob's parameters + let bob_seed = PrivateKey::random(&mut rng); + let bob_node_identity = NodeIdentity::random(&mut rng, "127.0.0.1:31582".parse().unwrap()).unwrap(); + let bob_database_name = "bob_test_tx_service1"; // Note: every test should have unique database + let bob_datastore = init_datastore(bob_database_name).unwrap(); + let bob_peer_database = bob_datastore.get_handle(bob_database_name).unwrap(); + + let (alice_services, alice_tx_api, alice_oms_api, _alice_comms) = setup_transaction_service( + alice_seed, + alice_node_identity.clone(), + vec![bob_node_identity.clone()], + alice_peer_database, + ); + + let value = MicroTari::from(1000); + let (_utxo, uo1) = make_input(&mut rng, MicroTari(2500)); + + assert!(alice_tx_api + .send_transaction( + bob_node_identity.identity.public_key.clone(), + value, + MicroTari::from(20), + ) + .is_err()); + + alice_oms_api.add_output(uo1).unwrap(); + + alice_tx_api + .send_transaction( + bob_node_identity.identity.public_key.clone(), + value, + MicroTari::from(20), + ) + .unwrap(); + + let alice_pending_outbound = alice_tx_api.get_pending_outbound_transaction().unwrap(); + let alice_completed_tx = alice_tx_api.get_completed_transaction().unwrap(); + assert_eq!(alice_pending_outbound.len(), 1); + assert_eq!(alice_completed_tx.len(), 0); + + let (bob_services, bob_tx_api, bob_oms_api, _bob_comms) = setup_transaction_service( + bob_seed, + bob_node_identity.clone(), + vec![alice_node_identity.clone()], + bob_peer_database, + ); + + assert_change(|| alice_tx_api.get_completed_transaction().unwrap().len(), 1, 50); + + let alice_pending_outbound = alice_tx_api.get_pending_outbound_transaction().unwrap(); + let alice_completed_tx = alice_tx_api.get_completed_transaction().unwrap(); + assert_eq!(alice_pending_outbound.len(), 0); + assert_eq!(alice_completed_tx.len(), 1); + + let bob_pending_inbound_tx = bob_tx_api.get_pending_inbound_transaction().unwrap(); + assert_eq!(bob_pending_inbound_tx.len(), 1); + + let mut alice_tx_id = 0; + for (k, _v) in alice_completed_tx.iter() { + alice_tx_id = k.clone(); + } + for (k, v) in bob_pending_inbound_tx.iter() { + assert_eq!(*k, alice_tx_id); + if let RecipientState::Finalized(rsm) = &v.state { + bob_oms_api + .confirm_received_output(alice_tx_id, rsm.output.clone()) + .unwrap(); + assert_eq!(bob_oms_api.get_balance().unwrap(), value); + } else { + assert!(false); + } + } + + alice_services.shutdown().unwrap(); + bob_services.shutdown().unwrap(); + clean_up_datastore(alice_database_name); + clean_up_datastore(bob_database_name); +} + +#[test] +fn manage_multiple_transactions() { + let _ = simple_logger::init_with_level(Level::Debug); + let mut rng = OsRng::new().unwrap(); + // Alice's parameters + let alice_seed = PrivateKey::random(&mut rng); + let alice_node_identity = NodeIdentity::random(&mut rng, "127.0.0.1:31584".parse().unwrap()).unwrap(); + let alice_database_name = "alice_test_tx_service2"; // Note: every test should have unique database + let alice_datastore = init_datastore(alice_database_name).unwrap(); + let alice_peer_database = alice_datastore.get_handle(alice_database_name).unwrap(); + // Bob's parameters + let bob_seed = PrivateKey::random(&mut rng); + let bob_node_identity = NodeIdentity::random(&mut rng, "127.0.0.1:31585".parse().unwrap()).unwrap(); + let bob_database_name = "bob_test_tx_service2"; // Note: every test should have unique database + let bob_datastore = init_datastore(bob_database_name).unwrap(); + let bob_peer_database = bob_datastore.get_handle(bob_database_name).unwrap(); + // Carols's parameters + let carol_seed = PrivateKey::random(&mut rng); + let carol_node_identity = NodeIdentity::random(&mut rng, "127.0.0.1:31586".parse().unwrap()).unwrap(); + let carol_database_name = "carol_test_tx_service2"; // Note: every test should have unique database + let carol_datastore = init_datastore(carol_database_name).unwrap(); + let carol_peer_database = carol_datastore.get_handle(carol_database_name).unwrap(); + let (alice_services, alice_tx_api, alice_oms_api, _alice_comms) = setup_transaction_service( + alice_seed, + alice_node_identity.clone(), + vec![bob_node_identity.clone(), carol_node_identity.clone()], + alice_peer_database, + ); + + // Add some funds to Alices wallet + let (_utxo, uo1a) = make_input(&mut rng, MicroTari(5500)); + alice_oms_api.add_output(uo1a).unwrap(); + let (_utxo, uo1b) = make_input(&mut rng, MicroTari(3000)); + alice_oms_api.add_output(uo1b).unwrap(); + let (_utxo, uo1c) = make_input(&mut rng, MicroTari(3000)); + alice_oms_api.add_output(uo1c).unwrap(); + + // A series of interleaved transactions. First with Bob and Carol offline and then two with them online + let value_a_to_b_1 = MicroTari::from(1000); + let value_a_to_b_2 = MicroTari::from(800); + let value_b_to_a_1 = MicroTari::from(1100); + let value_a_to_c_1 = MicroTari::from(1400); + alice_tx_api + .send_transaction( + bob_node_identity.identity.public_key.clone(), + value_a_to_b_1, + MicroTari::from(20), + ) + .unwrap(); + alice_tx_api + .send_transaction( + carol_node_identity.identity.public_key.clone(), + value_a_to_c_1, + MicroTari::from(20), + ) + .unwrap(); + let alice_pending_outbound = alice_tx_api.get_pending_outbound_transaction().unwrap(); + let alice_completed_tx = alice_tx_api.get_completed_transaction().unwrap(); + assert_eq!(alice_pending_outbound.len(), 2); + assert_eq!(alice_completed_tx.len(), 0); + + // Spin up Bob and Carol + let (bob_services, bob_tx_api, bob_oms_api, _bob_comms) = setup_transaction_service( + bob_seed, + bob_node_identity.clone(), + vec![alice_node_identity.clone()], + bob_peer_database, + ); + let (carol_services, carol_tx_api, carol_oms_api, _carol_comms) = setup_transaction_service( + carol_seed, + carol_node_identity.clone(), + vec![alice_node_identity.clone()], + carol_peer_database, + ); + let (_utxo, uo2) = make_input(&mut rng, MicroTari(3500)); + bob_oms_api.add_output(uo2).unwrap(); + let (_utxo, uo3) = make_input(&mut rng, MicroTari(4500)); + carol_oms_api.add_output(uo3).unwrap(); + + bob_tx_api + .send_transaction( + alice_node_identity.identity.public_key.clone(), + value_b_to_a_1, + MicroTari::from(20), + ) + .unwrap(); + alice_tx_api + .send_transaction( + bob_node_identity.identity.public_key.clone(), + value_a_to_b_2, + MicroTari::from(20), + ) + .unwrap(); + + assert_change(|| alice_tx_api.get_completed_transaction().unwrap().len(), 3, 50); + + let alice_pending_outbound = alice_tx_api.get_pending_outbound_transaction().unwrap(); + let alice_completed_tx = alice_tx_api.get_completed_transaction().unwrap(); + assert_eq!(alice_pending_outbound.len(), 0); + assert_eq!(alice_completed_tx.len(), 3); + let bob_pending_outbound = bob_tx_api.get_pending_outbound_transaction().unwrap(); + let bob_completed_tx = bob_tx_api.get_completed_transaction().unwrap(); + assert_eq!(bob_pending_outbound.len(), 0); + assert_eq!(bob_completed_tx.len(), 1); + let carol_pending_inbound = carol_tx_api.get_pending_inbound_transaction().unwrap(); + assert_eq!(carol_pending_inbound.len(), 1); + + alice_services.shutdown().unwrap(); + bob_services.shutdown().unwrap(); + carol_services.shutdown().unwrap(); + clean_up_datastore(alice_database_name); + clean_up_datastore(bob_database_name); + clean_up_datastore(carol_database_name); +} + +// TODO Test the following once the Tokio future based service architecture is in place. The current architecture +// makes it impossible to test this service without a running Service and Comms stack but then you cannot access the +// internals of the service as it is running the ServiceExecutor Thread +// +// What happens when repeated tx_id are sent to be accepted +// What happens with malformed sender message +// What happens with malformed recipient message +// What happens when accepting recipient reply for unknown tx_id diff --git a/base_layer/wallet/tests/wallet/mod.rs b/base_layer/wallet/tests/wallet/mod.rs new file mode 100644 index 0000000000..2649748f17 --- /dev/null +++ b/base_layer/wallet/tests/wallet/mod.rs @@ -0,0 +1,199 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support::utils::assert_change; +use std::{path::PathBuf, time::Duration}; +use tari_comms::{ + connection::{net_address::NetAddressWithStats, NetAddress, NetAddressesWithStats}, + control_service::ControlServiceConfig, + peer_manager::{peer::PeerFlags, NodeId, Peer}, + types::{CommsPublicKey, CommsSecretKey}, +}; +use tari_crypto::keys::{PublicKey, SecretKey}; +use tari_p2p::initialization::CommsConfig; +use tari_wallet::{text_message_service::Contact, wallet::WalletConfig, Wallet}; + +fn create_peer(public_key: CommsPublicKey, net_address: NetAddress) -> Peer { + Peer::new( + public_key.clone(), + NodeId::from_key(&public_key).unwrap(), + NetAddressesWithStats::new(vec![NetAddressWithStats::new(net_address.clone())]), + PeerFlags::empty(), + ) +} + +fn get_path(name: Option<&str>) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name.unwrap_or("")); + path.to_str().unwrap().to_string() +} + +fn clean_up_datastore(name: &str) { + std::fs::remove_dir_all(get_path(Some(name))).unwrap(); +} + +fn clean_up_sql_database(name: &str) { + if std::fs::metadata(get_path(Some(name))).is_ok() { + std::fs::remove_file(get_path(Some(name))).unwrap(); + } +} + +fn init_sql_database(name: &str) { + clean_up_sql_database(name); + let path = get_path(None); + let _ = std::fs::create_dir(&path).unwrap_or_default(); +} + +#[test] +fn test_wallet() { + let mut rng = rand::OsRng::new().unwrap(); + + let db_name1 = "test_wallet1.sqlite3"; + let db_path1 = get_path(Some(db_name1)); + init_sql_database(db_name1); + + let db_name2 = "test_wallet2.sqlite3"; + let db_path2 = get_path(Some(db_name2)); + init_sql_database(db_name2); + + let listener_address1: NetAddress = "127.0.0.1:32775".parse().unwrap(); + let secret_key1 = CommsSecretKey::random(&mut rng); + let public_key1 = CommsPublicKey::from_secret_key(&secret_key1); + let wallet1_peer_database_name = "wallet1_peer_database".to_string(); + let config1 = WalletConfig { + comms: CommsConfig { + control_service: ControlServiceConfig { + listener_address: listener_address1.clone(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(5000), + }, + socks_proxy_address: None, + host: "127.0.0.1".parse().unwrap(), + public_key: public_key1.clone(), + secret_key: secret_key1, + public_address: listener_address1.clone(), + datastore_path: get_path(Some(&wallet1_peer_database_name)), + peer_database_name: wallet1_peer_database_name.clone(), + }, + public_key: public_key1.clone(), + database_path: db_path1, + }; + let wallet1 = Wallet::new(config1).unwrap(); + + let listener_address2: NetAddress = "127.0.0.1:32776".parse().unwrap(); + let secret_key2 = CommsSecretKey::random(&mut rng); + let public_key2 = CommsPublicKey::from_secret_key(&secret_key2); + let wallet2_peer_database_name = "wallet2_peer_database".to_string(); + let config2 = WalletConfig { + comms: CommsConfig { + control_service: ControlServiceConfig { + listener_address: listener_address2.clone(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(5000), + }, + socks_proxy_address: None, + host: "127.0.0.1".parse().unwrap(), + public_key: public_key2.clone(), + secret_key: secret_key2, + public_address: listener_address2.clone(), + datastore_path: get_path(Some(&wallet2_peer_database_name)), + peer_database_name: wallet2_peer_database_name.clone(), + }, + public_key: public_key2.clone(), + database_path: db_path2, + }; + + let wallet2 = Wallet::new(config2).unwrap(); + + wallet1 + .comms_services + .peer_manager() + .add_peer(create_peer(public_key2.clone(), listener_address2.clone())) + .unwrap(); + + wallet2 + .comms_services + .peer_manager() + .add_peer(create_peer(public_key1.clone(), listener_address1.clone())) + .unwrap(); + + wallet1 + .text_message_service + .add_contact(Contact::new( + "Alice".to_string(), + public_key2.clone(), + listener_address2, + )) + .unwrap(); + + wallet2 + .text_message_service + .add_contact(Contact::new("Bob".to_string(), public_key1.clone(), listener_address1)) + .unwrap(); + + wallet1 + .text_message_service + .send_text_message(public_key2.clone(), "Say Hello,".to_string()) + .unwrap(); + + wallet2 + .text_message_service + .send_text_message(public_key1.clone(), "hello?".to_string()) + .unwrap(); + + wallet1 + .text_message_service + .send_text_message(public_key2.clone(), "to my little friend!".to_string()) + .unwrap(); + + assert_change( + || { + let msgs = wallet1.text_message_service.get_text_messages().unwrap(); + (msgs.sent_messages.len(), msgs.received_messages.len()) + }, + (2, 1), + 50, + ); + + assert_change( + || { + let msgs = wallet2.text_message_service.get_text_messages().unwrap(); + (msgs.sent_messages.len(), msgs.received_messages.len()) + }, + (1, 2), + 50, + ); + + wallet1.ping_pong_service.ping(public_key2.clone()).unwrap(); + wallet2.ping_pong_service.ping(public_key1.clone()).unwrap(); + + assert_change(|| wallet1.ping_pong_service.ping_count().unwrap(), 2, 20); + assert_change(|| wallet1.ping_pong_service.pong_count().unwrap(), 2, 20); + assert_change(|| wallet2.ping_pong_service.ping_count().unwrap(), 2, 20); + assert_change(|| wallet2.ping_pong_service.pong_count().unwrap(), 2, 20); + + clean_up_datastore(&wallet1_peer_database_name); + clean_up_datastore(&wallet2_peer_database_name); + clean_up_sql_database(db_name1); + clean_up_sql_database(db_name2); +} diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 0000000000..5771c8f07a --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tari_common" +description = "Utilities and features for the Tari domain layer, shared across both Base and Digital Asset layers." +repository = "https://github.com/tari-project/tari" +homepage = "https://tari.com" +readme = "README.md" +license = "BSD-3-Clause" +version = "0.0.5" +edition = "2018" + + +[dependencies] +dirs = "2.0" +log4rs = "0.8.3" diff --git a/common/README.md b/common/README.md new file mode 100644 index 0000000000..6cd207728c --- /dev/null +++ b/common/README.md @@ -0,0 +1,6 @@ +# Tari Common + +This crate provides commonly-used features that are shared across multiple Tari _domain layers_. Since they may refer to +domain-level concepts, it's inappropriate to place them inside the `infrastructure` folder, `tari-utilities` +specifically. + diff --git a/common/config/tari_config_sample.toml b/common/config/tari_config_sample.toml new file mode 100644 index 0000000000..fd4a104d9a --- /dev/null +++ b/common/config/tari_config_sample.toml @@ -0,0 +1,117 @@ +######################################################################################################################## +# # +# The Tari Network Configuration File # +# # +######################################################################################################################## + +# This file carries all the configuration options for running Tari-related nodes and infrastructure in one single +# file. As you'll notice, almost all configuraton options are commented out. This is because they are either not +# needed, are for advanced users that know what they want to tweak, or are already set at their default values. If +# things are working fine, then there's no need to change anything here. +# +# Each major section is clearly marked so that you can quickly find the section you're looking for. This first +# section holds configuration options that are common to all sections. + +# A note about Logging - The logger is initialised before the configuration file is loaded. For this reason, logging +# is not configured here, but in `~/.tari/log4rs.yml` (*nix / OsX) or `%HOME%/.tari/log4rs.yml` (Windows) by +# default, or the location specified in the TARI_LOGFILE environment variable. + +[common] +# Tari is a 100% peer-to-peer network, so there are no servers to hold messages for you while you're offline. +# Instead, we rely on our peers to hold messages for us while we're offline. This settings sets maximum size of the +# message cache that for holding our peers' messages, in MB. +#message_cache_size = 10 + +# When storing messages for peers, hold onto them for at most this long before discarding them. The default is 1440 +# minutes = or 24 hrs. +#message_cache_ttl = 1440 + +# When first logging onto the Tari network, you need to find a few peers to bootstrap the process. In the absence of +# any servers, this is a little more challenging than usual. Our best strategy is just to try and connect to the peers +# you knew about last time you ran the software. But what about when you run the software for the first time? That's +# where this whitelist comes in. It's a list of known Tari nodes that are likely to be around for a long time and that +# new nodes can use to introduce themselves to the network. +peer_whitelist = [ "8.8.8.8", "6.6.6.6", "7.7.7.7"] # TODO actually populate this with a list of bootstrap nodes + +# The peer database list is stored in a database file at this location +#peer_database = "~/.tari/peers" + +# If peer nodes spam you with messages, or are otherwise badly behaved, they will be added to your blacklist and banned +# You can set a time limit to release that ban (in minutes), or otherwise ban them for life (-1). The default is to +# ban them for 10 days. +#blacklist_ban_period = 1440 + + +######################################################################################################################## +# # +# Wallet Configuration Options # +# # +######################################################################################################################## + +# If you are not running a wallet from this configuration, you can simply leave everything in this section commented out + +[wallet] + +# Enable the gRPC server for the wallet library. Set this to true if you want to enable third-party wallet software +#grpc_enabled = true + +# The socket to expose for the gRPC wallet server. This value is ignored if grpc_enabled is false. +# Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") +#grpc_address = "tcp://127.0.0.1:80400" + +# The folder to store your local key data and transaction history. DO NOT EVER DELETE THIS FILE unless you +# a) have backed up your seed phrase and +# b) know what you are doing! +#wallet_file = "~/.tari/wallet/wallet.dat" + +######################################################################################################################## +# # +# Base Node Configuration Options # +# # +######################################################################################################################## + +# If you are not running a Tari Base node, you can simply leave everything in this section commented out. Base nodes +# help maintain the security of the Tari token and are the surest way to preserve your privacy and be 100% sure that +# no-one is cheating you out of your money. + +[base_node] + +# Select the netowrk to connect to. Valid options are: +# mainnet - the "real" Tari network (default) +# testnet - the Tari test net +# localnet - a local blockchain environmment +#network = "mainnet" + +# The folder to use to store the blockchain database +#blochchain-data = "~/.tari/chain_data/" + +# The address and port to listen for peer connections. This is the address that is advertised on the network so that +# peers can find you. You may specify more than one address here +#[[base_node.control-address]] +#address = "tcp://0.0.0.0:80898" + +# Enable the gRPC server for the base node. Set this to true if you want to enable third-party wallet software +#grpc_enabled = false + +# The socket to expose for the gRPC base node server. This value is ignored if grpc_enabled is false. +# Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") +#grpc_address = "tcp://127.0.0.1:80410" + +######################################################################################################################## +# # +# Validator Node Configuration Options # +# # +######################################################################################################################## + +# If you are not , you can simply leave everything in this section commented out. Base nodes +# help maintain the security of the Tari token and are the surest way to preserve your privacy and be 100% sure that +# no-one is cheating you out of your money. + +[validator_node] + +# Enable the gRPC server for the base node. Set this to true if you want to enable third-party wallet software +#grpc_enabled = false + +# The socket to expose for the gRPC base node server. This value is ignored if grpc_enabled is false. +# Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") +#grpc_address = "tcp://127.0.0.1:80420" \ No newline at end of file diff --git a/common/logging/README.md b/common/logging/README.md new file mode 100644 index 0000000000..5defa73571 --- /dev/null +++ b/common/logging/README.md @@ -0,0 +1,43 @@ +# Logging in Tari + +All source Tari modules use the standard `log` crate to write log messages. This provides flexible options to the end +user (tests / applications / dynamic libraries) as to how logs are managed. + +## Setup for no logging +If you're writing tests or applications on Tari and don't want to see log messages, do nothing. Log messages are +suppressed by default. + +## Setup for stdout logging + +This setup is usually used for tests. + +If you want messages dumped to stdout without any fancy configuration, include `simple-logger` in your `Cargo.toml` +file, under `dependencies` or `dev-dependencies` as appropriate and then initialise the logger at the top of your tests +or application with `simple-logger::init_logger()`. + +## Bespoke and file-based logging + +This setup is usually used for applications. + +`log-4rs` is a really handy crate that allows you to specify _exactly_ how and where log messages are put. The sample +configuration files in this directory provide a good start for setting up a useful logging solution. + +The `log4rs-sample.yml` file defines a configuration where only error messages are written to the console, typically low +signal-to-noise comms messages are stored in one file, and general log messages are stored in another. + +The `log4rs-debug-sample.rs` file has a similar setup, but logs more information useful for debugging, such as the +source code line number, and the thread that caused the log message to be emitted. + +You can use these files as a starting point, or create your own. + +To set up logging at the application level, we recommend the following pattern: + +1. Call `log4rs::init_file(path, Default::default()).unwrap();` as soon as possible in your app, possibly as the very + first line. +2. Obtain the path to the log configuration file. By convention, the following precedence set is recommended: + 1. from a command-line parameter, `log-configuration`, + 2. from the `TARI_LOG_CONFIGURATION` environment variable, + 3. from a default value, usually `~/.tari/log4rs.yml` (or OS equivalent). + +There is a convenience function provided by this crate that will provide the path for you, see +`get_log4rs_configuration_path()` \ No newline at end of file diff --git a/common/logging/log4rs-debug-sample.yml b/common/logging/log4rs-debug-sample.yml new file mode 100644 index 0000000000..4c92758139 --- /dev/null +++ b/common/logging/log4rs-debug-sample.yml @@ -0,0 +1,52 @@ +# A sample log configuration file for running in debug mode. By default, this configuration splits up log messages to +# three destinations: +# * Console: For log messages with level WARN and higher +# * log/network-debug.log: Debug-level logs related to the comms crate. This file will be very busy since there +# are lots of P2P debug messages, and so this traffic is segregated from the application log messages +# * log/base_layer-debug.log: Non-comms related Debug-level messages and higher are logged into this file +# +# See https://docs.rs/log4rs/0.8.3/log4rs/encode/pattern/index.html for deciphering the log pattern. The log format +# used in this sample configuration prints messages as: +# timestamp [source file#lno] [target] LEVEL message (thread) + +appenders: + # An appender named "stdout" that writes to stdout + stdout: + kind: console + encoder: + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{M}#{L}] [{t}] {h({l}):5} {m} (({T}:{I})){n}" + + # An appender named "network" that writes to a file with a custom pattern encoder + network: + kind: file + path: "log/network-debug.log" + encoder: + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{M}#{L}] [{t}] {l:5} {m} (({T}:{I})){n}" + + # An appender named "base_layer" that writes to a file with a custom pattern encoder + base_layer: + kind: file + path: "log/base_layer-debug.log" + encoder: + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{M}#{L}] [{t}] {l:5} {m} (({T}:{I})){n}" + +# Set the default logging level to "debug" and attach the "base_layer" appender to the root +root: + level: debug + appenders: + - base_layer + +loggers: + # Set the maximum console output to "warn" + stdout: + level: warn + appenders: + - stdout + additive: false + + # Route log events sent to the "comms" logger to the "network" appender + comms: + level: debug + appenders: + - network + additive: false \ No newline at end of file diff --git a/common/logging/log4rs-sample.yml b/common/logging/log4rs-sample.yml new file mode 100644 index 0000000000..4b1696b15a --- /dev/null +++ b/common/logging/log4rs-sample.yml @@ -0,0 +1,52 @@ +# A sample log configuration file for running in release mode. By default, this configuration splits up log messages to +# three destinations: +# * Console: For log messages with level ERROR and higher +# * log/network.log: INFO-level logs related to the comms crate. This file will be quite busy since there +# are lots of P2P debug messages, and so this traffic is segregated from the application log messages +# * log/base_layer.log: Non-comms related WARN-level messages and higher are logged into this file +# +# See https://docs.rs/log4rs/0.8.3/log4rs/encode/pattern/index.html for deciphering the log pattern. The log format +# used in this sample configuration prints messages as: +# timestamp [target] LEVEL message + +appenders: + # An appender named "stdout" that writes to stdout + stdout: + kind: console + encoder: + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {h({l}):5} {m}{n}" + + # An appender named "network" that writes to a file with a custom pattern encoder + network: + kind: file + path: "log/network.log" + encoder: + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}" + + # An appender named "base_layer" that writes to a file with a custom pattern encoder + base_layer: + kind: file + path: "log/base_layer.log" + encoder: + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}" + +# Set the default logging level to "error" and attach the "stdout" appender to the root +root: + level: warn + appenders: + - base_layer + +loggers: + # Set the maximum console output to "error" + stdout: + level: error + appenders: + - stdout + additive: false + + # Route log events sent to the "comms" logger to the "network" appender + comms: + level: info + appenders: + - network + additive: false \ No newline at end of file diff --git a/common/src/lib.rs b/common/src/lib.rs new file mode 100644 index 0000000000..89c7addb9a --- /dev/null +++ b/common/src/lib.rs @@ -0,0 +1,67 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use dirs; +use std::{env, path::PathBuf}; + +/// Determine the path to a log configuration file using the following precedence rules: +/// 1. Use the provided path (usually pulled from a CLI argument) +/// 2. Use the value in the `TARI_LOG_CONFIGURATION` envar +/// 3. The default path (OS-dependent), "~/.tari/log4rs.toml` +/// 4. The current directory +pub fn get_log_configuration_path(cli_path: Option) -> PathBuf { + cli_path + .or_else(|| { + env::var_os("TARI_LOG_CONFIGURATION") + .filter(|s| !s.is_empty()) + .map(PathBuf::from) + }) + .or_else(|| dirs::home_dir().map(|path| path.join(".tari/log4rs.toml"))) + .or_else(|| { + Some(env::current_dir().expect( + "Could find a suitable path to the log configuration file. Consider setting the \ + TARI_LOG_CONFIGURATION envar, or check that the current directory exists and that you have \ + permission to read it", + )) + }) + .unwrap() +} + +#[cfg(test)] +mod test { + use super::*; + use std::env; + + #[test] + fn get_log_configuration_path_cli() { + let path = get_log_configuration_path(Some(PathBuf::from("~/my-tari"))); + assert_eq!(path.to_str().unwrap(), "~/my-tari"); + } + + #[test] + fn get_log_configuration_path_by_env_var() { + env::set_var("TARI_LOG_CONFIGURATION", "~/fake-example"); + let path = get_log_configuration_path(None); + assert_eq!(path.to_str().unwrap(), "~/fake-example"); + env::set_var("TARI_LOG_CONFIGURATION", ""); + } +} diff --git a/comms/Cargo.toml b/comms/Cargo.toml new file mode 100644 index 0000000000..361003347f --- /dev/null +++ b/comms/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "tari_comms" +description = "A peer-to-peer messaging system" +repository = "https://github.com/tari-project/tari" +homepage = "https://tari.com" +readme = "README.md" +license = "BSD-3-Clause" +version = "0.0.5" +edition = "2018" + +[dependencies] +tari_crypto = { version="^0.0", path = "../infrastructure/crypto" } +tari_utilities = { version="^0.0", path = "../infrastructure/tari_util" } +tari_storage = { version="^0.0", path = "../infrastructure/storage" } + +bitflags ="1.0.4" +bus_queue = { git = "https://github.com/tari-project/bus-queue.git"} +chrono = { version = "0.4.6", features = ["serde"] } +clear_on_drop = "0.2.3" +crossbeam-channel = "0.3.9" +crossbeam-deque = "0.7.1" +derive-error = "0.0.4" +digest = "0.8.0" +futures = { version = "=0.3.0-alpha.18", package = "futures-preview", features = ["async-await", "nightly", "io-compat", "compat"] } +lazy_static = "1.3.0" +lmdb-zero = "0.4.4" +log = { version = "0.4.0", features = ["std"] } +rand = "0.5.5" +serde = "1.0.90" +serde_derive = "1.0.90" +time = "0.1.42" +tokio = "0.1" +ttl_cache = "0.5.1" +zmq = "0.9.1" + +[dev-dependencies] +criterion = "0.2" +rand = "0.5.5" +simple_logger = "1.2.0" +serde_json = "1.0.39" +tari_common = { path = "../common"} +tokio-mock-task = "0.1.1" + +[[bench]] +name = "benches_main" +harness = false diff --git a/comms/README.md b/comms/README.md new file mode 100644 index 0000000000..0f2d2b4282 --- /dev/null +++ b/comms/README.md @@ -0,0 +1,5 @@ +# Tari Common Comms + +The Tari Common Comms crate provide communication features shared across both the Base layer and Digital Asset Network. +This crate is part of the [Tari Cryptocurrency](https://tari.com) project. + diff --git a/comms/benches/benches_main.rs b/comms/benches/benches_main.rs new file mode 100644 index 0000000000..306dc4124b --- /dev/null +++ b/comms/benches/benches_main.rs @@ -0,0 +1,30 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[macro_use] +extern crate criterion; +#[macro_use] +extern crate lazy_static; + +mod connection; + +criterion_main!(connection::connection::benches, connection::peer_connection::benches,); diff --git a/comms/benches/connection/connection.rs b/comms/benches/connection/connection.rs new file mode 100644 index 0000000000..d734fbcf65 --- /dev/null +++ b/comms/benches/connection/connection.rs @@ -0,0 +1,58 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use criterion::Criterion; + +use std::{iter::repeat, time::Duration}; +use tari_comms::connection::{Connection, Direction, InprocAddress, ZmqContext}; + +/// Connection benchmark +/// +/// Benchmark a message being sent and received between two connections +fn bench_connection(c: &mut Criterion) { + let ctx = ZmqContext::new(); + let addr = InprocAddress::random(); + + let bytes = repeat(88u8).take(1 * 1024 * 1024 /* 10Mb */).collect::>(); + let receiver = Connection::new(&ctx, Direction::Inbound) + .set_receive_hwm(0) + .establish(&addr) + .unwrap(); + + let sender = Connection::new(&ctx, Direction::Outbound) + .set_send_hwm(0) + .establish(&addr) + .unwrap(); + + c.bench_function("connection: send/recv", move |b| { + b.iter(|| { + sender.send(&[bytes.as_slice()]).unwrap(); + receiver.receive(100).unwrap(); + }); + }); +} + +criterion_group!( + name = benches; + config = Criterion::default().warm_up_time(Duration::from_millis(500)); + targets = bench_connection, +); diff --git a/infrastructure/comms/src/connection/i2p/mod.rs b/comms/benches/connection/mod.rs similarity index 98% rename from infrastructure/comms/src/connection/i2p/mod.rs rename to comms/benches/connection/mod.rs index 8418dd7dbf..8d96000d1c 100644 --- a/infrastructure/comms/src/connection/i2p/mod.rs +++ b/comms/benches/connection/mod.rs @@ -21,3 +21,4 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pub mod connection; +pub mod peer_connection; diff --git a/comms/benches/connection/peer_connection.rs b/comms/benches/connection/peer_connection.rs new file mode 100644 index 0000000000..683b306a9d --- /dev/null +++ b/comms/benches/connection/peer_connection.rs @@ -0,0 +1,200 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use criterion::Criterion; + +use std::{ + iter::repeat, + sync::mpsc::{channel, sync_channel, Sender, SyncSender}, + thread, + time::Duration, +}; +use tari_comms::{ + connection::{ + peer_connection::PeerConnectionContext, + Connection, + Direction, + InprocAddress, + NetAddress, + PeerConnection, + PeerConnectionContextBuilder, + ZmqContext, + }, + message::FrameSet, +}; + +const BENCH_SOCKET_ADDRESS: &'static str = "127.0.0.1:9999"; + +/// Set the allocated stack size for each WorkerTask thread +const THREAD_STACK_SIZE: usize = 16 * 1024; // 16kb + +lazy_static! { + static ref DUMMY_DATA: FrameSet = vec![ + repeat(88u8).take(512 * 1024).collect::>(), + repeat(99u8).take(512 * 1024).collect::>(), + ]; // 1 MiB +} + +/// Task messages for the consumer thread +enum WorkerTask { + /// Receive a message + Receive, + /// Receive a message and then send it back + ReceiveSend, + /// Exit the loop (thread terminates) + Exit, +} + +/// Build a context for the connections to be used in the benchmark +fn build_context( + ctx: &ZmqContext, + dir: Direction, + addr: &NetAddress, + consumer_addr: &InprocAddress, +) -> PeerConnectionContext +{ + PeerConnectionContextBuilder::new() + .set_id("benchmark") + .set_direction(dir) + .set_max_msg_size(512 * 1024) + .set_address(addr.clone()) + .set_message_sink_address(consumer_addr.clone()) + .set_context(ctx) + .build() + .unwrap() +} + +/// Start a consumer thread. +/// This listens for control messages, execute the given task and signal when it's done (`signal` field) +fn start_message_sink_consumer( + ctx: &ZmqContext, + addr: &InprocAddress, + peer_conn: &PeerConnection, + signal: Sender<()>, +) -> SyncSender +{ + let ctx = ctx.clone(); + let addr = addr.clone(); + let peer_conn = peer_conn.clone(); + let (tx, rx) = sync_channel(2); + thread::Builder::new() + .name("peer-connection-consumer-thread".to_string()) + .stack_size(THREAD_STACK_SIZE) + .spawn(move || { + let conn = Connection::new(&ctx, Direction::Inbound).establish(&addr).unwrap(); + loop { + match rx.recv().unwrap() { + WorkerTask::Receive => { + conn.receive(1000).unwrap(); + signal.send(()).unwrap(); + }, + WorkerTask::ReceiveSend => { + let data = conn.receive(1000).unwrap(); + peer_conn.send(data).unwrap(); + signal.send(()).unwrap(); + }, + WorkerTask::Exit => break, + } + } + }); + tx +} + +/// Benchmark for PeerConnnection +/// +/// ## Setup Phase +/// +/// 1. Two peer connections (one inbound and one outbound) are "booted up" +/// 2. Consumer workers are started up and set to listen for messages from the peer connection +/// +/// ## Run phase +/// +/// 1. Peer 1's worker is told to go into ReceiveSend mode +/// 2. Peer 2's worker is told to go into Receive mode +/// 3. Peer 2 (outbound) sends DUMMY_DATA to peer 1 (inbound) +/// 4. Peer 1 receives the message and sends it back to Peer 2 and signals done +/// 5. Peer 2 receives the data and signals done +/// 6. Once both have completed their work, the next iteration can begin +/// +/// ## Teardown phase +/// +/// 1. Both consumer threads are given the Exit task +/// 2. All connections go out of scope (i.e. are dropped) +fn bench_peer_connection(c: &mut Criterion) { + // Setup + let ctx = ZmqContext::new(); + let addr = BENCH_SOCKET_ADDRESS.parse::().unwrap(); + let consumer1 = InprocAddress::random(); + let consumer2 = InprocAddress::random(); + + let p1_ctx = build_context(&ctx, Direction::Inbound, &addr, &consumer1); + let p2_ctx = build_context(&ctx, Direction::Outbound, &addr, &consumer2); + + // Start peer connections on either end + let mut p1 = PeerConnection::new(); + p1.start(p1_ctx).unwrap(); + + let mut p2 = PeerConnection::new(); + p2.start(p2_ctx).unwrap(); + + let (done1_tx, done1_rx) = channel(); + let (done2_tx, done2_rx) = channel(); + + // Start consumers + // These act as a threaded worker which consumes messages from peer connections + let signal1 = start_message_sink_consumer(&ctx, &consumer1, &p1, done1_tx); + let signal2 = start_message_sink_consumer(&ctx, &consumer2, &p2, done2_tx); + + // Duplicates which won't be moved into the bench function + let dup_signal1 = signal1.clone(); + let dup_signal2 = signal2.clone(); + + p1.wait_listening_or_failure(&Duration::from_millis(1000)).unwrap(); + p2.wait_connected_or_failure(&Duration::from_millis(1000)).unwrap(); + + c.bench_function("peer_connection: send/recv", move |b| { + // Benchmark + b.iter(|| { + // Signal p2 to receive and send back + signal1.send(WorkerTask::ReceiveSend).unwrap(); + // Signal p1 to receive + signal2.send(WorkerTask::Receive).unwrap(); + + // Send to p2 + p2.send(DUMMY_DATA.to_vec()).unwrap(); + + // Wait for done signals + done1_rx.recv().unwrap(); + done2_rx.recv().unwrap(); + }); + }); + + // Teardown + dup_signal1.send(WorkerTask::Exit).unwrap(); + dup_signal2.send(WorkerTask::Exit).unwrap(); +} + +criterion_group!( + name = benches; + config = Criterion::default().warm_up_time(Duration::from_millis(500)); + targets = bench_peer_connection, +); diff --git a/comms/src/builder/builder.rs b/comms/src/builder/builder.rs new file mode 100644 index 0000000000..0830e0410a --- /dev/null +++ b/comms/src/builder/builder.rs @@ -0,0 +1,508 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::{ConnectionError, DealerProxyError, InprocAddress, ZmqContext}, + connection_manager::{ConnectionManager, PeerConnectionConfig}, + consts::COMMS_BUILDER_IMS_DEFAULT_PUB_SUB_BUFFER_LENGTH, + control_service::{ControlService, ControlServiceConfig, ControlServiceError, ControlServiceHandle}, + dispatcher::DispatchableKey, + inbound_message_service::{ + comms_msg_handlers::construct_comms_msg_dispatcher, + error::InboundError, + inbound_message_publisher::{InboundMessagePublisher, PublisherError}, + inbound_message_service::{InboundMessageService, InboundMessageServiceConfig}, + InboundTopicSubscriptionFactory, + }, + message::InboundMessage, + outbound_message_service::{ + outbound_message_pool::{OutboundMessagePoolConfig, OutboundMessagePoolError}, + outbound_message_service::OutboundMessageService, + OutboundError, + OutboundMessage, + OutboundMessagePool, + }, + peer_manager::{NodeIdentity, PeerManager, PeerManagerError}, + pub_sub_channel::{pubsub_channel, TopicPublisher}, + types::CommsDatabase, +}; +use bitflags::_core::marker::PhantomData; +use crossbeam_channel::Sender; +use derive_error::Error; +use log::*; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + fmt::Debug, + sync::{Arc, RwLock}, +}; + +const LOG_TARGET: &str = "comms::builder"; + +#[derive(Debug, Error)] +pub enum CommsBuilderError { + PeerManagerError(PeerManagerError), + InboundMessageServiceError(ConnectionError), + #[error(no_from)] + OutboundMessageServiceError(OutboundError), + #[error(no_from)] + OutboundMessagePoolError(OutboundError), + /// Node identity not set. Call `with_node_identity(node_identity)` on [CommsBuilder] + NodeIdentityNotSet, + #[error(no_from)] + DealerProxyError(DealerProxyError), + DatastoreUndefined, +} + +/// The `CommsBuilder` provides a simple builder API for getting Tari comms p2p messaging up and running. +/// +/// The [build] method will return an error if any required builder methods are not called. These +/// are detailed further down on the method docs. +#[derive(Default)] +pub struct CommsBuilder { + zmq_context: ZmqContext, + peer_storage: Option, + control_service_config: Option, + omp_config: Option, + ims_config: Option, + node_identity: Option, + peer_conn_config: Option, + inbound_message_buffer_size: Option, + _m: PhantomData, +} + +impl CommsBuilder +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Clone + Debug, +{ + /// Create a new CommsBuilder + pub fn new() -> Self { + let zmq_context = ZmqContext::new(); + + Self { + zmq_context, + control_service_config: None, + peer_conn_config: None, + omp_config: None, + ims_config: None, + peer_storage: None, + node_identity: None, + inbound_message_buffer_size: None, + _m: PhantomData, + } + } + + /// Set the [NodeIdentity] for this comms instance. This is required. + /// + /// [OutboundMessagePool]: ../../outbound_message_service/index.html#outbound-message-pool + pub fn with_node_identity(mut self, node_identity: NodeIdentity) -> Self { + self.node_identity = Some(node_identity); + self + } + + /// Set the peer storage database to use. This is optional. + pub fn with_peer_storage(mut self, peer_storage: CommsDatabase) -> Self { + self.peer_storage = Some(peer_storage); + self + } + + /// Configure inbound message publisher/subscriber buffer size. This is optional + pub fn configure_inbound_message_publisher_buffer_size(mut self, size: usize) -> Self { + self.inbound_message_buffer_size = Some(size); + self + } + + /// Configure the [ControlService]. This is optional. + /// + /// [ControlService]: ../../control_service/index.html + pub fn configure_control_service(mut self, config: ControlServiceConfig) -> Self { + self.control_service_config = Some(config); + self + } + + /// Configure the [OutboundMessagePool]. This is optional. If omitted the default configuration is used. + /// + /// [OutboundMessagePool]: ../../outbound_message_service/index.html#outbound-message-pool + pub fn configure_outbound_message_pool(mut self, config: OutboundMessagePoolConfig) -> Self { + self.omp_config = Some(config); + self + } + + /// Common configuration for all [PeerConnection]s. This is optional. + /// If omitted the default configuration is used. + /// + /// [PeerConnection]: ../../connection/peer_connection/index.html + pub fn configure_peer_connections(mut self, config: PeerConnectionConfig) -> Self { + self.peer_conn_config = Some(config); + self + } + + fn make_peer_manager(&mut self) -> Result, CommsBuilderError> { + match self.peer_storage.take() { + Some(storage) => { + let peer_manager = PeerManager::new(storage).map_err(CommsBuilderError::PeerManagerError)?; + Ok(Arc::new(peer_manager)) + }, + None => Err(CommsBuilderError::DatastoreUndefined), + } + } + + fn make_control_service(&mut self, node_identity: Arc) -> Option { + self.control_service_config + .take() + .map(|config| ControlService::new(self.zmq_context.clone(), node_identity, config)) + } + + fn make_connection_manager( + &mut self, + node_identity: Arc, + peer_manager: Arc, + config: PeerConnectionConfig, + ) -> Arc + { + Arc::new(ConnectionManager::new( + self.zmq_context.clone(), + node_identity, + peer_manager, + config, + )) + } + + fn make_peer_connection_config(&mut self) -> PeerConnectionConfig { + let mut config = self.peer_conn_config.take().unwrap_or_default(); + // If the message_sink_address is not set (is default) set it to a random inproc address + if config.message_sink_address.is_default() { + config.message_sink_address = InprocAddress::random(); + } + config + } + + fn make_node_identity(&mut self) -> Result, CommsBuilderError> { + self.node_identity + .take() + .map(Arc::new) + .ok_or(CommsBuilderError::NodeIdentityNotSet) + } + + fn make_outbound_message_service( + &self, + node_identity: Arc, + message_sink: Sender, + peer_manager: Arc, + ) -> Result, CommsBuilderError> + { + OutboundMessageService::new(node_identity, message_sink, peer_manager) + .map(Arc::new) + .map_err(CommsBuilderError::OutboundMessageServiceError) + } + + fn make_outbound_message_pool( + &mut self, + peer_manager: Arc, + connection_manager: Arc, + ) -> OutboundMessagePool + { + let config = self.omp_config.take().unwrap_or_default(); + + OutboundMessagePool::new(config, peer_manager, connection_manager) + } + + // TODO Remove this Arc + RwLock when the IMS worker is refactored to be future based. + fn make_inbound_message_publisher( + &mut self, + publisher: TopicPublisher, + ) -> Arc>> + { + Arc::new(RwLock::new(InboundMessagePublisher::new(publisher))) + } + + fn make_inbound_message_service( + &mut self, + node_identity: Arc, + message_sink_address: InprocAddress, + inbound_message_publisher: Arc>>, + oms: Arc, + peer_manager: Arc, + ) -> InboundMessageService + { + let config = self.ims_config.take().unwrap_or_default(); + + InboundMessageService::new( + config, + self.zmq_context.clone(), + node_identity, + message_sink_address, + Arc::new(construct_comms_msg_dispatcher()), + inbound_message_publisher, + oms, + peer_manager, + ) + } + + /// Build the required comms services. Services will not be started. + pub fn build(mut self) -> Result, CommsBuilderError> { + let node_identity = self.make_node_identity()?; + + let peer_manager = self.make_peer_manager()?; + + let peer_conn_config = self.make_peer_connection_config(); + + let control_service = self.make_control_service(node_identity.clone()); + + let connection_manager = + self.make_connection_manager(node_identity.clone(), peer_manager.clone(), peer_conn_config.clone()); + + let outbound_message_pool = self.make_outbound_message_pool(peer_manager.clone(), connection_manager.clone()); + + let outbound_message_service = self.make_outbound_message_service( + node_identity.clone(), + outbound_message_pool.sender(), + peer_manager.clone(), + )?; + + // Create pub/sub channel for IMS + let (publisher, inbound_message_subscription_factory) = pubsub_channel( + self.inbound_message_buffer_size + .or(Some(COMMS_BUILDER_IMS_DEFAULT_PUB_SUB_BUFFER_LENGTH)) + .unwrap(), + ); + let inbound_message_publisher = self.make_inbound_message_publisher(publisher); + + let inbound_message_service = self.make_inbound_message_service( + node_identity.clone(), + peer_conn_config.message_sink_address, + inbound_message_publisher, + outbound_message_service.clone(), + peer_manager.clone(), + ); + + Ok(CommsServiceContainer { + zmq_context: self.zmq_context, + control_service, + inbound_message_service, + connection_manager, + outbound_message_pool, + outbound_message_service, + peer_manager, + node_identity, + inbound_message_subscription_factory: Arc::new(inbound_message_subscription_factory), + }) + } +} + +#[derive(Debug, Error)] +pub enum CommsServicesError { + ControlServiceError(ControlServiceError), + ConnectionManagerError(ConnectionError), + /// Comms services shut down uncleanly + UncleanShutdown, + /// The message type was not registered + MessageTypeNotRegistered, + OutboundMessagePoolError(OutboundMessagePoolError), + OutboundError(OutboundError), + InboundMessageServiceError(InboundError), + PublisherError(PublisherError), +} + +/// Contains the built comms services +pub struct CommsServiceContainer +where + MType: Serialize + DeserializeOwned, + MType: DispatchableKey, + MType: Clone + Debug, +{ + zmq_context: ZmqContext, + connection_manager: Arc, + control_service: Option, + inbound_message_service: InboundMessageService, + outbound_message_pool: OutboundMessagePool, + outbound_message_service: Arc, + peer_manager: Arc, + node_identity: Arc, + inbound_message_subscription_factory: Arc>, +} + +impl CommsServiceContainer +where + MType: Serialize + DeserializeOwned, + MType: DispatchableKey, + MType: Clone + Send + Debug, +{ + /// Start all the comms services and return a [CommsServices] object + /// + /// [CommsServices]: ./struct.CommsServices.html + pub fn start(mut self) -> Result, CommsServicesError> { + let mut control_service_handle = None; + if let Some(control_service) = self.control_service { + control_service_handle = Some( + control_service + .serve(self.connection_manager.clone()) + .map_err(CommsServicesError::ControlServiceError)?, + ); + } + + self.inbound_message_service + .start() + .map_err(CommsServicesError::InboundMessageServiceError)?; + self.outbound_message_pool + .start() + .map_err(CommsServicesError::OutboundMessagePoolError)?; + + Ok(CommsServices { + // Transfer ownership to CommsServices + zmq_context: self.zmq_context, + outbound_message_service: self.outbound_message_service, + connection_manager: self.connection_manager, + peer_manager: self.peer_manager, + inbound_message_subscription_factory: self.inbound_message_subscription_factory, + outbound_message_pool: self.outbound_message_pool, + node_identity: self.node_identity, + // Add handles for started services + control_service_handle, + }) + } +} + +/// # CommsServices +/// +/// This struct provides a handle to and control over all the running comms services. +/// You can get a [DomainConnector] from which to receive messages by using the `create_connector` +/// method. Use the `shutdown` method to attempt to cleanly shut all comms services down. +pub struct CommsServices +where MType: Send + Sync + Debug +{ + zmq_context: ZmqContext, + outbound_message_service: Arc, + control_service_handle: Option, + outbound_message_pool: OutboundMessagePool, + node_identity: Arc, + connection_manager: Arc, + peer_manager: Arc, + inbound_message_subscription_factory: Arc>, +} + +impl CommsServices +where + MType: DispatchableKey, + MType: Clone + Send + Debug, +{ + pub fn zmq_context(&self) -> &ZmqContext { + &self.zmq_context + } + + pub fn peer_manager(&self) -> Arc { + Arc::clone(&self.peer_manager) + } + + pub fn node_identity(&self) -> Arc { + Arc::clone(&self.node_identity) + } + + pub fn connection_manager(&self) -> Arc { + Arc::clone(&self.connection_manager) + } + + pub fn outbound_message_service(&self) -> Arc { + Arc::clone(&self.outbound_message_service) + } + + pub fn inbound_message_subscription_factory(&self) -> Arc> { + Arc::clone(&self.inbound_message_subscription_factory) + } + + pub fn shutdown(self) -> Result<(), CommsServicesError> { + info!(target: LOG_TARGET, "Comms is shutting down"); + let mut shutdown_results = Vec::new(); + // Shutdown control service + if let Some(control_service_shutdown_result) = self.control_service_handle.map(|hnd| hnd.shutdown()) { + shutdown_results.push(control_service_shutdown_result.map_err(CommsServicesError::ControlServiceError)); + } + + // Shutdown outbound message pool + shutdown_results.push( + self.outbound_message_pool + .shutdown() + .map_err(CommsServicesError::OutboundError), + ); + + // Lastly, Shutdown connection manager + match Arc::try_unwrap(self.connection_manager) { + Ok(conn_manager) => { + for result in conn_manager.shutdown() { + shutdown_results.push(result.map_err(CommsServicesError::ConnectionManagerError)); + } + }, + Err(_) => error!( + target: LOG_TARGET, + "Unable to cleanly shutdown connection manager because references are still held by other threads" + ), + } + + Self::check_clean_shutdown(shutdown_results) + } + + fn check_clean_shutdown(results: Vec>) -> Result<(), CommsServicesError> { + let mut has_error = false; + for result in results { + if let Err(err) = result { + error!(target: LOG_TARGET, "Error occurred when shutting down {:?}", err); + has_error = true; + } + } + + if has_error { + Err(CommsServicesError::UncleanShutdown) + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use tari_storage::HMapDatabase; + + #[test] + fn new_no_control_service() { + let comms_services: CommsServiceContainer = CommsBuilder::new() + .with_node_identity(NodeIdentity::random_for_test(None)) + .with_peer_storage(HMapDatabase::new()) + .build() + .unwrap(); + + assert!(comms_services.control_service.is_none()); + } + + #[test] + fn new_with_control_service() { + let comms_services: CommsServiceContainer = CommsBuilder::new() + .with_node_identity(NodeIdentity::random_for_test(None)) + .with_peer_storage(HMapDatabase::new()) + .configure_control_service(ControlServiceConfig::default()) + .build() + .unwrap(); + + assert!(comms_services.control_service.is_some()); + } +} diff --git a/comms/src/builder/mod.rs b/comms/src/builder/mod.rs new file mode 100644 index 0000000000..554a223645 --- /dev/null +++ b/comms/src/builder/mod.rs @@ -0,0 +1,78 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! # CommsBuilder +//! +//! The [CommsBuilder] provides a simple builder API for getting Tari comms p2p messaging up and running. +//! +//! ```edition2018 +//! # use tari_comms::builder::CommsBuilder; +//! # use tari_comms::dispatcher::HandlerError; +//! # use tari_comms::message::InboundMessage; +//! # use tari_comms::control_service::ControlServiceConfig; +//! # use tari_comms::peer_manager::NodeIdentity; +//! # use std::sync::Arc; +//! # use rand::OsRng; +//! # use tari_storage::lmdb_store::LMDBBuilder; +//! # use lmdb_zero::db; +//! # use tari_storage::LMDBWrapper; +//! +//! // This should be loaded up from storage +//! let my_node_identity = NodeIdentity::random(&mut OsRng::new().unwrap(), "127.0.0.1:9000".parse().unwrap()).unwrap(); +//! +//! fn my_handler(_: InboundMessage) -> Result<(), HandlerError> { +//! println!("Your handler is called!"); +//! Ok(()) +//! } +//! +//! let database_name = "b_peer_database"; +//! let datastore = LMDBBuilder::new() +//! .set_path("/tmp/") +//! .set_environment_size(10) +//! .set_max_number_of_databases(2) +//! .add_database(database_name, lmdb_zero::db::CREATE) +//! .build().unwrap(); +//! let peer_database = datastore.get_handle(database_name).unwrap(); +//! let peer_database = LMDBWrapper::new(Arc::new(peer_database)); +//! +//! let services = CommsBuilder::::new() +//! // This enables the control service - allowing another peer to connect to this node +//! .configure_control_service(ControlServiceConfig::default()) +//! .with_node_identity(my_node_identity) +//! .with_peer_storage(peer_database) +//! .build() +//! .unwrap(); +//! +//! let handle = services.start().unwrap(); +//! // Call shutdown when program shuts down +//! handle.shutdown(); +//! ``` +//! +//! [CommsBuilder]: ./builder/struct.CommsBuilder.html + +mod builder; +mod routes; + +pub use self::{ + builder::{CommsBuilder, CommsBuilderError, CommsServices, CommsServicesError}, + routes::CommsRoutes, +}; diff --git a/comms/src/builder/routes.rs b/comms/src/builder/routes.rs new file mode 100644 index 0000000000..64946ef93f --- /dev/null +++ b/comms/src/builder/routes.rs @@ -0,0 +1,48 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::connection::InprocAddress; + +#[derive(Clone, Debug, Default)] +pub struct CommsRoutes(Vec<(MType, InprocAddress)>); + +impl CommsRoutes +where MType: Clone + Eq +{ + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn register(mut self, message_type: MType) -> Self { + let addr = InprocAddress::random(); + self.0.push((message_type, addr.clone())); + self + } + + pub fn inner(&self) -> &Vec<(MType, InprocAddress)> { + &self.0 + } + + pub fn get_address(&self, message_type: &MType) -> Option<&InprocAddress> { + self.0.iter().find(|(mt, _)| mt == message_type).map(|(_, addr)| addr) + } +} diff --git a/comms/src/connection/connection.rs b/comms/src/connection/connection.rs new file mode 100644 index 0000000000..255e3dc081 --- /dev/null +++ b/comms/src/connection/connection.rs @@ -0,0 +1,579 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::{ + net_address::ip::SocketAddress, + types::{Direction, Linger, Result, SocketEstablishment, SocketType}, + zmq::{CurveEncryption, InprocAddress, ZmqContext, ZmqEndpoint}, + ConnectionError, + }, + message::FrameSet, +}; +use log::*; +use std::{borrow::Borrow, cmp, iter::IntoIterator, str::FromStr, time::Duration}; + +const LOG_TARGET: &str = "comms::connection::Connection"; + +/// Represents a low-level connection which can be bound an address +/// supported by [`ZeroMQ`] the `ZMQ_ROUTER` socket. +/// +/// ```edition2018 +/// # use tari_comms::connection::{ +/// # zmq::{ZmqContext, InprocAddress, CurveEncryption}, +/// # connection::Connection, +/// # types::{Linger, Direction}, +/// # }; +/// +/// let ctx = ZmqContext::new(); +/// +/// let (secret_key, _public_key) =CurveEncryption::generate_keypair().unwrap(); +/// +/// let addr = "inproc://docs-comms-inbound-connection".parse::().unwrap(); +/// +/// let conn = Connection::new(&ctx, Direction::Inbound) +/// .set_curve_encryption(CurveEncryption::Server {secret_key}) +/// .set_linger(Linger::Never) +/// .set_max_message_size(Some(123)) +/// .set_receive_hwm(1) +/// .set_send_hwm(2) +/// .establish(&addr) +/// .unwrap(); +/// +/// // Receive timeout is 1 so timeout error is returned +/// let result = conn.receive(1); +/// assert!(result.is_err()); +/// ``` +/// [`ZeroMQ`]: http://zeromq.org/ +pub struct Connection<'a> { + pub(super) context: &'a ZmqContext, + pub(super) name: String, + pub(super) curve_encryption: CurveEncryption, + pub(super) direction: Direction, + pub(super) identity: Option, + pub(super) linger: Linger, + pub(super) max_message_size: Option, + pub(super) monitor_addr: Option, + pub(super) recv_hwm: Option, + pub(super) send_hwm: Option, + pub(super) backlog: Option, + pub(super) socket_establishment: SocketEstablishment, + pub(super) socks_proxy_addr: Option, + pub(super) heartbeat_interval: Option, + pub(super) heartbeat_remote_ttl: Option, + pub(super) heartbeat_timeout: Option, +} + +impl<'a> Connection<'a> { + /// Create a new InboundConnection + pub fn new(context: &'a ZmqContext, direction: Direction) -> Self { + Self { + context, + name: "Unnamed".to_string(), + curve_encryption: Default::default(), + direction, + identity: None, + linger: Linger::Never, + max_message_size: None, + monitor_addr: None, + recv_hwm: None, + send_hwm: None, + backlog: None, + socket_establishment: Default::default(), + socks_proxy_addr: None, + heartbeat_interval: None, + heartbeat_remote_ttl: None, + heartbeat_timeout: None, + } + } + + /// Set receive high water mark + pub fn set_receive_hwm(mut self, hwm: i32) -> Self { + self.recv_hwm = Some(hwm); + self + } + + /// Set send high water mark + pub fn set_send_hwm(mut self, hwm: i32) -> Self { + self.send_hwm = Some(hwm); + self + } + + /// Set the connection identity + pub fn set_identity(mut self, identity: &str) -> Self { + self.identity = Some(identity.to_owned()); + self + } + + /// Set the maximum length of the queue of outstanding peer connections + /// for the specified outbound connection. + pub fn set_backlog(mut self, backlog: i32) -> Self { + self.backlog = Some(backlog); + self + } + + /// Set the period the underling socket connection should + /// continue to send messages after this connection is dropped. + pub fn set_linger(mut self, linger: Linger) -> Self { + self.linger = linger; + self + } + + /// Set a name for the connection. This is used in logs and for debugging purposes. + pub fn set_name(mut self, name: &str) -> Self { + self.name = name.to_string(); + self + } + + /// The maximum size in bytes of the inbound message. If a message is + /// received which is larger, the connection will disconnect. + /// `msg_size` has an upper bound of i64::MAX due to zMQ's usage of a signed 64-bit + /// integer for this socket option. Setting it higher will result in i64::MAX being used. + /// Set to None for no limit + pub fn set_max_message_size(mut self, msg_size: Option) -> Self { + self.max_message_size = msg_size; + self + } + + /// Set the InprocAddress to enable monitoring on the underlying socket. + /// All socket events are sent to sent to this address. + /// The monitor must be connected before the connection is established. + pub fn set_monitor_addr(mut self, addr: InprocAddress) -> Self { + self.monitor_addr = Some(addr); + self + } + + /// Set the ip:port of a SOCKS proxy to use for this connection + pub fn set_socks_proxy_addr(mut self, addr: Option) -> Self { + self.socks_proxy_addr = addr; + self + } + + /// Used to select the method to use when establishing the connection. + pub fn set_socket_establishment(mut self, establishment: SocketEstablishment) -> Self { + self.socket_establishment = establishment; + self + } + + /// Set Curve25519 encryption for this connection. + pub fn set_curve_encryption(mut self, encryption: CurveEncryption) -> Self { + self.curve_encryption = encryption; + self + } + + /// Set the interval in which to send heartbeat pings. + pub fn set_heartbeat_interval(mut self, interval: Duration) -> Self { + self.heartbeat_interval = Some(interval); + self + } + + /// Set the length of time to wait for a pong after sending a ping before closing the connection. + pub fn set_heartbeat_timeout(mut self, timeout: Duration) -> Self { + self.heartbeat_timeout = Some(timeout); + self + } + + /// Set the interval time that the remote peer expect to receive heartbeat/other messages. + /// If the remote peer does not receive any message within the TTL period it should close the connection. + /// More info: http://api.zeromq.org/4-2:zmq-setsockopt#toc17 + pub fn set_heartbeat_remote_ttl(mut self, ttl: Duration) -> Self { + self.heartbeat_remote_ttl = Some(ttl); + self + } + + /// Create the socket, configure it and bind/connect it to the given address + pub fn establish(self, addr: &T) -> Result { + let socket = match self.direction { + Direction::Inbound => self.context.socket(SocketType::Router).unwrap(), + Direction::Outbound => self.context.socket(SocketType::Dealer).unwrap(), + }; + + let config_error_mapper = |e| ConnectionError::SocketError(format!("Unable to configure socket: {}", e)); + + if self.direction == Direction::Inbound { + socket.set_router_mandatory(true).map_err(config_error_mapper)?; + } + + if let Some(v) = self.recv_hwm { + socket.set_rcvhwm(v).map_err(config_error_mapper)?; + } + + if let Some(v) = self.send_hwm { + socket.set_sndhwm(v).map_err(config_error_mapper)?; + } + + if let Some(ident) = self.identity { + socket.set_identity(ident.as_bytes()).map_err(config_error_mapper)?; + } + + if let Some(v) = self.max_message_size { + socket + .set_maxmsgsize(cmp::min(v, std::i64::MAX as u64) as i64) + .map_err(config_error_mapper)?; + } + + set_linger(&socket, self.linger)?; + + if let Some(backlog) = self.backlog { + socket.set_backlog(backlog).map_err(config_error_mapper)?; + } + + match self.curve_encryption { + CurveEncryption::None => {}, + CurveEncryption::Server { secret_key } => { + socket.set_curve_server(true).map_err(config_error_mapper)?; + socket + .set_curve_secretkey(&secret_key.into_inner()) + .map_err(config_error_mapper)?; + }, + CurveEncryption::Client { + secret_key, + public_key, + server_public_key, + } => { + socket + .set_curve_serverkey(&server_public_key.into_inner()) + .map_err(config_error_mapper)?; + socket + .set_curve_secretkey(&secret_key.into_inner()) + .map_err(config_error_mapper)?; + socket + .set_curve_publickey(&public_key.into_inner()) + .map_err(config_error_mapper)?; + }, + } + + // Set heartbeat socket opts + if let Some(interval) = self.heartbeat_interval { + socket + .set_heartbeat_ivl(interval.as_millis() as i32) + .map_err(config_error_mapper)?; + } + + if let Some(timeout) = self.heartbeat_timeout { + socket + .set_heartbeat_timeout(timeout.as_millis() as i32) + .map_err(config_error_mapper)?; + } + + if let Some(ttl) = self.heartbeat_remote_ttl { + socket + .set_heartbeat_ttl(ttl.as_millis() as i32) + .map_err(config_error_mapper)?; + } + + if let Some(v) = self.socks_proxy_addr { + socket + .set_socks_proxy(Some(&v.to_string())) + .map_err(config_error_mapper)?; + } + + if let Some(ref addr) = self.monitor_addr { + socket + .monitor(addr.to_zmq_endpoint().as_str(), zmq::SocketEvent::ALL as i32) + .map_err(|e| ConnectionError::SocketError(format!("Unable to set monitor address: {}", e)))?; + } + + let endpoint = &addr.to_zmq_endpoint(); + match self.socket_establishment { + SocketEstablishment::Bind => socket.bind(endpoint), + SocketEstablishment::Connect => socket.connect(endpoint), + SocketEstablishment::Auto => match self.direction { + Direction::Inbound => socket.bind(endpoint), + Direction::Outbound => socket.connect(endpoint), + }, + } + .map_err(|e| ConnectionError::SocketError(format!("Failed to establish socket: {}", e)))?; + + let connected_address = get_socket_address(&socket); + + debug!( + target: LOG_TARGET, + "Established {} connection on {:?} (name: {})", + self.direction, + connected_address + .borrow() + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| endpoint.to_owned()), + self.name, + ); + + Ok(EstablishedConnection { + socket, + connected_address, + name: self.name, + direction: self.direction, + }) + } +} + +fn set_linger(socket: &zmq::Socket, linger: Linger) -> Result<()> { + let config_error_mapper = |e| ConnectionError::SocketError(format!("Unable to configure linger on socket: {}", e)); + match linger { + Linger::Indefinitely => socket.set_linger(-1).map_err(config_error_mapper), + + Linger::Never => socket.set_linger(0).map_err(config_error_mapper), + + Linger::Timeout(t) => socket.set_linger(t as i32).map_err(config_error_mapper), + } +} + +/// Represents an established connection. +pub struct EstablishedConnection { + socket: zmq::Socket, + // If the connection is a TCP connection, it will be stored here, otherwise it is None + connected_address: Option, + name: String, + direction: Direction, +} + +impl EstablishedConnection { + /// Receive a multipart message or return a `ConnectionError::Timeout` if the specified timeout has expired. + /// This method may be repeatably called, probably in a loop in a separate thread, to receive multiple multipart + /// messages. + pub fn receive(&self, timeout_ms: u32) -> Result { + match self.socket.poll(zmq::POLLIN, i64::from(timeout_ms)) { + Ok(rc) => { + match rc { + // Internal error when polling connection + -1 => Err(ConnectionError::SocketError("Failed to poll socket".to_string())), + // Nothing to receive + 0 => Err(ConnectionError::Timeout), + // Ready to receive + _ => self.receive_multipart(), + } + }, + + Err(e) => Err(ConnectionError::SocketError(format!("Failed to poll: {}", e))), + } + } + + /// Return the actual address that we're connected to. On inbound connections, once can delegate port selection to + /// the OS, (e.g. "127.0.0.1:0") which means that the actual port we're connecting to isn't known until the binding + /// has been made. This function queries the socket for the connection info, and extracts the address & port if it + /// was a TCP connection, returning None otherwise + pub fn get_connected_address(&self) -> &Option { + &self.connected_address + } + + /// Read entire multipart message + pub fn receive_multipart(&self) -> Result { + self.socket + .recv_multipart(0) + .and_then(|frames| { + trace!( + target: LOG_TARGET, + "Received {} frame(s) (name: {})", + frames.len(), + self.name + ); + Ok(frames) + }) + .map_err(|e| ConnectionError::SocketError(format!("Error receiving: {} ({})", e, e.to_raw()))) + } + + /// Set the period the underling socket connection should + /// continue to send messages after this connection is dropped. + pub fn set_linger(&self, linger: Linger) -> Result<()> { + set_linger(&self.socket, linger) + } + + /// Sends multipart message frames. This function is non-blocking. + pub fn send(&self, frames: I) -> Result<()> + where + I: IntoIterator, + T: AsRef<[u8]>, + { + self.send_with_flags(frames, zmq::DONTWAIT) + } + + /// Sends multipart message frames. This will block until the message is queued + /// for sending. + pub fn send_sync(&self, frames: I) -> Result<()> + where + I: IntoIterator, + T: AsRef<[u8]>, + { + self.send_with_flags(frames, 0) + } + + fn send_with_flags(&self, frames: I, flags: i32) -> Result<()> + where + I: IntoIterator, + T: AsRef<[u8]>, + { + let mut last_frame: Option = None; + for frame in frames.into_iter() { + if let Some(f) = last_frame.take() { + self.send_frame(f, flags | zmq::SNDMORE)?; + } + last_frame = Some(frame); + } + if let Some(f) = last_frame { + self.send_frame(f, flags)?; + } + + Ok(()) + } + + fn send_frame(&self, frame: T, flags: i32) -> Result<()> + where T: AsRef<[u8]> { + self.socket + .send(frame.as_ref(), flags) + .map_err(|e| ConnectionError::SocketError(format!("Error sending: {} ({})", e, e.to_raw()))) + } + + #[cfg(test)] + pub(crate) fn get_socket(&self) -> &zmq::Socket { + &self.socket + } + + pub(crate) fn get_socket_mut(&mut self) -> &mut zmq::Socket { + &mut self.socket + } + + pub fn direction(&self) -> &Direction { + &self.direction + } +} + +impl Drop for EstablishedConnection { + fn drop(&mut self) { + debug!( + target: LOG_TARGET, + "Dropping {} connection {:?} (name: {})", + self.direction, + self.get_connected_address(), + self.name, + ); + } +} + +/// Extract the actual address that we're connected to. On inbound connections, once can delegate port selection to +/// the OS, (e.g. "127.0.0.1:0") which means that the actual port we're connecting to isn't known until the binding +/// has been made. This function queries the socket for the connection info, and extracts the address & port if it +/// was a TCP connection, returning None otherwise +fn get_socket_address(socket: &zmq::Socket) -> Option { + let addr = match socket.get_last_endpoint() { + Ok(v) => v.unwrap(), + Err(_) => return None, + }; + let parts = &addr.split("//").collect::>(); + if parts.len() < 2 || parts[0] != "tcp:" { + return None; + } + let addr = parts[1]; + SocketAddress::from_str(&addr).ok() +} + +#[cfg(test)] +mod test { + use super::*; + use crate::connection::zmq::{CurveEncryption, InprocAddress}; + + #[test] + fn sets_socketopts() { + let ctx = ZmqContext::new(); + + let addr = InprocAddress::random(); + let monitor_addr = InprocAddress::random(); + + let conn = Connection::new(&ctx, Direction::Inbound) + .set_name("dummy") + .set_heartbeat_remote_ttl(Duration::from_millis(1000)) + .set_heartbeat_timeout(Duration::from_millis(1001)) + .set_heartbeat_interval(Duration::from_millis(1002)) + .set_identity("identity") + .set_linger(Linger::Timeout(200)) + .set_max_message_size(Some(123)) + .set_receive_hwm(1) + .set_send_hwm(2) + .set_socks_proxy_addr(Some("127.0.0.1:9988".parse::().unwrap())) + .set_monitor_addr(monitor_addr) + .establish(&addr) + .unwrap(); + + assert_eq!("dummy", conn.name); + let sock = conn.get_socket(); + assert!(!sock.is_curve_server().unwrap()); + assert_eq!(200, sock.get_linger().unwrap()); + assert_eq!(123, sock.get_maxmsgsize().unwrap()); + assert_eq!(1000, sock.get_heartbeat_ttl().unwrap()); + assert_eq!(1001, sock.get_heartbeat_timeout().unwrap()); + assert_eq!(1002, sock.get_heartbeat_ivl().unwrap()); + assert_eq!("identity".as_bytes(), sock.get_identity().unwrap().as_slice()); + assert_eq!(1, sock.get_rcvhwm().unwrap()); + assert_eq!(2, sock.get_sndhwm().unwrap()); + assert_eq!(Ok("127.0.0.1:9988".to_string()), sock.get_socks_proxy().unwrap()); + } + + #[test] + fn set_server_encryption() { + let ctx = ZmqContext::new(); + + let addr = InprocAddress::random(); + + let (sk, _) = CurveEncryption::generate_keypair().unwrap(); + let expected_sk = sk.clone(); + + let conn = Connection::new(&ctx, Direction::Inbound) + .set_curve_encryption(CurveEncryption::Server { secret_key: sk }) + .establish(&addr) + .unwrap(); + + let sock = conn.get_socket(); + assert!(sock.is_curve_server().unwrap()); + assert_eq!(expected_sk.into_inner(), sock.get_curve_secretkey().unwrap().as_slice()); + } + + #[test] + fn set_client_encryption() { + let ctx = ZmqContext::new(); + + let addr = InprocAddress::random(); + + let (sk, pk) = CurveEncryption::generate_keypair().unwrap(); + let (_, spk) = CurveEncryption::generate_keypair().unwrap(); + let expected_sk = sk.clone(); + let expected_pk = pk.clone(); + let expected_spk = spk.clone(); + + let conn = Connection::new(&ctx, Direction::Inbound) + .set_curve_encryption(CurveEncryption::Client { + secret_key: sk, + public_key: pk, + server_public_key: spk, + }) + .establish(&addr) + .unwrap(); + + let sock = conn.get_socket(); + assert!(!sock.is_curve_server().unwrap()); + assert_eq!(expected_sk.into_inner(), sock.get_curve_secretkey().unwrap().as_slice()); + assert_eq!(expected_pk.into_inner(), sock.get_curve_publickey().unwrap().as_slice()); + assert_eq!( + expected_spk.into_inner(), + sock.get_curve_serverkey().unwrap().as_slice() + ); + } +} diff --git a/comms/src/connection/dealer_proxy.rs b/comms/src/connection/dealer_proxy.rs new file mode 100644 index 0000000000..19ee0dd724 --- /dev/null +++ b/comms/src/connection/dealer_proxy.rs @@ -0,0 +1,219 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use log::*; + +use std::thread; + +use derive_error::Error; + +use crate::connection::{ + types::{Direction, SocketEstablishment, SocketType}, + zmq::ZmqEndpoint, + Connection, + ConnectionError, + InprocAddress, + ZmqContext, +}; +use std::{sync::mpsc::channel, thread::JoinHandle, time::Duration}; +use tari_utilities::thread_join::{ThreadError, ThreadJoinWithTimeout}; + +const LOG_TARGET: &str = "comms::dealer_proxy"; + +/// Set the allocated stack size for the DealerProxy thread +const THREAD_STACK_SIZE: usize = 64 * 1024; // 64kb + +/// Set the maximum waiting time for DealerProxy thread to join +const THREAD_JOIN_TIMEOUT_IN_MS: Duration = Duration::from_millis(100); + +#[derive(Debug, Error)] +pub enum DealerProxyError { + #[error(msg_embedded, no_from, non_std)] + SocketError(String), + ConnectionError(ConnectionError), + /// The dealer [thread::JoinHandle] is unavailable + DealerUndefined, + /// Could not join the dealer thread + ThreadJoinError(ThreadError), + /// Proxy thread failed to start within 10 seconds + ThreadStartFailed, + #[error(msg_embedded, no_from, non_std)] + ZmqError(String), + /// Dealer proxy thread failed to start + ThreadInitializationError, +} + +/// A DealerProxy Result +pub type Result = std::result::Result; + +/// Proxies two addresses, receiving from the source_address and fair dealing to the +/// sink_address. +pub struct DealerProxy { + context: ZmqContext, + source_address: InprocAddress, + sink_address: InprocAddress, + control_address: InprocAddress, + thread_handle: Option>>, +} + +impl DealerProxy { + /// Creates a new DealerProxy. + pub fn new(context: ZmqContext, source_address: InprocAddress, sink_address: InprocAddress) -> Self { + Self { + context, + source_address, + sink_address, + control_address: InprocAddress::random(), + thread_handle: None, + } + } + + /// Proxy the source and sink addresses. This method does not block and returns stores the [thread::JoinHandle] in + /// the DealerProxy. + pub fn spawn_proxy(&mut self) -> Result<()> { + let (ready_tx, ready_rx) = channel(); + + let context = self.context.clone(); + let source_address = self.source_address.clone(); + let sink_address = self.sink_address.clone(); + let control_address = self.control_address.clone(); + + self.thread_handle = Some( + thread::Builder::new() + .name("dealer-proxy-thread".to_string()) + .stack_size(THREAD_STACK_SIZE) + .spawn(move || { + let mut source = Connection::new(&context.clone(), Direction::Inbound) + .set_name("dealer-proxy-source") + .set_socket_establishment(SocketEstablishment::Bind) + .establish(&source_address.clone()) + .map_err(DealerProxyError::ConnectionError)?; + + let mut sink = Connection::new(&context.clone(), Direction::Outbound) + .set_name("dealer-proxy-sink") + .set_socket_establishment(SocketEstablishment::Bind) + .establish(&sink_address.clone()) + .map_err(DealerProxyError::ConnectionError)?; + + let mut control = context + .socket(SocketType::Sub) + .map_err(|err| DealerProxyError::ZmqError(err.to_string()))?; + control + .connect(&control_address.to_zmq_endpoint()) + .map_err(|err| DealerProxyError::ZmqError(err.to_string()))?; + control + .set_subscribe(&[]) + .map_err(|err| DealerProxyError::ZmqError(err.to_string()))?; + + ready_tx.send(()).unwrap(); + + zmq::proxy_steerable(source.get_socket_mut(), sink.get_socket_mut(), &mut control) + .map_err(|err| DealerProxyError::SocketError(err.to_string())) + }) + .map_err(|_| DealerProxyError::ThreadInitializationError)?, + ); + + ready_rx + .recv_timeout(Duration::from_secs(10)) + .map_err(|_| DealerProxyError::ThreadStartFailed) + } + + pub fn is_running(&self) -> bool { + self.thread_handle.is_some() + } + + /// Send a shutdown request to the dealer proxy. If the dealer proxy has not been started + /// this method has no effect. + pub fn shutdown(self) -> Result<()> { + if let Some(thread_handle) = self.thread_handle { + info!(target: LOG_TARGET, "Dealer proxy SHUTDOWN"); + let control = self + .context + .socket(SocketType::Pub) + .map_err(|err| DealerProxyError::ZmqError(err.to_string()))?; + + control + .set_linger(3000) + .map_err(|err| DealerProxyError::ZmqError(err.to_string()))?; + + control + .bind(&self.control_address.to_zmq_endpoint()) + .map_err(|err| DealerProxyError::ZmqError(err.to_string()))?; + + control + .send("TERMINATE", zmq::DONTWAIT) + .map_err(|err| DealerProxyError::ZmqError(err.to_string()))?; + + thread_handle + .timeout_join(THREAD_JOIN_TIMEOUT_IN_MS) + .map_err(DealerProxyError::ThreadJoinError) + .or_else(|err| { + error!( + target: LOG_TARGET, + "Dealer proxy thread exited with an error: {:?}", err + ); + Err(err) + })?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn threaded_proxy() { + let context = ZmqContext::new(); + let sender_addr = InprocAddress::random(); + let receiver_addr = InprocAddress::random(); + + let sender = Connection::new(&context, Direction::Outbound) + .establish(&sender_addr) + .unwrap(); + + let receiver = Connection::new(&context, Direction::Outbound) + .establish(&receiver_addr) + .unwrap(); + + let mut proxy = DealerProxy::new(context, sender_addr, receiver_addr); + proxy.spawn_proxy().unwrap(); + + sender.send_sync(&["HELLO".as_bytes()]).unwrap(); + + let msg = receiver.receive(2000).unwrap(); + assert_eq!("HELLO".as_bytes().to_vec(), msg[1]); + + // You need to attach the identity frame to the head of the message + // so that the internal ZMQ_ROUTER will send messages back to the + // connection which sent the message + receiver.send_sync(&[&msg[0], "WORLD".as_bytes()]).unwrap(); + + let msg = sender.receive(2000).unwrap(); + assert_eq!("WORLD".as_bytes().to_vec(), msg[0]); + + // Test steerable dealer shutdown + proxy.shutdown().unwrap(); + } +} diff --git a/comms/src/connection/error.rs b/comms/src/connection/error.rs new file mode 100644 index 0000000000..30e5c7d52e --- /dev/null +++ b/comms/src/connection/error.rs @@ -0,0 +1,65 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{monitor, NetAddressError, PeerConnectionError}; +use derive_error::Error; +use tari_utilities::thread_join::ThreadError; + +#[derive(Debug, Error, PartialEq)] +pub enum ConnectionError { + NetAddressError(NetAddressError), + #[error(msg_embedded, no_from, non_std)] + SocketError(String), + /// Connection timed out + Timeout, + #[error(msg_embedded, no_from, non_std)] + CurveKeypairError(String), + PeerError(PeerConnectionError), + MonitorError(monitor::ConnectionMonitorError), + #[error(msg_embedded, no_from, non_std)] + InvalidOperation(String), + ThreadJoinError(ThreadError), +} + +impl ConnectionError { + /// Returns true if the error is a Timeout error, otherwise false + pub fn is_timeout(&self) -> bool { + match *self { + ConnectionError::Timeout => true, + _ => false, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn is_timeout() { + let err = ConnectionError::Timeout; + assert!(err.is_timeout()); + + let err = ConnectionError::SocketError("dummy error".to_string()); + assert!(!err.is_timeout()); + } +} diff --git a/comms/src/connection/macros.rs b/comms/src/connection/macros.rs new file mode 100644 index 0000000000..ee169c4d51 --- /dev/null +++ b/comms/src/connection/macros.rs @@ -0,0 +1,36 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// Converts a [connection::Result] into an Option. If the call results in +/// a [ConnectionError::Timeout], `None` is passed back. For any other error, +/// the error `return`ed from containing function. +macro_rules! connection_try { + ($e: expr) => { + match $e { + Ok(d) => Some(d), + Err(e) => match e { + crate::connection::ConnectionError::Timeout => None, + _ => return Err(e.into()), + }, + } + }; +} diff --git a/comms/src/connection/mod.rs b/comms/src/connection/mod.rs new file mode 100644 index 0000000000..1cbe3c1a76 --- /dev/null +++ b/comms/src/connection/mod.rs @@ -0,0 +1,98 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! # Connection +//! +//! Related modules that abstract [0MQ] connections and other 0MQ related features, +//! address types and connections to peers. +//! +//! This module consists of: +//! +//! - [NetAddress] +//! +//! Represents an IP, Onion or I2P address. +//! +//! - Zero MQ module +//! +//! Thin wrappers of [0MQ]. Namely, +//! - [ZmqContext] +//! - [CurveEncryption] +//! - [ZmqEndpoint] trait +//! - [InprocAddress] +//! +//! - [Connection] +//! +//! Provides a connection builder and thin wrapper around a [0MQ] Router +//! (Inbound [Direction](./connection/enum.Direction.html) or Dealer (Outbound [Direction]) socket. +//! +//! - [DealerProxy] +//! +//! Async wrapper around [zmq_proxy_steerable] for use with [Connection]. +//! +//! - [ConnectionMonitor] +//! +//! Receives socket events from [0MQ] connections. Wrapper around [zmq_socket_monitor]. +//! +//! - [PeerConnection] +//! +//! Represents a connection to a peer. See [PeerConnection]. +//! +//! [0MQ]: http://zeromq.org/ +//! [NetAddress]: (./net_address/enum.NetAddress.html) +//! [Connection]: (./connection/struct.Connection.html) +//! [DealerProxy]: (./dealer_proxy/struct.DealerProxy.html) +//! [PeerConnection]: (./peer_connection/index.html) +//! [ZmqContext]: (./zmq/context/struct.ZmqContext.html) +//! [CurveEncryption]: (./zmq/curve_keypair/enum.CurveEncryption.html) +//! [ZmqEndpoint]: (./zmq/endpoint/trait.ZmqEndpoint.html) +//! [InprocAddress]: (./zmq/inproc_address/struct.InprocAddress.html) +//! [Direction]: (./connection/enum.Direction.html) +//! [zmq_proxy_steerable]: http://api.zeromq.org/4-1:zmq-proxy-steerable +//! [ConnectionMonitor]: ./monitor/struct.ConnectionMonitor.html +//! [zmq_socket_monitor]: http://api.zeromq.org/4-1:zmq-socket-monitor + +#[macro_use] +mod macros; + +pub mod connection; +pub mod dealer_proxy; +pub mod error; +pub mod monitor; +pub mod net_address; +pub mod peer_connection; +pub mod types; +pub mod zmq; + +pub use self::{ + connection::{Connection, EstablishedConnection}, + dealer_proxy::{DealerProxy, DealerProxyError}, + error::ConnectionError, + net_address::{NetAddress, NetAddressError, NetAddressesWithStats}, + peer_connection::{ + PeerConnection, + PeerConnectionContextBuilder, + PeerConnectionError, + PeerConnectionSimpleState as PeerConnectionState, + }, + types::{Direction, SocketEstablishment}, + zmq::{curve_keypair, CurveEncryption, CurvePublicKey, CurveSecretKey, InprocAddress, ZmqContext}, +}; diff --git a/comms/src/connection/monitor.rs b/comms/src/connection/monitor.rs new file mode 100644 index 0000000000..8d16fc899b --- /dev/null +++ b/comms/src/connection/monitor.rs @@ -0,0 +1,259 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::{ + types::{Result, SocketType}, + zmq::ZmqEndpoint, + ConnectionError, + InprocAddress, + ZmqContext, + }, + message::FrameSet, +}; +use derive_error::Error; +use log::*; +use std::{ + convert::{TryFrom, TryInto}, + fmt, +}; + +const LOG_TARGET: &str = "comms::connection::monitor"; + +#[derive(Debug, Error, PartialEq)] +pub enum ConnectionMonitorError { + #[error(msg_embedded, non_std, no_from)] + CreateSocketFailed(String), + /// Failed to convert integer type to SocketEvent + SocketEventConversionFailed, + #[error(msg_embedded, non_std, no_from)] + ConnectionFailed(String), + /// Received incorrect number of frames + IncorrectFrameCount, +} + +/// ConnectionMonitor is the read side of the ZMQ_PAIR socket. +/// It allows SocketEvents to be read from the given InprocAddress. +/// +/// More details here: http://api.zeromq.org/4-1:zmq-socket-monitor +/// +/// ```edition2018 +/// # use tari_comms::connection::{ZmqContext, monitor::ConnectionMonitor, Connection, Direction, InprocAddress, NetAddress}; +/// +/// let ctx = ZmqContext::new(); +/// let monitor_addr = InprocAddress::random(); +/// let address = "127.0.0.1:9999".parse::().unwrap(); +/// +/// // Monitor MUST start before the connection is established +/// let monitor = ConnectionMonitor::connect(&ctx, &monitor_addr).unwrap(); +/// +/// { +/// Connection::new(&ctx, Direction::Inbound) +/// .set_monitor_addr(monitor_addr) +/// .establish(&address) +/// .unwrap(); +/// } +/// +/// // Read events +/// while let Ok(event) = monitor.read(100) { +/// println!("Got event: {:?}", event); +/// } +/// ``` +pub struct ConnectionMonitor { + socket: zmq::Socket, +} + +impl ConnectionMonitor { + /// Create a new connected ConnectionMonitor. + /// + /// ## Arguments + /// `context` - Connection context. Must be the same context as the connection being monitored + /// `address` - The inproc address from which to read socket events + pub fn connect(context: &ZmqContext, address: &InprocAddress) -> Result { + let socket = context.socket(SocketType::Pair).map_err(|e| { + ConnectionError::MonitorError(ConnectionMonitorError::CreateSocketFailed(format!( + "Failed to create monitor pair socket: {}", + e + ))) + })?; + + socket.connect(&address.to_zmq_endpoint()).map_err(|e| { + ConnectionError::MonitorError(ConnectionMonitorError::ConnectionFailed(format!( + "Failed to connect: {}", + e + ))) + })?; + + debug!(target: LOG_TARGET, "Connection monitor connected on {}", address); + + Ok(Self { socket }) + } + + /// Read a SocketEvent within the given timeout. + /// If the timeout is reached a `Err(ConnectionError::Timeout)` is returned. + /// + /// ## Arguments + /// `timeout_ms` - The timeout to wait in milliseconds + pub fn read(&self, timeout_ms: u32) -> Result { + let frames = self.read_frames(timeout_ms)?; + + if frames.len() != 2 { + return Err(ConnectionMonitorError::IncorrectFrameCount.into()); + } + + macro_rules! transmute_value { + ($data: expr, $start: expr, $end: expr, $type: ty) => { + unsafe { + let mut a: [u8; $end - $start] = Default::default(); + a.copy_from_slice(&$data[$start..$end]); + std::mem::transmute::<[u8; $end - $start], $type>(a).to_le() + } + }; + } + + // First 2 bytes are the event type + let event_type: SocketEventType = transmute_value!(frames[0], 0, 2, u16).try_into()?; + // Next 4 bytes are the event value + let event_value = transmute_value!(frames[0], 2, 6, u32); + + let address = String::from_utf8_lossy(&frames[1]).into_owned(); + + Ok(SocketEvent { + event_type, + event_value, + address, + }) + } + + fn read_frames(&self, timeout_ms: u32) -> Result { + match self.socket.poll(zmq::POLLIN, i64::from(timeout_ms)) { + Ok(rc) => { + match rc { + // Internal error when polling connection + -1 => Err(ConnectionError::SocketError("Failed to poll socket".to_string())), + // Nothing to receive + 0 => Err(ConnectionError::Timeout), + // Ready to receive + _ => self + .socket + .recv_multipart(0) + .map_err(|e| ConnectionError::SocketError(format!("Error receiving: {} ({})", e, e.to_raw()))), + } + }, + + Err(e) => Err(ConnectionError::SocketError(format!("Failed to poll: {}", e))), + } + } +} + +impl Drop for ConnectionMonitor { + fn drop(&mut self) { + debug!(target: LOG_TARGET, "Connection monitor dropped"); + } +} + +/// Represents an event for a socket +#[derive(Debug)] +pub struct SocketEvent { + /// The type of event received + pub event_type: SocketEventType, + /// The value of the event. This value depends on the event received. + /// Usually nothing (zero) or a file descriptor for the monitored socket. + pub event_value: u32, + /// The address of the connection which triggered this event + pub address: String, +} + +/// Represents the types of socket events which can occur. +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum SocketEventType { + Connected = 0x0001, + ConnectDelayed = 0x0002, + ConnectRetried = 0x0004, + Listening = 0x0008, + BindFailed = 0x0010, + Accepted = 0x0020, + AcceptFailed = 0x0040, + Closed = 0x0080, + CloseFailed = 0x0100, + Disconnected = 0x0200, + MonitorStopped = 0x0400, + HandshakeFailedNoDetail = 0x0800, + HandshakeSucceeded = 0x1000, + HandshakeFailedProtocol = 0x2000, + HandshakeFailedAuth = 0x4000, +} + +impl fmt::Display for SocketEventType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", &self) + } +} + +impl TryFrom for SocketEventType { + type Error = ConnectionError; + + /// Try to convert from a u16 to a SocketEventType + fn try_from(raw: u16) -> Result { + let event = match raw { + 0x0001 => SocketEventType::Connected, + 0x0002 => SocketEventType::ConnectDelayed, + 0x0004 => SocketEventType::ConnectRetried, + 0x0008 => SocketEventType::Listening, + 0x0010 => SocketEventType::BindFailed, + 0x0020 => SocketEventType::Accepted, + 0x0040 => SocketEventType::AcceptFailed, + 0x0080 => SocketEventType::Closed, + 0x0100 => SocketEventType::CloseFailed, + 0x0200 => SocketEventType::Disconnected, + 0x0400 => SocketEventType::MonitorStopped, + 0x0800 => SocketEventType::HandshakeFailedNoDetail, + 0x1000 => SocketEventType::HandshakeSucceeded, + 0x2000 => SocketEventType::HandshakeFailedProtocol, + 0x4000 => SocketEventType::HandshakeFailedAuth, + _ => return Err(ConnectionMonitorError::SocketEventConversionFailed.into()), + }; + + Ok(event) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn socket_event_type_try_from() { + let valid_event_types = vec![ + 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080, 0x0100, 0x0200, 0x0400, 0x0800, 0x1000, + 0x2000, 0x4000, + ]; + + for raw_evt in valid_event_types { + let evt_type: Result = raw_evt.try_into(); + assert!(evt_type.is_ok()); + } + + let invalid: Result = 0xF000u16.try_into(); + assert!(invalid.is_err()); + } +} diff --git a/infrastructure/comms/src/connection/net_address/i2p.rs b/comms/src/connection/net_address/i2p.rs similarity index 89% rename from infrastructure/comms/src/connection/net_address/i2p.rs rename to comms/src/connection/net_address/i2p.rs index 90401b96fc..331788abd8 100644 --- a/infrastructure/comms/src/connection/net_address/i2p.rs +++ b/comms/src/connection/net_address/i2p.rs @@ -20,16 +20,22 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::str::FromStr; - use super::{parser::AddressParser, NetAddressError}; +use serde::{Deserialize, Serialize}; +use std::{fmt, str::FromStr}; /// Represents an I2P address -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Hash, Deserialize, Serialize)] pub struct I2PAddress { pub name: String, } +impl I2PAddress { + pub fn host(&self) -> String { + format!("{}.b32.i2p", self.name) + } +} + impl FromStr for I2PAddress { type Err = NetAddressError; @@ -42,6 +48,12 @@ impl FromStr for I2PAddress { } } +impl fmt::Display for I2PAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}.b32.i2p", self.name) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/infrastructure/comms/src/connection/net_address/ip.rs b/comms/src/connection/net_address/ip.rs similarity index 80% rename from infrastructure/comms/src/connection/net_address/ip.rs rename to comms/src/connection/net_address/ip.rs index c635ba0372..9f01b6b199 100644 --- a/infrastructure/comms/src/connection/net_address/ip.rs +++ b/comms/src/connection/net_address/ip.rs @@ -20,14 +20,32 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{net::SocketAddr, str::FromStr}; - use crate::connection::NetAddressError; +use serde::{Deserialize, Serialize}; +use std::{ + fmt, + net::{IpAddr, SocketAddr, ToSocketAddrs}, + str::FromStr, +}; /// Represents an {IPv4, IPv6} address and port -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)] pub struct SocketAddress(SocketAddr); +impl SocketAddress { + pub fn ip(&self) -> IpAddr { + self.0.ip() + } + + pub fn host(&self) -> String { + self.0.ip().to_string() + } + + pub fn port(&self) -> u16 { + self.0.port() + } +} + impl FromStr for SocketAddress { type Err = NetAddressError; @@ -38,6 +56,26 @@ impl FromStr for SocketAddress { } } +impl> From<(I, u16)> for SocketAddress { + fn from(v: (I, u16)) -> Self { + Self(v.into()) + } +} + +impl ToSocketAddrs for SocketAddress { + type Iter = std::option::IntoIter; + + fn to_socket_addrs(&self) -> std::io::Result { + self.0.to_socket_addrs() + } +} + +impl fmt::Display for SocketAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}", self.ip(), self.port()) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/infrastructure/comms/src/connection/net_address/mod.rs b/comms/src/connection/net_address/mod.rs similarity index 70% rename from infrastructure/comms/src/connection/net_address/mod.rs rename to comms/src/connection/net_address/mod.rs index 5181f92d16..5da5ad96fc 100644 --- a/infrastructure/comms/src/connection/net_address/mod.rs +++ b/comms/src/connection/net_address/mod.rs @@ -22,20 +22,31 @@ pub mod i2p; pub mod ip; +pub mod net_address_with_stats; +pub mod net_addresses; pub mod onion; pub mod parser; +use self::{i2p::I2PAddress, ip::SocketAddress, onion::OnionAddress}; +use crate::connection::zmq::ZmqEndpoint; use derive_error::Error; -use std::str::FromStr; +use serde::{Deserialize, Serialize}; +use std::{fmt, str::FromStr}; -use self::{i2p::I2PAddress, ip::SocketAddress, onion::OnionAddress}; +pub use self::{net_address_with_stats::NetAddressWithStats, net_addresses::NetAddressesWithStats}; -#[derive(Debug, Error)] +#[derive(Debug, Error, PartialEq)] pub enum NetAddressError { /// Failed to parse address ParseFailed, /// Specified port range is invalid InvalidPortRange, + /// The specified net address does not exist + AddressNotFound, + /// Empty set of net addresses + NoValidAddresses, + /// The number of connection attempts for all net addresses in the set exceeded the threshold + ConnectionAttemptsExceeded, } /// A Tari network address, either IP (v4 or v6), Tor Onion or I2P. @@ -50,12 +61,12 @@ pub enum NetAddressError { /// assert!(address.is_ok()); /// assert!(address.unwrap().is_tor()); /// ``` -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Hash, Deserialize, Serialize)] /// Represents an address which can be used to reach a node on the network pub enum NetAddress { /// IPv4 and IPv6 IP(SocketAddress), - Tor(OnionAddress), + Onion(OnionAddress), I2P(I2PAddress), } @@ -71,7 +82,7 @@ impl NetAddress { /// Returns true if the [`NetAddress`] is a Tor Onion address, otherwise false pub fn is_tor(&self) -> bool { match *self { - NetAddress::Tor(_) => true, + NetAddress::Onion(_) => true, _ => false, } } @@ -83,6 +94,23 @@ impl NetAddress { _ => false, } } + + pub fn host(&self) -> String { + match self { + NetAddress::Onion(addr) => addr.host(), + NetAddress::IP(addr) => addr.host(), + NetAddress::I2P(addr) => addr.host(), + } + } + + /// Returns the port for the NetAddress if applicable, otherwise None + pub fn maybe_port(&self) -> Option { + match self { + NetAddress::Onion(addr) => Some(addr.port()), + NetAddress::IP(addr) => Some(addr.port()), + NetAddress::I2P(_) => None, + } + } } impl FromStr for NetAddress { @@ -102,6 +130,18 @@ impl FromStr for NetAddress { } } +impl fmt::Display for NetAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use NetAddress::*; + + match *self { + IP(ref addr) => write!(f, "{}", addr), + Onion(ref addr) => write!(f, "{}", addr), + I2P(ref addr) => write!(f, "{}", addr), + } + } +} + impl From for NetAddress { /// Converts a [`SocketAddress`] into a [`NetAddress::IP`]. fn from(addr: SocketAddress) -> Self { @@ -112,7 +152,7 @@ impl From for NetAddress { impl From for NetAddress { /// Converts a [`OnionAddress`] into a [`NetAddress::Tor`]. fn from(addr: OnionAddress) -> Self { - NetAddress::Tor(addr) + NetAddress::Onion(addr) } } @@ -123,6 +163,17 @@ impl From for NetAddress { } } +impl ZmqEndpoint for NetAddress { + fn to_zmq_endpoint(&self) -> String { + match *self { + NetAddress::IP(ref addr) => format!("tcp://{}:{}", addr.ip(), addr.port()), + NetAddress::Onion(ref addr) => format!("tcp://{}:{}", addr.public_key, addr.port), + // TODO: need to confirm this works + NetAddress::I2P(ref addr) => format!("tcp://{}.b32.i2p", addr.name), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/comms/src/connection/net_address/net_address_with_stats.rs b/comms/src/connection/net_address/net_address_with_stats.rs new file mode 100644 index 0000000000..c44f0355a7 --- /dev/null +++ b/comms/src/connection/net_address/net_address_with_stats.rs @@ -0,0 +1,254 @@ +use crate::connection::NetAddress; +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use std::{ + cmp::{Ord, Ordering}, + fmt, + time::Duration, +}; + +const MAX_LATENCY_SAMPLE_COUNT: u32 = 100; + +#[derive(Debug, Eq, Clone, Deserialize, Serialize)] +pub struct NetAddressWithStats { + pub net_address: NetAddress, + pub last_seen: Option>, + pub connection_attempts: u32, + pub rejected_message_count: u32, + pub avg_latency: Duration, + latency_sample_count: u32, +} + +impl NetAddressWithStats { + /// Constructs a new net address with zero stats + pub fn new(net_address: NetAddress) -> NetAddressWithStats { + NetAddressWithStats { + net_address, + last_seen: None, + connection_attempts: 0, + rejected_message_count: 0, + avg_latency: Duration::from_millis(0), + latency_sample_count: 0, + } + } + + /// Constructs a new net address with usage stats + pub fn new_with_stats( + net_address: NetAddress, + last_seen: Option>, + connection_attempts: u32, + rejected_message_count: u32, + avg_latency: Duration, + latency_sample_count: u32, + ) -> NetAddressWithStats + { + NetAddressWithStats { + net_address, + last_seen, + connection_attempts, + rejected_message_count, + avg_latency, + latency_sample_count, + } + } + + /// Updates the average latency by including another measured latency sample. The historical average is updated by + /// allowing the new measurement to provide a weighted contribution to the historical average. As more samples are + /// received the historical average will have a larger weight compare to the new measurement, this will have a + /// filtering effect similar to a sliding window without needing previous measurements to be stored. When a new + /// latency measurement is received and the latency_sample_count is equal or have surpassed the + /// MAX_LATENCY_SAMPLE_COUNT then the current avg_latency is scaled so that the new latency_measurement only makes a + /// small weighted change to the avg_latency. The previous avg_latency will have a weight of + /// MAX_LATENCY_SAMPLE_COUNT and the new latency_measurement will have a weight of 1. + pub fn update_latency(&mut self, latency_measurement: Duration) { + self.last_seen = Some(Utc::now()); + + self.avg_latency = + ((self.avg_latency * self.latency_sample_count) + latency_measurement) / (self.latency_sample_count + 1); + if self.latency_sample_count < MAX_LATENCY_SAMPLE_COUNT { + self.latency_sample_count += 1; + } + } + + /// Mark that a message was received from this net address + pub fn mark_message_received(&mut self) { + self.last_seen = Some(Utc::now()); + } + + /// Mark that a rejected message was received from this net address + pub fn mark_message_rejected(&mut self) { + self.last_seen = Some(Utc::now()); + self.rejected_message_count += 1; + } + + /// Mark that a successful connection was established with this net address + pub fn mark_successful_connection_attempt(&mut self) { + self.last_seen = Some(Utc::now()); + self.connection_attempts = 0; + } + + /// Reset the connection attempts on this net address for a later session of retries + pub fn reset_connection_attempts(&mut self) { + self.connection_attempts = 0; + } + + /// Mark that a connection could not be established with this net address + pub fn mark_failed_connection_attempt(&mut self) { + self.connection_attempts += 1; + } + + /// Get as a NetAddress + pub fn as_net_address(&self) -> NetAddress { + self.clone().net_address + } +} + +impl From for NetAddressWithStats { + /// Constructs a new net address with usage stats from a net address + fn from(net_address: NetAddress) -> Self { + NetAddressWithStats { + net_address, + last_seen: None, + connection_attempts: 0, + rejected_message_count: 0, + avg_latency: Duration::new(0, 0), + latency_sample_count: 0, + } + } +} + +// Reliability ordering of net addresses: prioritize net addresses according to previous successful connections, +// connection attempts, latency and last seen A lower ordering has a higher priority and a higher ordering has a lower +// priority, this ordering switch allows searching for, and updating of net addresses to be performed more efficiently +impl Ord for NetAddressWithStats { + fn cmp(&self, other: &NetAddressWithStats) -> Ordering { + if self.last_seen.is_some() && other.last_seen.is_none() { + return Ordering::Less; + } else if self.last_seen.is_none() && other.last_seen.is_some() { + return Ordering::Greater; + } + if self.connection_attempts < other.connection_attempts { + return Ordering::Less; + } else if self.connection_attempts > other.connection_attempts { + return Ordering::Greater; + } + if self.latency_sample_count > 0 && other.latency_sample_count > 0 { + if self.avg_latency < other.avg_latency { + return Ordering::Less; + } else if self.avg_latency > other.avg_latency { + return Ordering::Greater; + } + } + if self.last_seen.is_some() && other.last_seen.is_some() { + let self_last_seen = self.last_seen.unwrap(); + let other_last_seen = other.last_seen.unwrap(); + if self_last_seen > other_last_seen { + return Ordering::Less; + } else if self_last_seen < other_last_seen { + return Ordering::Greater; + } + } + Ordering::Equal + } +} + +impl PartialOrd for NetAddressWithStats { + fn partial_cmp(&self, other: &NetAddressWithStats) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for NetAddressWithStats { + fn eq(&self, other: &NetAddressWithStats) -> bool { + self.net_address == other.net_address + } +} + +impl fmt::Display for NetAddressWithStats { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.net_address) + } +} + +#[cfg(test)] +mod test { + use crate::connection::{net_address::net_address_with_stats::NetAddressWithStats, NetAddress}; + use std::{thread, time::Duration}; + + #[test] + fn test_update_latency() { + let net_address = "123.0.0.123:8000".parse::().unwrap(); + let mut net_address_with_stats = NetAddressWithStats::from(net_address); + let latency_measurement1 = Duration::from_millis(100); + let latency_measurement2 = Duration::from_millis(200); + let latency_measurement3 = Duration::from_millis(60); + let latency_measurement4 = Duration::from_millis(140); + net_address_with_stats.update_latency(latency_measurement1); + assert_eq!(net_address_with_stats.avg_latency, latency_measurement1); + net_address_with_stats.update_latency(latency_measurement2); + assert_eq!(net_address_with_stats.avg_latency, Duration::from_millis(150)); + net_address_with_stats.update_latency(latency_measurement3); + assert_eq!(net_address_with_stats.avg_latency, Duration::from_millis(120)); + net_address_with_stats.update_latency(latency_measurement4); + assert_eq!(net_address_with_stats.avg_latency, Duration::from_millis(125)); + } + + #[test] + fn test_message_received_and_rejected() { + let net_address = "123.0.0.123:8000".parse::().unwrap(); + let mut net_address_with_stats = NetAddressWithStats::from(net_address); + assert!(net_address_with_stats.last_seen.is_none()); + net_address_with_stats.mark_message_received(); + assert!(net_address_with_stats.last_seen.is_some()); + let last_seen = net_address_with_stats.last_seen.unwrap(); + net_address_with_stats.mark_message_rejected(); + net_address_with_stats.mark_message_rejected(); + assert_eq!(net_address_with_stats.rejected_message_count, 2); + assert!(last_seen < net_address_with_stats.last_seen.unwrap()); + } + + #[test] + fn test_successful_and_failed_connection_attempts() { + let net_address = "123.0.0.123:8000".parse::().unwrap(); + let mut net_address_with_stats = NetAddressWithStats::from(net_address); + net_address_with_stats.mark_failed_connection_attempt(); + net_address_with_stats.mark_failed_connection_attempt(); + assert!(net_address_with_stats.last_seen.is_none()); + assert_eq!(net_address_with_stats.connection_attempts, 2); + net_address_with_stats.mark_successful_connection_attempt(); + assert!(net_address_with_stats.last_seen.is_some()); + assert_eq!(net_address_with_stats.connection_attempts, 0); + } + + #[test] + fn test_reseting_connection_attempts() { + let net_address = "123.0.0.123:8000".parse::().unwrap(); + let mut net_address_with_stats = NetAddressWithStats::from(net_address); + net_address_with_stats.mark_failed_connection_attempt(); + net_address_with_stats.mark_failed_connection_attempt(); + assert_eq!(net_address_with_stats.connection_attempts, 2); + net_address_with_stats.reset_connection_attempts(); + assert_eq!(net_address_with_stats.connection_attempts, 0); + } + + #[test] + fn test_net_address_reliability_ordering() { + let net_address = "123.0.0.123:8000".parse::().unwrap(); + let mut na1 = NetAddressWithStats::from(net_address.clone()); + let mut na2 = NetAddressWithStats::from(net_address); + thread::sleep(Duration::from_millis(1)); + na1.mark_successful_connection_attempt(); + assert!(na1 < na2); + thread::sleep(Duration::from_millis(1)); + na2.mark_successful_connection_attempt(); + assert!(na1 > na2); + thread::sleep(Duration::from_millis(1)); + na1.mark_message_rejected(); + assert!(na1 < na2); + na1.update_latency(Duration::from_millis(200)); + na2.update_latency(Duration::from_millis(100)); + assert!(na1 > na2); + na1.mark_failed_connection_attempt(); + assert!(na1 > na2); + } +} diff --git a/comms/src/connection/net_address/net_addresses.rs b/comms/src/connection/net_address/net_addresses.rs new file mode 100644 index 0000000000..caf2857a92 --- /dev/null +++ b/comms/src/connection/net_address/net_addresses.rs @@ -0,0 +1,382 @@ +use crate::connection::{ + net_address::{net_address_with_stats::NetAddressWithStats, NetAddressError}, + NetAddress, +}; +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use std::{ops::Index, time::Duration}; + +pub const MAX_CONNECTION_ATTEMPTS: u32 = 3; + +/// This struct is used to store a set of different net addresses such as IPv4, IPv6, Tor or I2P for a single peer. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] +pub struct NetAddressesWithStats { + pub addresses: Vec, + last_attempted: Option>, +} + +impl NetAddressesWithStats { + /// Constructs a new list of addresses with usage stats from a list of net addresses + pub fn new(addresses: Vec) -> NetAddressesWithStats { + NetAddressesWithStats { + addresses, + last_attempted: None, + } + } + + /// Finds the specified address in the set and allow updating of its variables such as its usage stats + pub fn find_address_mut(&mut self, address: &NetAddress) -> Result<&mut NetAddressWithStats, NetAddressError> { + for (i, curr_address) in &mut self.addresses.iter().enumerate() { + if curr_address.net_address == *address { + return self.addresses.get_mut(i).ok_or(NetAddressError::AddressNotFound); + } + } + Err(NetAddressError::AddressNotFound) + } + + /// Provides the date and time of the last successful communication with this peer + pub fn last_seen(&self) -> Option> { + let mut latest_valid_datetime: Option> = None; + for curr_address in &self.addresses { + if curr_address.last_seen.is_none() { + continue; + } + match latest_valid_datetime { + Some(latest_datetime) => { + if latest_datetime < curr_address.last_seen.unwrap() { + latest_valid_datetime = curr_address.last_seen; + } + }, + None => latest_valid_datetime = curr_address.last_seen, + } + } + latest_valid_datetime + } + + /// Return the time of last attempted connection to this collection of addresses + pub fn last_attempted(&self) -> Option> { + self.last_attempted + } + + /// Adds a new net address to the peer. This function will not add a duplicate if the address + /// already exists. + pub fn add_net_address(&mut self, net_address: &NetAddress) { + if !self.addresses.iter().any(|x| x.net_address == *net_address) { + self.addresses.push(net_address.clone().into()); + } + } + + /// Compares the existing set of net_addresses to the provided net_address set and remove missing net_addresses and + /// add new net_addresses without discarding the usage stats of the existing and remaining net_addresses. + pub fn update_net_addresses(&mut self, net_addresses: Vec) { + // Remove missing elements + let mut remove_indices: Vec = Vec::new(); + for index in 0..self.addresses.len() { + if !net_addresses + .iter() + .any(|new_net_address| *new_net_address == self.addresses[index].net_address) + { + remove_indices.push(index); + } + } + for index in remove_indices.iter().rev() { + self.addresses.remove(*index); + } + // Add new elements + for new_net_address in &net_addresses { + if !self + .addresses + .iter() + .any(|curr_net_address| curr_net_address.net_address == *new_net_address) + { + self.add_net_address(new_net_address); + } + } + } + + /// Finds and returns the highest priority net address until all connection attempts for each net address have been + /// reached. Should the node fail to connect to the address, the address should be marked as such + /// using `mark_failed_connection_attempt`. If a maximum number of attempts is reached, for all addresses + /// a `NetAddressError::ConnectionAttemptsExceeded` error is returned. + pub fn get_best_net_address(&mut self) -> Result { + if !self.addresses.is_empty() { + let any_reachable = self + .addresses + .iter() + .any(|a| a.connection_attempts < MAX_CONNECTION_ATTEMPTS); + if any_reachable { + self.addresses.sort(); + Ok(self.addresses[0].net_address.clone()) + } else { + Err(NetAddressError::ConnectionAttemptsExceeded) + } + } else { + Err(NetAddressError::NoValidAddresses) + } + } + + /// The average connection latency of the provided net address will be updated to include the current measured + /// latency sample + pub fn update_latency( + &mut self, + address: &NetAddress, + latency_measurement: Duration, + ) -> Result<(), NetAddressError> + { + let updatable_address = self.find_address_mut(address)?; + updatable_address.update_latency(latency_measurement); + Ok(()) + } + + /// Mark that a message was received from the specified net address + pub fn mark_message_received(&mut self, address: &NetAddress) -> Result<(), NetAddressError> { + let updatable_address = self.find_address_mut(address)?; + updatable_address.mark_message_received(); + Ok(()) + } + + /// Mark that a rejected message was received from the specified net address + pub fn mark_message_rejected(&mut self, address: &NetAddress) -> Result<(), NetAddressError> { + let updatable_address = self.find_address_mut(address)?; + updatable_address.mark_message_rejected(); + Ok(()) + } + + /// Mark that a successful connection was established with the specified net address + pub fn mark_successful_connection_attempt(&mut self, address: &NetAddress) -> Result<(), NetAddressError> { + let updatable_address = self.find_address_mut(address)?; + updatable_address.mark_successful_connection_attempt(); + self.last_attempted = Some(Utc::now()); + Ok(()) + } + + /// Mark that a connection could not be established with the specified net address + pub fn mark_failed_connection_attempt(&mut self, address: &NetAddress) -> Result<(), NetAddressError> { + let updatable_address = self.find_address_mut(address)?; + updatable_address.mark_failed_connection_attempt(); + self.last_attempted = Some(Utc::now()); + Ok(()) + } + + /// Reset the connection attempts stat on all of this Peers net addresses to retry connection + pub fn reset_connection_attempts(&mut self) { + for a in self.addresses.iter_mut() { + a.reset_connection_attempts(); + } + } + + /// Returns the number of addresses + pub fn len(&self) -> usize { + self.addresses.len() + } + + /// Returns if there are addresses or not + pub fn is_empty(&self) -> bool { + self.addresses.is_empty() + } +} + +impl Index for NetAddressesWithStats { + type Output = NetAddressWithStats; + + /// Returns the NetAddressWithStats at the given index + fn index(&self, index: usize) -> &Self::Output { + &self.addresses[index] + } +} + +impl From for NetAddressesWithStats { + /// Constructs a new list of addresses with usage stats from a single net address + fn from(net_address: NetAddress) -> Self { + NetAddressesWithStats { + addresses: vec![NetAddressWithStats::from(net_address)], + last_attempted: None, + } + } +} + +impl From> for NetAddressesWithStats { + /// Constructs a new list of addresses with usage stats from a Vec + fn from(net_addresses: Vec) -> Self { + NetAddressesWithStats { + addresses: net_addresses + .into_iter() + .map(NetAddressWithStats::from) + .collect::>(), + last_attempted: None, + } + } +} + +impl From> for NetAddressesWithStats { + /// Constructs NetAddressesWithStats from a list of addresses with usage stats + fn from(addresses: Vec) -> Self { + NetAddressesWithStats { + addresses, + last_attempted: None, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::connection::{ + net_address::{net_address_with_stats::NetAddressWithStats, net_addresses::NetAddressesWithStats}, + NetAddress, + }; + use std::thread; + + #[test] + fn test_index_impl() { + let net_address1 = "123.0.0.123:8000".parse::().unwrap(); + let net_address2 = "125.1.54.254:7999".parse::().unwrap(); + let net_address3 = "175.6.3.145:8000".parse::().unwrap(); + let net_addresses: NetAddressesWithStats = + vec![net_address1.clone(), net_address2.clone(), net_address3.clone()].into(); + + assert_eq!(net_addresses[0].net_address, net_address1); + assert_eq!(net_addresses[1].net_address, net_address2); + assert_eq!(net_addresses[2].net_address, net_address3); + } + + #[test] + fn test_last_seen() { + let net_address1 = "123.0.0.123:8000".parse::().unwrap(); + let net_address2 = "125.1.54.254:7999".parse::().unwrap(); + let net_address3 = "175.6.3.145:8000".parse::().unwrap(); + let mut net_addresses = NetAddressesWithStats::from(net_address1.clone()); + net_addresses.add_net_address(&net_address2); + net_addresses.add_net_address(&net_address3); + + assert!(net_addresses.mark_successful_connection_attempt(&net_address3).is_ok()); + assert!(net_addresses.mark_successful_connection_attempt(&net_address1).is_ok()); + assert!(net_addresses.mark_successful_connection_attempt(&net_address2).is_ok()); + let desired_last_seen = net_addresses.addresses[1].last_seen; + let last_seen = net_addresses.last_seen(); + assert!(desired_last_seen.is_some()); + assert!(last_seen.is_some()); + assert_eq!(desired_last_seen.unwrap(), last_seen.unwrap()); + } + + #[test] + fn test_add_net_address() { + let net_address1 = "123.0.0.123:8000".parse::().unwrap(); + let net_address2 = "125.1.54.254:7999".parse::().unwrap(); + let net_address3 = "175.6.3.145:8000".parse::().unwrap(); + let mut net_addresses = NetAddressesWithStats::from(net_address1.clone()); + net_addresses.add_net_address(&net_address2); + net_addresses.add_net_address(&net_address3); + // Add duplicate address, test add_net_address is idempotent + net_addresses.add_net_address(&net_address2); + assert_eq!(net_addresses.addresses.len(), 3); + assert_eq!(net_addresses.addresses[0].net_address, net_address1); + assert_eq!(net_addresses.addresses[1].net_address, net_address2); + assert_eq!(net_addresses.addresses[2].net_address, net_address3); + } + + #[test] + fn test_get_net_address() { + let net_address1 = "123.0.0.123:8000".parse::().unwrap(); + let net_address2 = "125.1.54.254:7999".parse::().unwrap(); + let net_address3 = "175.6.3.145:8000".parse::().unwrap(); + let mut net_addresses = NetAddressesWithStats::from(net_address1.clone()); + net_addresses.add_net_address(&net_address2); + net_addresses.add_net_address(&net_address3); + + let mut priority_address = net_addresses.get_best_net_address(); + assert!(priority_address.is_ok()); + assert_eq!(priority_address.unwrap(), net_address1); + + assert!(net_addresses + .update_latency(&net_address1, Duration::from_millis(250)) + .is_ok()); + assert!(net_addresses + .update_latency(&net_address2, Duration::from_millis(50)) + .is_ok()); + assert!(net_addresses + .update_latency(&net_address3, Duration::from_millis(100)) + .is_ok()); + priority_address = net_addresses.get_best_net_address(); + assert!(priority_address.is_ok()); + assert_eq!(priority_address.unwrap(), net_address2); + + assert!(net_addresses.mark_failed_connection_attempt(&net_address2).is_ok()); + priority_address = net_addresses.get_best_net_address(); + assert!(priority_address.is_ok()); + assert_eq!(priority_address.unwrap(), net_address3); + + for _i in 0..MAX_CONNECTION_ATTEMPTS { + assert!(net_addresses.mark_failed_connection_attempt(&net_address1).is_ok()); + assert!(net_addresses.mark_failed_connection_attempt(&net_address2).is_ok()); + assert!(net_addresses.mark_failed_connection_attempt(&net_address3).is_ok()); + } + assert!(net_addresses.get_best_net_address().is_err()); + } + + #[test] + fn test_stats_updates_on_addresses() { + let net_address1 = "123.0.0.123:8000".parse::().unwrap(); + let net_address2 = "125.1.54.254:7999".parse::().unwrap(); + let net_address3 = "175.6.3.145:8000".parse::().unwrap(); + let mut addresses: Vec = Vec::new(); + addresses.push(NetAddressWithStats::from(net_address1.clone())); + addresses.push(NetAddressWithStats::from(net_address2.clone())); + addresses.push(NetAddressWithStats::from(net_address3.clone())); + let mut net_addresses = NetAddressesWithStats::new(addresses); + + assert!(net_addresses + .update_latency(&net_address2, Duration::from_millis(200)) + .is_ok()); + assert_eq!(net_addresses.addresses[0].avg_latency, Duration::from_millis(0)); + assert_eq!(net_addresses.addresses[1].avg_latency, Duration::from_millis(200)); + assert_eq!(net_addresses.addresses[2].avg_latency, Duration::from_millis(0)); + + thread::sleep(Duration::from_millis(1)); + assert!(net_addresses.mark_message_received(&net_address1).is_ok()); + assert!(net_addresses.addresses[0].last_seen.is_some()); + assert!(net_addresses.addresses[1].last_seen.is_some()); + assert!(net_addresses.addresses[2].last_seen.is_none()); + assert!(net_addresses.addresses[0].last_seen.unwrap() > net_addresses.addresses[1].last_seen.unwrap()); + + assert!(net_addresses.mark_message_rejected(&net_address2).is_ok()); + assert!(net_addresses.mark_message_rejected(&net_address3).is_ok()); + assert!(net_addresses.mark_message_rejected(&net_address3).is_ok()); + assert_eq!(net_addresses.addresses[0].rejected_message_count, 0); + assert_eq!(net_addresses.addresses[1].rejected_message_count, 1); + assert_eq!(net_addresses.addresses[2].rejected_message_count, 2); + + assert!(net_addresses.mark_failed_connection_attempt(&net_address1).is_ok()); + assert!(net_addresses.mark_failed_connection_attempt(&net_address2).is_ok()); + assert!(net_addresses.mark_failed_connection_attempt(&net_address3).is_ok()); + assert!(net_addresses.mark_failed_connection_attempt(&net_address1).is_ok()); + assert!(net_addresses.mark_successful_connection_attempt(&net_address2).is_ok()); + assert_eq!(net_addresses.addresses[0].connection_attempts, 2); + assert_eq!(net_addresses.addresses[1].connection_attempts, 0); + assert_eq!(net_addresses.addresses[2].connection_attempts, 1); + } + + #[test] + fn test_resetting_all_connection_attempts() { + let net_address1 = "123.0.0.123:8000".parse::().unwrap(); + let net_address2 = "125.1.54.254:7999".parse::().unwrap(); + let net_address3 = "175.6.3.145:8000".parse::().unwrap(); + let mut addresses: Vec = Vec::new(); + addresses.push(NetAddressWithStats::from(net_address1.clone())); + addresses.push(NetAddressWithStats::from(net_address2.clone())); + addresses.push(NetAddressWithStats::from(net_address3.clone())); + let mut net_addresses = NetAddressesWithStats::new(addresses); + assert!(net_addresses.mark_failed_connection_attempt(&net_address1).is_ok()); + assert!(net_addresses.mark_failed_connection_attempt(&net_address2).is_ok()); + assert!(net_addresses.mark_failed_connection_attempt(&net_address3).is_ok()); + assert!(net_addresses.mark_failed_connection_attempt(&net_address1).is_ok()); + + assert_eq!(net_addresses.addresses[0].connection_attempts, 2); + assert_eq!(net_addresses.addresses[1].connection_attempts, 1); + assert_eq!(net_addresses.addresses[2].connection_attempts, 1); + net_addresses.reset_connection_attempts(); + assert_eq!(net_addresses.addresses[0].connection_attempts, 0); + assert_eq!(net_addresses.addresses[1].connection_attempts, 0); + assert_eq!(net_addresses.addresses[2].connection_attempts, 0); + } +} diff --git a/infrastructure/comms/src/connection/net_address/onion.rs b/comms/src/connection/net_address/onion.rs similarity index 89% rename from infrastructure/comms/src/connection/net_address/onion.rs rename to comms/src/connection/net_address/onion.rs index 945e1713d5..4c479d6d97 100644 --- a/infrastructure/comms/src/connection/net_address/onion.rs +++ b/comms/src/connection/net_address/onion.rs @@ -20,17 +20,29 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::str::FromStr; +use std::{fmt, str::FromStr}; + +use serde::{Deserialize, Serialize}; use super::{parser::AddressParser, NetAddressError}; /// Represents a Tor Onion address -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)] pub struct OnionAddress { pub public_key: String, pub port: u16, } +impl OnionAddress { + pub fn host(&self) -> String { + format!("{}.onion", self.public_key) + } + + pub fn port(&self) -> u16 { + self.port + } +} + impl FromStr for OnionAddress { type Err = NetAddressError; @@ -43,6 +55,12 @@ impl FromStr for OnionAddress { } } +impl fmt::Display for OnionAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}.onion:{}", self.public_key, self.port) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/infrastructure/comms/src/connection/net_address/parser.rs b/comms/src/connection/net_address/parser.rs similarity index 96% rename from infrastructure/comms/src/connection/net_address/parser.rs rename to comms/src/connection/net_address/parser.rs index d201ccb98d..86573688c0 100644 --- a/infrastructure/comms/src/connection/net_address/parser.rs +++ b/comms/src/connection/net_address/parser.rs @@ -88,16 +88,14 @@ impl<'a> AddressParser<'a> { None => return None, } - if p.consume_char(':').is_none() { - return None; - } + p.consume_char(':')?; let port = match p.read_number() { Some(p) => p, None => return None, }; - if port > std::u16::MAX as u64 { + if port > u64::from(std::u16::MAX) { return None; } @@ -131,7 +129,7 @@ impl<'a> AddressParser<'a> { self.pos += 1; } - if buf.len() > 0 { + if !buf.is_empty() { match String::from_utf8(buf) { Ok(s) => Some(s), Err(_) => None, @@ -143,7 +141,7 @@ impl<'a> AddressParser<'a> { fn read_char(&mut self) -> Option { if self.is_end() { - return None; + None } else { let ch = self.data[self.pos]; self.pos += 1; @@ -168,7 +166,7 @@ impl<'a> AddressParser<'a> { if ch < b'0' || ch > b'9' { break; } - number = number * 10u64 + (ch - b'0') as u64; + number = number * 10u64 + u64::from(ch - b'0'); pos += 1; } diff --git a/comms/src/connection/peer_connection/connection.rs b/comms/src/connection/peer_connection/connection.rs new file mode 100644 index 0000000000..43e556f763 --- /dev/null +++ b/comms/src/connection/peer_connection/connection.rs @@ -0,0 +1,839 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{ + control::{ControlMessage, ThreadControlMessenger}, + worker::PeerConnectionWorker, + PeerConnectionContext, + PeerConnectionError, +}; +use crate::{ + connection::{ + net_address::ip::SocketAddress, + types::{Linger, Result}, + ConnectionError, + Direction, + NetAddress, + }, + message::FrameSet, +}; +use chrono::{NaiveDateTime, Utc}; +use std::{ + fmt, + sync::{Arc, RwLock}, + thread::{self, JoinHandle}, + time::Duration, +}; +use tari_utilities::hex::to_hex; + +/// Represents messages that must be sent to a PeerConnection. +pub enum PeerConnectionProtoMessage { + /// Sent to establish the identity frame for a PeerConnection. This must be sent by an + /// Outbound connection to an Inbound connection before any other communication occurs. + Identify = 0, + /// A peer message to be forwarded to the message sink (the IMS) + Message = 1, + /// Any other message is invalid and is discarded + Invalid, +} + +impl From for PeerConnectionProtoMessage { + fn from(val: u8) -> Self { + match val { + 0 => PeerConnectionProtoMessage::Identify, + 1 => PeerConnectionProtoMessage::Message, + _ => PeerConnectionProtoMessage::Invalid, + } + } +} + +/// Represents the ID of a PeerConnection. This is sent as the first frame +/// to the message sink on the peer connection. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ConnectionId(Vec); + +impl ConnectionId { + pub fn new(id: Vec) -> Self { + Self(id) + } + + pub fn into_inner(self) -> Vec { + self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_slice() + } + + /// Returns a shortened (length of 8 or less) connection ID + /// This would typically be used for display purposes when the connection ID is a + /// sufficiently large random value and you don't want to have large strings displayed. + pub fn to_short_id(&self) -> Self { + let start = match self.0.len().checked_sub(8) { + Some(s) => s, + None => self.0.len(), + }; + Self(self.0[start..].to_vec()) + } +} + +impl Default for ConnectionId { + fn default() -> Self { + Self(Default::default()) + } +} + +impl PartialEq for Vec { + fn eq(&self, other: &ConnectionId) -> bool { + self == &other.0 + } +} + +impl From> for ConnectionId { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From<&[u8]> for ConnectionId { + fn from(bytes: &[u8]) -> Self { + Self(bytes.to_vec()) + } +} + +impl From<&str> for ConnectionId { + fn from(bytes: &str) -> Self { + Self(bytes.as_bytes().to_vec()) + } +} + +impl AsRef<[u8]> for ConnectionId { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl fmt::Display for ConnectionId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", to_hex(self.as_bytes())) + } +} + +pub struct ConnectionInfo { + pub(super) control_messenger: Arc, + pub(super) connected_address: Option, +} + +/// The state of the PeerConnection +#[derive(Clone)] +pub(super) enum PeerConnectionState { + /// The connection object has been created but is not connected + Initial, + /// The connection thread is running, but the connection has not been accepted + Connecting(Arc), + /// The inbound connection is listening for connections + Listening(Arc), + /// The connection thread is running, and has been accepted. + Connected(Arc), + /// The connection has been shut down (node disconnected) + Shutdown, + /// The remote peer has disconnected + Disconnected, + /// Peer connection runner failed + Failed(PeerConnectionError), +} + +impl Default for PeerConnectionState { + fn default() -> Self { + PeerConnectionState::Initial + } +} + +macro_rules! is_state { + ($name: ident, $($e: pat)|*) => { + pub fn $name(&self) -> bool { + use PeerConnectionState::*; + let lock = acquire_read_lock!(self.state); + match *lock { + $($e)|* => true, + _ => false, + } + } + }; +} + +/// Basic stats for peer connections. PeerConnectionStats are updated by the [PeerConnectionWorker] +/// and read by the [PeerConnection]. +/// +/// [PeerConnectionWorker](../worker/struct.PeerConnectionWorker.html) +/// [PeerConnection](./struct.PeerConnection.html) +#[derive(Clone, Debug)] +pub struct PeerConnectionStats { + last_activity: NaiveDateTime, + messages_sent: usize, + messages_recv: usize, +} + +impl PeerConnectionStats { + pub fn new() -> Self { + Default::default() + } + + pub fn incr_message_recv(&mut self) { + self.messages_recv += 1; + self.last_activity = Utc::now().naive_utc(); + } + + pub fn incr_message_sent(&mut self) { + self.messages_sent += 1; + self.last_activity = Utc::now().naive_utc(); + } + + pub fn messages_sent(&self) -> usize { + self.messages_sent + } + + pub fn messages_recv(&self) -> usize { + self.messages_recv + } + + pub fn last_activity(&self) -> &NaiveDateTime { + &self.last_activity + } +} + +impl Default for PeerConnectionStats { + fn default() -> Self { + Self { + last_activity: Utc::now().naive_local(), + messages_sent: 0, + messages_recv: 0, + } + } +} + +/// Represents an asynchonous bi-directional connection to a Peer. +/// A PeerConnectionContext must be given to start the underlying thread +/// This may be easily shared and cloned across threads +/// +/// # Fields +/// +/// `state` - current state of the thread +/// +/// # Example +/// +/// ```edition2018 +/// +/// # use tari_comms::connection::*; +/// # use std::time::Duration; +/// +/// let ctx = ZmqContext::new(); +/// let addr: NetAddress = "127.0.0.1:8080".parse().unwrap(); +/// +/// let peer_context = PeerConnectionContextBuilder::new() +/// .set_id("123") +/// .set_context(&ctx) +/// .set_direction(Direction::Outbound) +/// .set_message_sink_address(InprocAddress::random()) +/// .set_address(addr.clone()) +/// .build() +/// .unwrap(); +/// +/// let mut conn = PeerConnection::new(); +/// +/// assert!(!conn.is_connected()); +/// // Start the peer connection worker thread +/// conn.start(peer_context).unwrap(); +/// // Wait for connection +/// // This will never connect because there is nothing +/// // listening on the other end +/// match conn.wait_connected_or_failure(&Duration::from_millis(100)) { +/// Ok(()) => { +/// assert!(conn.is_connected()); +/// println!("Able to establish connection on {}", addr); +/// } +/// Err(err) => { +/// assert!(!conn.is_connected()); +/// println!("Failed to connect to {} after 100ms (may still be trying if err is Timeout). Error: {:?}", addr, err); +/// } +/// } +/// ``` +#[derive(Default, Clone)] +pub struct PeerConnection { + state: Arc>, + connection_stats: Arc>, + direction: Option, + peer_address: Option, +} + +impl PeerConnection { + /// Returns true if the PeerConnection is in an `Initial` state, otherwise false + is_state!(is_initial, Initial); + + /// Returns true if the PeerConnection is in a `Connected` state, otherwise false + is_state!(is_connected, Connected(_)); + + /// Returns true if the PeerConnection is in a `Shutdown` state, otherwise false + is_state!(is_shutdown, Shutdown); + + /// Returns true if the PeerConnection is in a `Listening` state, otherwise false + is_state!(is_listening, Listening(_)); + + /// Returns true if the PeerConnection is in a `Disconnected`/`Shutdown`/`Failed` state, otherwise false + is_state!(is_disconnected, Disconnected | Shutdown | Failed(_)); + + /// Returns true if the PeerConnection is in a `Failed` state, otherwise false + is_state!(is_failed, Failed(_)); + + /// Returns true if the PeerConnection is in a `Connecting`, `Listening` or `Connected` state, otherwise false + is_state!(is_active, Connecting(_) | Connected(_) | Listening(_)); + + /// Create a new PeerConnection + pub fn new() -> Self { + Default::default() + } + + /// Start the worker thread for the PeerConnection and transition the + /// state to PeerConnectionState::Connected. The PeerConnection now + /// has a ThreadMessenger which is used to send ControlMessages to the + /// underlying thread. + /// + /// # Arguments + /// + /// `context` - The PeerConnectionContext which is owned by the underlying thread + pub fn start(&mut self, context: PeerConnectionContext) -> Result>> { + self.direction = Some(context.direction.clone()); + self.peer_address = Some(context.peer_address.clone()); + + let worker = PeerConnectionWorker::new(context, self.state.clone(), self.connection_stats.clone()); + let handle = worker.spawn()?; + Ok(handle) + } + + /// Tell the underlying thread to shut down. The connection will not immediately + /// be in a `Shutdown` state. [wait_shutdown] can be used to wait for the + /// connection to shut down. If the connection is not active, this method does nothing. + pub fn shutdown(&self) -> Result<()> { + match self.send_control_message(ControlMessage::Shutdown) { + // StateError only returns from send_control_message + // if the connection worker is not active + Ok(_) | Err(ConnectionError::PeerError(PeerConnectionError::StateError(_))) => Ok(()), + e => e, + } + } + + /// Send frames to the connected Peer. An Err will be returned if the + /// connection is not in a Connected state. + /// + /// # Arguments + /// + /// `frames` - The frames to send + pub fn send(&self, frames: FrameSet) -> Result<()> { + self.send_control_message(ControlMessage::SendMsg(frames)) + } + + /// Set the linger for the connection + /// + /// # Arguments + /// + /// `linger` - The Linger to set + pub fn set_linger(&self, linger: Linger) -> Result<()> { + self.send_control_message(ControlMessage::SetLinger(linger)) + } + + /// Temporarily suspend messages from being processed and forwarded to the consumer. + /// Pending messages will be buffered until reaching the receive HWM. Once resumed, + /// buffered messages will be released to the consumer. + /// An Err will be returned if the connection is not in a Connected state. + pub fn pause(&self) -> Result<()> { + self.send_control_message(ControlMessage::Pause) + } + + /// Unpause the connection and resume message processing from the peer. + /// An Err will be returned if the connection is not in a Connected state. + pub fn resume(&self) -> Result<()> { + self.send_control_message(ControlMessage::Resume) + } + + /// Return the actual address this connection is bound to. If the connection is not over a TCP socket, or the + /// connection state is not Connected, this function returns None + pub fn get_connected_address(&self) -> Option { + let lock = acquire_read_lock!(self.state); + match &*lock { + PeerConnectionState::Listening(info) | PeerConnectionState::Connected(info) => { + info.connected_address.clone() + }, + _ => None, + } + } + + /// Return the actual address this connection is bound to. If the connection state is not Connected, + /// this function returns None + pub fn get_address(&self) -> Option { + let lock = acquire_read_lock!(self.state); + match &*lock { + PeerConnectionState::Listening(info) | PeerConnectionState::Connected(info) => info + .connected_address + .clone() + .map_or(self.peer_address.clone(), |addr| Some(addr.into())), + _ => None, + } + } + + /// Returns a snapshot of latest connection stats from this peer connection + pub fn connection_stats(&self) -> PeerConnectionStats { + acquire_read_lock!(self.connection_stats).clone() + } + + /// Returns the last time this connection sent or received a message + pub fn last_activity(&self) -> NaiveDateTime { + *acquire_read_lock!(self.connection_stats).last_activity() + } + + /// Send control message to the ThreadControlMessenger. + /// Will return an error if the connection is not in an active state. + /// + /// # Arguments + /// + /// `msg` - The ControlMessage to send + fn send_control_message(&self, msg: ControlMessage) -> Result<()> { + use PeerConnectionState::*; + let lock = acquire_read_lock!(self.state); + match &*lock { + Connecting(ref thread_ctl) => thread_ctl.send(msg), + Listening(ref info) => info.control_messenger.send(msg), + Connected(ref info) => info.control_messenger.send(msg), + state => Err(PeerConnectionError::StateError(format!( + "Attempt to retrieve thread messenger on peer connection with state '{}'", + PeerConnectionSimpleState::from(state) + )) + .into()), + } + } + + /// Blocks the current thread until the connection is in a `Connected` state (returning `Ok`), + /// the timeout has been reached (returning `Err(ConnectionError::Timeout)`), or the connection + /// is in a `Failed` state (returning the error which caused the failure) + pub fn wait_listening_or_failure(&self, until: &Duration) -> Result<()> { + match self.get_direction() { + Some(direction) => { + if *direction == Direction::Outbound { + return Err(ConnectionError::InvalidOperation( + "Call to wait_listening_or_failure on Outbound connection".to_string(), + )); + } + }, + None => { + return Err(ConnectionError::InvalidOperation( + "Call to wait_listening_or_failure before peer connection has started".to_string(), + )); + }, + } + self.wait_until(until, || !self.is_active() || self.is_listening())?; + if self.is_listening() { + Ok(()) + } else { + match self.failure() { + Some(err) => Err(err), + None => Err(ConnectionError::Timeout), + } + } + } + + /// Blocks the current thread until the connection is in a `Connected` state (returning `Ok`), + /// the timeout has been reached (returning `Err(ConnectionError::Timeout)`), or the connection + /// is in a `Failed` state (returning the error which caused the failure) + pub fn wait_connected_or_failure(&self, until: &Duration) -> Result<()> { + self.wait_until(until, || !self.is_active() || self.is_connected())?; + if self.is_connected() { + Ok(()) + } else { + match self.failure() { + Some(err) => Err(err), + None => Err(ConnectionError::Timeout), + } + } + } + + /// Blocks the current thread until the connection is in a `Shutdown` or `Disconnected` state (Ok) or + /// the timeout is reached (Err). + pub fn wait_disconnected(&self, until: &Duration) -> Result<()> { + self.wait_until(until, || self.is_disconnected()) + } + + /// If the connection is in a `Failed` state, the failure error is returned, otherwise `None` + pub fn failure(&self) -> Option { + let lock = acquire_read_lock!(self.state); + match &*lock { + PeerConnectionState::Failed(err) => Some(err.clone().into()), + _ => None, + } + } + + /// Returns the connection state without the ThreadControlMessenger + /// which should never be leaked. + pub fn get_state(&self) -> PeerConnectionSimpleState { + let lock = acquire_read_lock!(self.state); + PeerConnectionSimpleState::from(&*lock) + } + + /// Gets the direction for this peer connection + pub fn get_direction(&self) -> &Option { + &self.direction + } + + /// Waits until the condition returns true or the timeout (`until`) is reached. + /// If the timeout was reached, an `Err(ConnectionError::Timeout)` is returned, otherwise `Ok(())` + fn wait_until(&self, until: &Duration, condition: impl Fn() -> bool) -> Result<()> { + let mut count = 0; + let timeout_ms = until.as_millis(); + while !condition() && count < timeout_ms { + thread::sleep(Duration::from_millis(1)); + count += 1; + } + + if count < timeout_ms { + Ok(()) + } else { + Err(ConnectionError::Timeout) + } + } + + #[cfg(test)] + pub fn new_with_connecting_state_for_test() -> (Self, std::sync::mpsc::Receiver) { + use std::sync::mpsc::sync_channel; + let (tx, rx) = sync_channel(1); + ( + Self { + state: Arc::new(RwLock::new(PeerConnectionState::Connecting(Arc::new(tx.into())))), + ..Default::default() + }, + rx, + ) + } +} + +/// Represents the states that a peer connection can be in without +/// exposing ThreadControlMessenger which should not be leaked. +#[derive(Debug)] +pub enum PeerConnectionSimpleState { + /// The connection object has been created but is not connected + Initial, + /// The connection thread is running, but the connection has not been accepted + Connecting, + /// The connection is listening, and has been not been accepted. + Listening(Option), + /// The connection is connected, and has been accepted. + Connected(Option), + /// The connection has been shut down (node disconnected) + Shutdown, + /// The remote peer has disconnected + Disconnected, + /// Peer connection failed + Failed(PeerConnectionError), +} + +impl From<&PeerConnectionState> for PeerConnectionSimpleState { + fn from(state: &PeerConnectionState) -> Self { + match state { + PeerConnectionState::Initial => PeerConnectionSimpleState::Initial, + PeerConnectionState::Connecting(_) => PeerConnectionSimpleState::Connecting, + PeerConnectionState::Listening(info) => { + PeerConnectionSimpleState::Listening(info.connected_address.clone()) + }, + PeerConnectionState::Connected(info) => { + PeerConnectionSimpleState::Connected(info.connected_address.clone()) + }, + PeerConnectionState::Shutdown => PeerConnectionSimpleState::Shutdown, + PeerConnectionState::Disconnected => PeerConnectionSimpleState::Disconnected, + PeerConnectionState::Failed(e) => PeerConnectionSimpleState::Failed(e.clone()), + } + } +} + +impl fmt::Display for PeerConnectionSimpleState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use PeerConnectionSimpleState::*; + match *self { + Initial => write!(f, "Initial"), + Connecting => write!(f, "Connecting"), + Listening(Some(ref addr)) => write!(f, "Listening on {}", addr), + Listening(None) => write!(f, "Listening on non TCP socket"), + Connected(Some(ref addr)) => write!(f, "Connected to {}", addr), + Connected(None) => write!(f, "Connected to non TCP socket"), + Shutdown => write!(f, "Shutdown"), + Disconnected => write!(f, "Disconnected"), + Failed(ref event) => write!(f, "Failed({})", event), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::sync::{ + mpsc::{sync_channel, Receiver}, + Arc, + }; + + fn create_thread_ctl() -> (Arc, Receiver) { + let (tx, rx) = sync_channel::(1); + (Arc::new(tx.into()), rx) + } + + #[test] + fn state_display() { + let addr = "127.0.0.1:8000".parse().ok(); + assert_eq!("Initial", format!("{}", PeerConnectionSimpleState::Initial)); + assert_eq!("Connecting", format!("{}", PeerConnectionSimpleState::Connecting)); + assert_eq!( + "Connected to non TCP socket", + format!("{}", PeerConnectionSimpleState::Connected(None)) + ); + assert_eq!( + "Connected to 127.0.0.1:8000", + format!("{}", PeerConnectionSimpleState::Connected(addr)) + ); + assert_eq!("Shutdown", format!("{}", PeerConnectionSimpleState::Shutdown)); + assert_eq!( + format!("Failed({})", PeerConnectionError::ConnectFailed), + format!( + "{}", + PeerConnectionSimpleState::Failed(PeerConnectionError::ConnectFailed) + ) + ); + } + + #[test] + fn new() { + let conn = PeerConnection::new(); + assert!(!conn.is_connected()); + assert!(!conn.is_listening()); + assert!(!conn.is_disconnected()); + assert!(!conn.is_active()); + assert!(!conn.is_shutdown()); + assert!(!conn.is_failed()); + } + + #[test] + fn state_connected() { + let (thread_ctl, _) = create_thread_ctl(); + + let info = ConnectionInfo { + control_messenger: thread_ctl, + connected_address: Some("127.0.0.1:1000".parse().unwrap()), + }; + let conn = PeerConnection { + state: Arc::new(RwLock::new(PeerConnectionState::Connected(Arc::new(info)))), + connection_stats: Arc::new(RwLock::new(PeerConnectionStats::new())), + direction: None, + peer_address: None, + }; + + assert!(conn.is_connected()); + assert!(!conn.is_listening()); + assert!(!conn.is_disconnected()); + assert!(conn.is_active()); + assert!(!conn.is_shutdown()); + assert!(!conn.is_failed()); + } + + #[test] + fn state_listening() { + let (thread_ctl, _) = create_thread_ctl(); + + let info = ConnectionInfo { + control_messenger: thread_ctl, + connected_address: Some("127.0.0.1:1000".parse().unwrap()), + }; + let conn = PeerConnection { + state: Arc::new(RwLock::new(PeerConnectionState::Listening(Arc::new(info)))), + connection_stats: Arc::new(RwLock::new(PeerConnectionStats::new())), + direction: None, + peer_address: None, + }; + + assert!(!conn.is_connected()); + assert!(conn.is_listening()); + assert!(!conn.is_disconnected()); + assert!(conn.is_active()); + assert!(!conn.is_shutdown()); + assert!(!conn.is_failed()); + } + + #[test] + fn state_connecting() { + let (thread_ctl, _) = create_thread_ctl(); + + let conn = PeerConnection { + state: Arc::new(RwLock::new(PeerConnectionState::Connecting(thread_ctl))), + connection_stats: Arc::new(RwLock::new(PeerConnectionStats::new())), + direction: None, + peer_address: None, + }; + + assert!(!conn.is_connected()); + assert!(!conn.is_listening()); + assert!(!conn.is_disconnected()); + assert!(conn.is_active()); + assert!(!conn.is_shutdown()); + assert!(!conn.is_failed()); + } + + #[test] + fn state_active() { + let (thread_ctl, _) = create_thread_ctl(); + + let conn = PeerConnection { + state: Arc::new(RwLock::new(PeerConnectionState::Connecting(thread_ctl))), + connection_stats: Arc::new(RwLock::new(PeerConnectionStats::new())), + direction: None, + peer_address: None, + }; + + assert!(!conn.is_connected()); + assert!(!conn.is_listening()); + assert!(!conn.is_disconnected()); + assert!(conn.is_active()); + assert!(!conn.is_shutdown()); + assert!(!conn.is_failed()); + } + + #[test] + fn state_failed() { + let conn = PeerConnection { + state: Arc::new(RwLock::new(PeerConnectionState::Failed( + PeerConnectionError::ConnectFailed, + ))), + connection_stats: Arc::new(RwLock::new(PeerConnectionStats::new())), + direction: None, + peer_address: None, + }; + + assert!(!conn.is_connected()); + assert!(!conn.is_listening()); + assert!(conn.is_disconnected()); + assert!(!conn.is_active()); + assert!(!conn.is_shutdown()); + assert!(conn.is_failed()); + } + + #[test] + fn state_disconnected() { + let conn = PeerConnection { + state: Arc::new(RwLock::new(PeerConnectionState::Disconnected)), + connection_stats: Arc::new(RwLock::new(PeerConnectionStats::new())), + direction: None, + peer_address: None, + }; + + assert!(!conn.is_connected()); + assert!(!conn.is_listening()); + assert!(conn.is_disconnected()); + assert!(!conn.is_active()); + assert!(!conn.is_shutdown()); + assert!(!conn.is_failed()); + } + + #[test] + fn state_shutdown() { + let conn = PeerConnection { + state: Arc::new(RwLock::new(PeerConnectionState::Shutdown)), + connection_stats: Arc::new(RwLock::new(PeerConnectionStats::new())), + direction: None, + peer_address: None, + }; + + assert!(!conn.is_connected()); + assert!(!conn.is_listening()); + assert!(conn.is_disconnected()); + assert!(!conn.is_active()); + assert!(conn.is_shutdown()); + assert!(!conn.is_failed()); + } + + fn create_connected_peer_connection() -> (PeerConnection, Receiver) { + let (thread_ctl, rx) = create_thread_ctl(); + let info = ConnectionInfo { + control_messenger: thread_ctl, + connected_address: Some("127.0.0.1:1000".parse().unwrap()), + }; + let conn = PeerConnection { + state: Arc::new(RwLock::new(PeerConnectionState::Connected(Arc::new(info)))), + connection_stats: Arc::new(RwLock::new(PeerConnectionStats::new())), + direction: None, + peer_address: None, + }; + (conn, rx) + } + + #[test] + fn send() { + let (conn, rx) = create_connected_peer_connection(); + + let sample_frames = vec![vec![123u8]]; + conn.send(sample_frames.clone()).unwrap(); + let msg = rx.recv_timeout(Duration::from_millis(10)).unwrap(); + match msg { + ControlMessage::SendMsg(frames) => { + assert_eq!(sample_frames, frames); + }, + m => panic!("Unexpected control message '{}'", m), + } + } + + #[test] + fn pause() { + let (conn, rx) = create_connected_peer_connection(); + + conn.pause().unwrap(); + let msg = rx.recv_timeout(Duration::from_millis(10)).unwrap(); + assert_eq!(ControlMessage::Pause, msg); + } + + #[test] + fn resume() { + let (conn, rx) = create_connected_peer_connection(); + + conn.resume().unwrap(); + let msg = rx.recv_timeout(Duration::from_millis(10)).unwrap(); + assert_eq!(ControlMessage::Resume, msg); + } + + #[test] + fn shutdown() { + let (conn, rx) = create_connected_peer_connection(); + + conn.shutdown().unwrap(); + let msg = rx.recv_timeout(Duration::from_millis(10)).unwrap(); + assert_eq!(ControlMessage::Shutdown, msg); + } + + #[test] + fn connection_stats() { + let (conn, _) = create_connected_peer_connection(); + + let stats = conn.connection_stats(); + assert_eq!(stats.messages_recv, 0); + assert_eq!(stats.messages_sent, 0); + } +} diff --git a/comms/src/connection/peer_connection/context.rs b/comms/src/connection/peer_connection/context.rs new file mode 100644 index 0000000000..5f6b443438 --- /dev/null +++ b/comms/src/connection/peer_connection/context.rs @@ -0,0 +1,327 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::convert::{TryFrom, TryInto}; + +use super::{ConnectionId, PeerConnectionError}; + +use crate::connection::{ + net_address::ip::SocketAddress, + types::{Direction, Linger, Result}, + zmq::{CurveEncryption, InprocAddress, ZmqContext}, + ConnectionError, + NetAddress, +}; + +/// The default maximum message size which will be used if no maximum message size is set. +const DEFAULT_MAX_MSG_SIZE: u64 = 500 * 1024; // 500 kb +/// The default maximum number of retries before failing the connection. +const DEFAULT_MAX_RETRY_ATTEMPTS: u16 = 10; + +/// Context for connecting to a Peer. This is handed to a PeerConnection and is used to establish the connection. +/// +/// # Fields +/// +/// `context` - the underlying connection context +/// `peer_address` - the address to listen (Direction::Inbound) or connect(Direction::Outbound) +/// `message_sink_address` - the address to forward all received messages +/// `direction` - the connection direction (Inbound or Outbound) +/// `curve_encryption` - the [CurveEncryption] for the connection +/// `max_msg_size` - the maximum size of a incoming message +/// `socks_address` - optional address for a SOCKS proxy +/// +/// [CurveEncryption]: ./../zmq/CurveEncryption/struct.CurveEncryption.html +pub struct PeerConnectionContext { + pub(crate) context: ZmqContext, + pub(crate) peer_address: NetAddress, + pub(crate) message_sink_address: InprocAddress, + pub(crate) direction: Direction, + pub(crate) id: ConnectionId, + pub(crate) curve_encryption: CurveEncryption, + pub(crate) max_msg_size: u64, + pub(crate) max_retry_attempts: u16, + pub(crate) socks_address: Option, + pub(crate) linger: Linger, +} + +impl<'a> TryFrom> for PeerConnectionContext { + type Error = ConnectionError; + + /// Convert a PeerConnectionContextBuilder to a PeerConnectionContext + fn try_from(builder: PeerConnectionContextBuilder<'a>) -> Result { + builder.check_curve_encryption()?; + + let message_sink_address = unwrap_prop(builder.message_sink_address, "message_sink_address")?; + let context = unwrap_prop(builder.context, "context")?.clone(); + let curve_encryption = builder.curve_encryption; + let direction = unwrap_prop(builder.direction, "direction")?; + let id = unwrap_prop(builder.id, "id")?; + let max_msg_size = builder.max_msg_size.unwrap_or(DEFAULT_MAX_MSG_SIZE); + let max_retry_attempts = builder.max_retry_attempts.unwrap_or(DEFAULT_MAX_RETRY_ATTEMPTS); + let peer_address = unwrap_prop(builder.address, "peer_address")?; + let socks_address = builder.socks_address; + let linger = builder.linger.or(Some(Linger::Timeout(100))).unwrap(); + + Ok(PeerConnectionContext { + message_sink_address, + context, + curve_encryption, + direction, + id, + max_msg_size, + max_retry_attempts, + peer_address, + socks_address, + linger, + }) + } +} + +/// Local utility function to unwrap a builder property, or return a PeerConnectionError::InitializationError +#[inline(always)] +fn unwrap_prop(prop: Option, prop_name: &str) -> Result { + match prop { + Some(t) => Ok(t), + None => Err(ConnectionError::PeerError(PeerConnectionError::InitializationError( + format!("Missing required connection property '{}'", prop_name), + ))), + } +} + +/// Used to build a context for a PeerConnection. Fields +/// are the same as a [PeerConnectionContext]. +/// +/// # Example +/// +/// ```edition2018 +/// # use tari_comms::connection::{ +/// # ZmqContext, +/// # InprocAddress, +/// # Direction, +/// # PeerConnectionContextBuilder, +/// # PeerConnection, +/// # }; +/// +/// let ctx = ZmqContext::new(); +/// +/// let peer_context = PeerConnectionContextBuilder::new() +/// .set_id("123") +/// .set_context(&ctx) +/// .set_direction(Direction::Outbound) +/// .set_message_sink_address(InprocAddress::random()) +/// .set_address("127.0.0.1:8080".parse().unwrap()) +/// .build() +/// .unwrap(); +/// ``` +/// +/// [PeerConnectionContext]: ./struct.PeerConnectionContext.html +#[derive(Default)] +pub struct PeerConnectionContextBuilder<'c> { + pub(super) address: Option, + pub(super) message_sink_address: Option, + pub(super) context: Option<&'c ZmqContext>, + pub(super) curve_encryption: CurveEncryption, + pub(super) direction: Option, + pub(super) id: Option, + pub(super) max_msg_size: Option, + pub(super) max_retry_attempts: Option, + pub(super) socks_address: Option, + pub(super) linger: Option, +} + +impl<'c> PeerConnectionContextBuilder<'c> { + /// Set the peer address + setter!(set_address, address, Option); + + /// Set the address where incoming peer messages are forwarded + setter!(set_message_sink_address, message_sink_address, Option); + + /// Set the zmq context + setter!(set_context, context, Option<&'c ZmqContext>); + + /// Set the connection direction + setter!(set_direction, direction, Option); + + /// Set the maximum connection retry attempts + setter!(set_max_retry_attempts, max_retry_attempts, Option); + + /// Set the maximum message size in bytes + setter!(set_max_msg_size, max_msg_size, Option); + + /// Set the socks proxy address + setter!(set_socks_proxy, socks_address, Option); + + /// Set the [Linger] for this connection + setter!(set_linger, linger, Option); + + /// Return a new PeerConnectionContextBuilder + pub fn new() -> Self { + Default::default() + } + + /// Set CurveEncryption. Defaults to the default of CurveEncryption. + pub fn set_curve_encryption(mut self, enc: CurveEncryption) -> Self { + self.curve_encryption = enc; + self + } + + /// Set the ID for the connection. This will be sent as the first + /// frame to the message sink address + pub fn set_id(mut self, id: T) -> Self + where T: Into { + self.id = Some(id.into()); + self + } + + /// Build the PeerConnectionContext. + /// + /// Will return an Err if any of the required fields are not set or if + /// curve encryption is not set correctly for the connection direction. + /// i.e CurveEncryption::Server must be set with Direction::Inbound and + /// CurveEncryption::Client must be set with Direction::Outbound. + /// CurveEncryption::None will succeed in either direction. + pub fn build(self) -> Result { + self.try_into() + } + + fn check_curve_encryption(&self) -> Result<()> { + match self.direction { + Some(ref direction) => match direction { + Direction::Outbound => match self.curve_encryption { + CurveEncryption::None { .. } => Ok(()), + CurveEncryption::Client { .. } => Ok(()), + CurveEncryption::Server { .. } => Err(PeerConnectionError::InitializationError( + "'Client' curve encryption required for outbound connection".to_string(), + ) + .into()), + }, + Direction::Inbound => match self.curve_encryption { + CurveEncryption::None { .. } => Ok(()), + CurveEncryption::Client { .. } => Err(PeerConnectionError::InitializationError( + "'Server' curve encryption required for inbound connection".to_string(), + ) + .into()), + CurveEncryption::Server { .. } => Ok(()), + }, + }, + + None => Err( + PeerConnectionError::InitializationError("Direction not set for peer connection".to_string()).into(), + ), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::connection::{ + peer_connection::PeerConnectionError, + types::{Direction, Result}, + zmq::{CurveEncryption, InprocAddress, ZmqContext}, + ConnectionError, + NetAddress, + }; + + fn assert_initialization_error(result: Result, expected: &str) { + if let Err(error) = result { + match error { + ConnectionError::PeerError(err) => match err { + PeerConnectionError::InitializationError(s) => { + assert_eq!(expected, s); + }, + _ => panic!("Unexpected PeerConnectionError {:?}", err), + }, + _ => panic!("Unexpected ConnectionError {:?}", error), + } + } else { + panic!("Unexpected success when building invalid PeerConnectionContext"); + } + } + + #[test] + fn valid_build() { + let ctx = ZmqContext::new(); + + let recv_addr = InprocAddress::random(); + let peer_addr = "127.0.0.1:80".parse::().unwrap(); + let conn_id = "123".as_bytes(); + let socks_addr = "127.0.0.1:8080".parse::().unwrap(); + + let peer_ctx = PeerConnectionContextBuilder::new() + .set_id(conn_id.clone()) + .set_direction(Direction::Inbound) + .set_context(&ctx) + .set_socks_proxy(socks_addr.clone()) + .set_message_sink_address(recv_addr.clone()) + .set_address(peer_addr.clone()) + .build() + .unwrap(); + + assert_eq!(conn_id.to_vec(), peer_ctx.id); + assert_eq!(recv_addr, peer_ctx.message_sink_address); + assert_eq!(Direction::Inbound, peer_ctx.direction); + assert_eq!(peer_addr, peer_ctx.peer_address); + assert_eq!(Some(socks_addr), peer_ctx.socks_address); + } + + #[test] + fn invalid_build() { + let (sk, pk) = CurveEncryption::generate_keypair().unwrap(); + let ctx = ZmqContext::new(); + + let result = PeerConnectionContextBuilder::new() + .set_id("123") + .set_direction(Direction::Outbound) + .set_message_sink_address(InprocAddress::random()) + .set_address("127.0.0.1:80".parse().unwrap()) + .build(); + + assert_initialization_error(result, "Missing required connection property 'context'"); + + let result = PeerConnectionContextBuilder::new() + .set_id("123") + .set_direction(Direction::Inbound) + .set_context(&ctx) + .set_message_sink_address(InprocAddress::random()) + .set_curve_encryption(CurveEncryption::Client { + secret_key: sk.clone(), + public_key: pk.clone(), + server_public_key: pk.clone(), + }) + .set_address("127.0.0.1:80".parse().unwrap()) + .build(); + + assert_initialization_error(result, "'Server' curve encryption required for inbound connection"); + + let result = PeerConnectionContextBuilder::new() + .set_id("123") + .set_direction(Direction::Outbound) + .set_context(&ctx) + .set_message_sink_address(InprocAddress::random()) + .set_curve_encryption(CurveEncryption::Server { secret_key: sk.clone() }) + .set_address("127.0.0.1:80".parse().unwrap()) + .build(); + + assert_initialization_error(result, "'Client' curve encryption required for outbound connection"); + } +} diff --git a/comms/src/connection/peer_connection/control.rs b/comms/src/connection/peer_connection/control.rs new file mode 100644 index 0000000000..0df8857b49 --- /dev/null +++ b/comms/src/connection/peer_connection/control.rs @@ -0,0 +1,124 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use log::*; + +use std::{convert::From, fmt, sync::mpsc::SyncSender}; + +use crate::{ + connection::types::{Linger, Result}, + message::FrameSet, +}; + +use super::PeerConnectionError; + +const LOG_TARGET: &str = "comms::connections::peer_connection::control"; + +/// Control messages which are sent by PeerConnection to the underlying thread. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ControlMessage { + /// Shut the thread down + Shutdown, + /// Send the given frames to the peer + SendMsg(FrameSet), + /// Temporarily pause receiving messages from this connection + Pause, + /// Resume receiving messages from peer + Resume, + /// Sets the linger on the peer connection + SetLinger(Linger), +} + +impl fmt::Display for ControlMessage { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", *self) + } +} + +/// Send and join handles to the worker thread for a PeerConnection +/// This can be converted from a SyncSender +#[derive(Clone)] +pub(super) struct ThreadControlMessenger(SyncSender); + +impl ThreadControlMessenger { + /// Send a [ControlMessage] to the listening thread. + /// + /// # Arguments + /// `msg` - The [ControlMessage] to send + /// + /// [ControlMessage]: ./enum.ControlMessage.html + pub fn send(&self, msg: ControlMessage) -> Result<()> { + self.0.send(msg).map_err(|e| { + PeerConnectionError::ControlSendError(format!("Failed to send control message: {:?}", e)).into() + }) + } + + pub fn get_sender(&self) -> &SyncSender { + &self.0 + } +} + +impl From> for ThreadControlMessenger { + /// Convert a SyncSender to a ThreadControlMessenger + fn from(sender: SyncSender) -> Self { + Self(sender) + } +} + +impl Drop for ThreadControlMessenger { + /// Send a ControlMessage::Shutdown on drop. + fn drop(&mut self) { + debug!(target: LOG_TARGET, "ThreadControlMessenger dropped"); + // We assume here that the thread responds to the shutdown request. + let _ = self.0.try_send(ControlMessage::Shutdown); + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::{sync::mpsc::sync_channel, thread, time::Duration}; + use tari_utilities::thread_join::ThreadJoinWithTimeout; + + #[test] + fn send_control_message() { + let (tx, rx) = sync_channel::(1); + + let handle = thread::spawn(move || { + let msg = rx + .recv_timeout(Duration::from_millis(100)) + .map_err(|e| format!("{:?}", e))?; + match msg { + ControlMessage::Shutdown => Ok(()), + x => Err(format!("Received unexpected message {}", x)), + } + }); + + let messenger: ThreadControlMessenger = tx.into(); + messenger.send(ControlMessage::Shutdown).unwrap(); + + handle + .timeout_join(Duration::from_millis(3000)) + .map_err(|e| format!("Test thread errored: {:?}", e)) + .unwrap(); + } +} diff --git a/comms/src/connection/peer_connection/error.rs b/comms/src/connection/peer_connection/error.rs new file mode 100644 index 0000000000..da2240b28e --- /dev/null +++ b/comms/src/connection/peer_connection/error.rs @@ -0,0 +1,50 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; + +/// Represents errors which can occur in a PeerConnection. +#[derive(Debug, Error, Clone, PartialEq)] +pub enum PeerConnectionError { + #[error(msg_embedded, non_std, no_from)] + InitializationError(String), + #[error(msg_embedded, non_std, no_from)] + ControlSendError(String), + /// Peer connection control port has disconnected + ControlPortDisconnected, + /// Unexpected identity received from peer + UnexpectedIdentity, + /// Connection identity of peer has not been established + IdentityNotEstablished, + #[error(msg_embedded, non_std, no_from)] + StateError(String), + /// Error occurred while shutting down the connection + ShutdownError, + /// Failed to establish a connection + ConnectFailed, + #[error(msg_embedded, non_std, no_from)] + UnexpectedConnectionError(String), + /// Connection attempts exceeded max retries + ExceededMaxConnectRetryCount, + /// Peer connection worker thread failed to start + ThreadInitializationError, +} diff --git a/comms/src/connection/peer_connection/mod.rs b/comms/src/connection/peer_connection/mod.rs new file mode 100644 index 0000000000..4c8dab0b7d --- /dev/null +++ b/comms/src/connection/peer_connection/mod.rs @@ -0,0 +1,95 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// # peer_connection +/// +/// A peer connection is a bi-directional connection to a given [NetAddress]. The [Direction] of +/// a [PeerConnection] relates to which side initiate the connection. i.e for Inbound, this node +/// initiated the connection and waits for the peer to connect. For Outbound, this node is connecting +/// out to a listening socket. Frames can be sent and received over a single [Connection]. All received +/// messages are forwarded to a consumer connection. +/// +/// The [PeerConnection] object starts a [Worker] which is responsible for establishing the required +/// connections. Two [Connections] are needed: a connection to/from the given [NetAddress] and a +/// connection to the consumer. A [ConnectionMonitor] is started which receives socket events from the +/// peer connection. +/// +/// A [PeerConnection] consists of these modules: +/// +/// 1. `connection` - responsible for starting and sending control messages to the PeerConnection +/// worker thread. +/// 2. `context` - Builder for a `PeerConnectionContext` which is owned by a PeerConnection worker +/// thread. This provides all the information required to create the underlying +/// connections to the peer and consumer. +/// 3. `control` - Contains the control messages which can be sent from the [PeerConnection] to +/// the [Worker], as well as a thin wrapper around [std::sync::mpsc::Sender]. +/// 4. `error` - Contains [PeerConnectionError] +/// 5. `worker` - Where all the work is done. Contains the code responsible for establishing +/// connections (peer and consumer), receiving messages to forward to the +/// consumer connection, updating the peer connection state from socket events +/// and receiving control messages and acting on them. +/// +/// ## PeerConnectionState +/// +/// +--------------------+ +/// | | +/// | Initial | +/// | | +/// +--------------------+ +/// | +/// | +------------------+ +/// +--------------------+ | | +/// | | +---------| Shutdown | +/// | Connecting | | | | +/// | |- | +------------------+ +/// +---------|----------+ \ +-----+ +------------------+ +/// | | | | | +/// | | |-------+ Disconnected | +/// Accepted / Connected | | | | +/// | /+-----+ +------------------+ +/// | / | +------------------+ +/// | / | | | +/// +--------------------- | | Failed | +/// | | +---------- | +/// | Connected | +------------------+ +/// | | +/// +--------------------+ +/// +/// [PeerConnection](./connection/struct.PeerConnection.html] +/// [Direction](../types/enum.Direction.html] +/// [NetAddress](../net_address/enum.NetAddress.html] +/// [Connection](../connection/struct.Connection.html] +/// [Worker](./worker/struct.Worker.html] +/// [ConnectionMonitor](../monitor/struct.ConnectionMonitor.html] +/// [PeerConnectionError](./error/struct.PeerConnectionError.html] +mod connection; +mod context; +mod control; +mod error; +mod worker; + +pub use self::{ + connection::{ConnectionId, PeerConnection, PeerConnectionProtoMessage, PeerConnectionSimpleState}, + context::{PeerConnectionContext, PeerConnectionContextBuilder}, + control::ControlMessage, + error::PeerConnectionError, +}; diff --git a/comms/src/connection/peer_connection/worker.rs b/comms/src/connection/peer_connection/worker.rs new file mode 100644 index 0000000000..86e152d7ef --- /dev/null +++ b/comms/src/connection/peer_connection/worker.rs @@ -0,0 +1,722 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{ + connection::{ + ConnectionInfo, + PeerConnectionProtoMessage, + PeerConnectionSimpleState, + PeerConnectionState, + PeerConnectionStats, + }, + control::ControlMessage, + PeerConnectionContext, + PeerConnectionError, +}; +use crate::{ + connection::{ + connection::{Connection, EstablishedConnection}, + monitor::{ConnectionMonitor, SocketEvent, SocketEventType}, + types::{Direction, Linger, Result}, + ConnectionError, + InprocAddress, + NetAddress, + }, + message::{Frame, FrameSet}, +}; +use log::*; +use std::{ + sync::{ + mpsc::{sync_channel, Receiver, RecvTimeoutError, SyncSender}, + Arc, + RwLock, + }, + thread::{self, JoinHandle}, + time::Duration, +}; +use tari_utilities::message_format::MessageFormat; + +const LOG_TARGET: &str = "comms::connection::peer_connection::worker"; + +/// Send HWM for peer connections +const PEER_CONNECTION_SEND_HWM: i32 = 10; +/// Receive HWM for peer connections +const PEER_CONNECTION_RECV_HWM: i32 = 10; + +/// Set the allocated stack size for each PeerConnectionWorker thread +const THREAD_STACK_SIZE: usize = 64 * 1024; // 64kb + +/// Worker which: +/// - Establishes a connection to peer +/// - Establishes a connection to the message consumer +/// - Receives and handles ControlMessages +/// - Forwards frames to consumer +/// - Handles SocketEvents and updates shared connection state +pub(super) struct PeerConnectionWorker { + context: PeerConnectionContext, + sender: SyncSender, + receiver: Receiver, + identity: Option, + paused: bool, + monitor_addr: InprocAddress, + connection_state: Arc>, + connection_stats: Arc>, + retry_count: u16, +} + +impl PeerConnectionWorker { + /// Create a new Worker from the given context + pub fn new( + context: PeerConnectionContext, + connection_state: Arc>, + connection_stats: Arc>, + ) -> Self + { + let (sender, receiver) = sync_channel(10); + Self { + context, + sender, + receiver, + identity: None, + paused: false, + monitor_addr: InprocAddress::random(), + connection_state, + connection_stats, + retry_count: 0, + } + } + + /// Spawn a worker thread + pub fn spawn(mut self) -> Result>> { + { + // Set connecting state + let mut state_lock = acquire_write_lock!(self.connection_state); + *state_lock = PeerConnectionState::Connecting(Arc::new(self.sender.clone().into())); + } + + let handle = thread::Builder::new() + .name(format!("peer-conn-{}-thread", &self.context.id.to_short_id())) + .stack_size(THREAD_STACK_SIZE) + .spawn(move || -> Result<()> { + let result = self.run(); + + // Main loop exited, let's set the shared connection state. + self.handle_run_result(result)?; + + Ok(()) + }) + .map_err(|_| PeerConnectionError::ThreadInitializationError)?; + + Ok(handle) + } + + /// Handle the result for the worker loop and update connection state if necessary + fn handle_run_result(&mut self, result: Result<()>) -> Result<()> { + let mut lock = acquire_write_lock!(self.connection_state); + match result { + Ok(_) => { + info!( + target: LOG_TARGET, + "[{}] Peer connection shut down cleanly", self.context.peer_address + ); + // The loop exited cleanly. + match *lock { + // The connection is still in a connected state, transition to Shutdown + PeerConnectionState::Connected(_) | PeerConnectionState::Connecting(_) => { + *lock = PeerConnectionState::Shutdown; + }, + // Connection is in some other state, the loop exited without error + // so we won't change the state to preserve failed or disconnected states. + _ => {}, + } + }, + Err(err) => { + error!( + target: LOG_TARGET, + "[{}] Peer connection exited with an error: {:?}", self.context.peer_address, err + ); + // Loop failed, update the connection state to reflect that + *lock = match err { + ConnectionError::PeerError(err) => PeerConnectionState::Failed(err), + e => PeerConnectionState::Failed(PeerConnectionError::UnexpectedConnectionError(format!("{}", e))), + }; + }, + } + + Ok(()) + } + + /// The main loop for the worker. This is where the work is done. + /// The required connections are set up and messages processed. + fn run(&mut self) -> Result<()> { + let monitor = self.connect_monitor()?; + let peer_conn = self.establish_peer_connection()?; + let consumer = self.establish_sink_connection()?; + let addr = peer_conn.get_connected_address(); + + if let Some(a) = addr { + debug!(target: LOG_TARGET, "Starting peer connection worker thread on {}", a); + self.context.peer_address = a.clone().into(); + } + + if self.context.direction == Direction::Outbound { + debug!(target: LOG_TARGET, "Sending Identify to remote connection"); + self.identify(&peer_conn)?; + } + + loop { + if let Some(msg) = self.receive_control_msg()? { + match msg { + ControlMessage::Shutdown => { + debug!(target: LOG_TARGET, "[{:?}] Shutdown message received", addr); + peer_conn.set_linger(Linger::Never)?; + // Ensure that the peer connection is dropped as soon as possible. + // This somehow seemed to improve connection reliability. + drop(peer_conn); + break Ok(()); + }, + ControlMessage::SendMsg(frames) => { + debug!( + target: LOG_TARGET, + "[{:?}] SendMsg control message received ({} frames)", + addr, + frames.len() + ); + let payload = self.create_payload(PeerConnectionProtoMessage::Message, frames)?; + peer_conn.send(payload)?; + acquire_write_lock!(self.connection_stats).incr_message_sent(); + }, + ControlMessage::Pause => { + debug!(target: LOG_TARGET, "[{:?}] Pause control message received", addr); + self.paused = true; + }, + ControlMessage::Resume => { + debug!(target: LOG_TARGET, "[{:?}] Resume control message received", addr); + self.paused = false; + }, + ControlMessage::SetLinger(linger) => { + debug!( + target: LOG_TARGET, + "[{:?}] Setting linger to {:?} on peer connection", addr, linger + ); + // Log and ignore errors here since this is unlikely to happen or cause any issues + match peer_conn.set_linger(linger) { + Ok(_) => {}, + Err(err) => error!(target: LOG_TARGET, "Error setting linger on connection: {:?}", err), + } + }, + } + } + + if let Ok(event) = monitor.read(1) { + self.handle_socket_event(event)?; + } + + if !self.paused { + self.handle_frames(&peer_conn, &consumer)?; + } + } + } + + /// Handles socket events from the ConnectionMonitor. Updating connection + /// state as necessary. + fn handle_socket_event(&mut self, event: SocketEvent) -> Result<()> { + use SocketEventType::*; + + debug!(target: LOG_TARGET, "{:?}", event); + match event.event_type { + Disconnected => { + let mut lock = acquire_write_lock!(self.connection_state); + *lock = PeerConnectionState::Disconnected; + }, + Listening => { + self.retry_count = 0; + let mut lock = acquire_write_lock!(self.connection_state); + match *lock { + PeerConnectionState::Connecting(ref thread_ctl) => { + let info = ConnectionInfo { + control_messenger: thread_ctl.clone(), + connected_address: match self.context.peer_address { + NetAddress::IP(ref socket_addr) => Some(socket_addr.clone()), + _ => None, + }, + }; + info!( + target: LOG_TARGET, + "[{}] Listening on Inbound connection", self.context.peer_address + ); + *lock = PeerConnectionState::Listening(Arc::new(info)); + }, + PeerConnectionState::Connected(_) => { + warn!( + target: LOG_TARGET, + "[{}] Listening event when connected", self.context.peer_address + ); + }, + ref s => { + return Err(PeerConnectionError::StateError(format!( + "Unable to transition to connected state from state '{}'", + PeerConnectionSimpleState::from(s) + )) + .into()); + }, + } + }, + Connected => { + self.retry_count = 0; + self.transition_connected()?; + }, + BindFailed | AcceptFailed | HandshakeFailedNoDetail | HandshakeFailedProtocol | HandshakeFailedAuth => { + let mut lock = acquire_write_lock!(self.connection_state); + *lock = PeerConnectionState::Failed(PeerConnectionError::ConnectFailed); + }, + ConnectRetried => { + let mut lock = acquire_write_lock!(self.connection_state); + match *lock { + PeerConnectionState::Connecting(_) => { + self.retry_count += 1; + if self.retry_count >= self.context.max_retry_attempts { + *lock = PeerConnectionState::Failed(PeerConnectionError::ExceededMaxConnectRetryCount); + } + }, + _ => {}, + } + }, + _ => {}, + } + + Ok(()) + } + + fn transition_connected(&self) -> Result<()> { + let mut lock = acquire_write_lock!(self.connection_state); + + match *lock { + PeerConnectionState::Connecting(ref thread_ctl) => { + let info = ConnectionInfo { + control_messenger: thread_ctl.clone(), + connected_address: match self.context.peer_address { + NetAddress::IP(ref socket_addr) => Some(socket_addr.clone()), + _ => None, + }, + }; + info!(target: LOG_TARGET, "[{}] Connected", self.context.peer_address); + match self.context.direction { + Direction::Inbound => { + if self.identity.is_some() { + *lock = PeerConnectionState::Connected(Arc::new(info)); + } + }, + Direction::Outbound => { + *lock = PeerConnectionState::Connected(Arc::new(info)); + }, + } + }, + PeerConnectionState::Listening(ref info) => match self.context.direction { + Direction::Inbound => { + info!( + target: LOG_TARGET, + "Inbound connection listening on {}", self.context.peer_address + ); + if self.identity.is_some() { + *lock = PeerConnectionState::Connected(info.clone()); + } + }, + Direction::Outbound => { + return Err(PeerConnectionError::StateError( + "Should not happen: outbound connection was in listening state".to_string(), + ) + .into()); + }, + }, + PeerConnectionState::Connected(_) => { + warn!( + target: LOG_TARGET, + "[{}] Connected event when already connected", self.context.peer_address + ); + }, + ref s => { + return Err(PeerConnectionError::StateError(format!( + "Unable to transition to connected state from state '{}'", + PeerConnectionSimpleState::from(s) + )) + .into()); + }, + } + + debug!( + target: LOG_TARGET, + "[{}] Peer connection state is '{}'", + self.context.peer_address, + PeerConnectionSimpleState::from(&*lock) + ); + + Ok(()) + } + + /// Send PeerMessageType::Identify to remote peer + fn identify(&self, peer_conn: &EstablishedConnection) -> Result<()> { + let payload = self.create_payload(PeerConnectionProtoMessage::Identify, vec![vec![]])?; + peer_conn.send(payload) + } + + /// Connects the connection monitor to this worker's peer Connection. + fn connect_monitor(&self) -> Result { + let context = &self.context; + ConnectionMonitor::connect(&context.context, &self.monitor_addr) + } + + /// Handles PeerMessageType messages .Forwards frames from the source to the sink + fn handle_frames(&mut self, frontend: &EstablishedConnection, backend: &EstablishedConnection) -> Result<()> { + let context = &self.context; + if let Some(frames) = connection_try!(frontend.receive(10)) { + // Attempt to extract the parts of a peer message. + // If we can't extract the correct frames, we ignore the message + if let Some((identity, message_type, frames)) = self.extract_frame_parts(frames) { + match message_type { + PeerConnectionProtoMessage::Identify => { + if self.context.direction == Direction::Outbound { + warn!( + target: LOG_TARGET, + "Ignoring IDENTIFY message on outbound peer connection {:?}", self.context.id, + ); + return Ok(()); + } + + match self.identity { + Some(_) => { + warn!( + target: LOG_TARGET, + "Peer sent IDENT message when already set {:x?} {:x?}", self.identity, identity, + ); + }, + None => { + self.identity = identity; + self.transition_connected()?; + debug!( + target: LOG_TARGET, + "Peer sent IDENT, set peer connection identity to {:x?}", self.identity + ); + }, + } + }, + PeerConnectionProtoMessage::Message => { + acquire_write_lock!(self.connection_stats).incr_message_recv(); + + match context.direction { + // For a ZMQ_ROUTER, the first frame is the identity + Direction::Inbound => match self.identity { + Some(ref ident) => { + let identity = identity.expect( + "Invariant check: Inbound connections should always have an identity frame.", + ); + if identity != *ident { + return Err(PeerConnectionError::UnexpectedIdentity.into()); + } + }, + None => { + return Err(PeerConnectionError::IdentityNotEstablished.into()); + }, + }, + Direction::Outbound => {}, + } + + let payload = self.construct_consumer_payload(frames); + backend.send(&payload)?; + }, + PeerConnectionProtoMessage::Invalid => { + debug!( + target: LOG_TARGET, + "Peer sent invalid message type. Discarding the message" + ); + }, + } + } + } + Ok(()) + } + + fn extract_frame_parts( + &self, + mut frames: FrameSet, + ) -> Option<(Option, PeerConnectionProtoMessage, FrameSet)> + { + match self.context.direction { + Direction::Inbound => { + if frames.len() < 2 { + return None; + } + let identity = frames.drain(0..1).collect::().remove(0); + let message_type_u8 = frames.drain(0..1).collect::().remove(0).remove(0); + let message_type = PeerConnectionProtoMessage::from(message_type_u8); + + Some((Some(identity), message_type, frames)) + }, + Direction::Outbound => { + if frames.len() < 1 { + return None; + } + let message_type_u8 = frames.drain(0..1).collect::().remove(0).remove(0); + let message_type = PeerConnectionProtoMessage::from(message_type_u8); + + Some((None, message_type, frames)) + }, + } + } + + fn construct_consumer_payload(&self, frames: FrameSet) -> FrameSet { + let mut payload = Vec::with_capacity(2 + frames.len()); + payload.push(self.context.id.clone().into_inner()); + let forwardable = true; + payload.extend(forwardable.to_binary()); + payload.extend_from_slice(&frames); + payload + } + + /// Creates the payload to be sent to the underlying connection + fn create_payload(&self, message_type: PeerConnectionProtoMessage, frames: FrameSet) -> Result { + let context = &self.context; + + match context.direction { + // Add identity frame to the front of the payload for ROUTER socket + Direction::Inbound => match self.identity { + Some(ref ident) => { + let mut payload = Vec::with_capacity(2 + frames.len()); + payload.push(ident.clone()); + payload.push(vec![message_type as u8]); + payload.extend(frames); + + debug!( + target: LOG_TARGET, + "Created payload with identity frame {:x?} ({} frame(s))", + ident, + payload.len() + ); + Ok(payload) + }, + None => Err(PeerConnectionError::IdentityNotEstablished.into()), + }, + Direction::Outbound => { + let mut payload = Vec::with_capacity(1 + frames.len()); + payload.push(vec![message_type as u8]); + payload.extend(frames); + + Ok(payload) + }, + } + } + + /// Receive a `ControlMessage` on the control message channel + fn receive_control_msg(&self) -> Result> { + match self.receiver.recv_timeout(Duration::from_millis(5)) { + Ok(msg) => Ok(Some(msg)), + Err(e) => match e { + RecvTimeoutError::Disconnected => Err(PeerConnectionError::ControlPortDisconnected.into()), + RecvTimeoutError::Timeout => Ok(None), + }, + } + } + + /// Establish the connection to the peer address + fn establish_peer_connection(&self) -> Result { + let context = &self.context; + Connection::new(&context.context, context.direction.clone()) + .set_name(format!("peer-conn-{}", self.context.id).as_str()) + .set_linger(context.linger.clone()) + .set_heartbeat_interval(Duration::from_millis(1000)) + .set_heartbeat_timeout(Duration::from_millis(5000)) + .set_monitor_addr(self.monitor_addr.clone()) + .set_curve_encryption(context.curve_encryption.clone()) + .set_receive_hwm(PEER_CONNECTION_RECV_HWM) + .set_send_hwm(PEER_CONNECTION_SEND_HWM) + .set_socks_proxy_addr(context.socks_address.clone()) + .set_max_message_size(Some(context.max_msg_size)) + .establish(&context.peer_address) + } + + /// Establish the connection to the consumer + fn establish_sink_connection(&self) -> Result { + let context = &self.context; + Connection::new(&context.context, Direction::Outbound) + .set_name("peer-conn-sink") + .establish(&context.message_sink_address) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::connection::{ + peer_connection::{control::ThreadControlMessenger, ConnectionId}, + types::Linger, + CurveEncryption, + ZmqContext, + }; + + fn make_thread_ctl() -> (Arc, Receiver) { + let (tx, rx) = sync_channel(1); + (Arc::new(tx.into()), rx) + } + + fn transition_connected_setup( + direction: Direction, + initial_state: PeerConnectionSimpleState, + identity: Option, + ) -> PeerConnectionWorker + { + let context = ZmqContext::new(); + let peer_address = "127.0.0.1:9000".parse().unwrap(); + + let (thread_ctl, receiver) = make_thread_ctl(); + let info = Arc::new(ConnectionInfo { + connected_address: None, + control_messenger: Arc::clone(&thread_ctl), + }); + let connection_state = match initial_state { + PeerConnectionSimpleState::Initial => PeerConnectionState::Initial, + PeerConnectionSimpleState::Connecting => PeerConnectionState::Connecting(Arc::clone(&thread_ctl)), + PeerConnectionSimpleState::Connected(_) => PeerConnectionState::Connected(info), + PeerConnectionSimpleState::Disconnected => PeerConnectionState::Disconnected, + PeerConnectionSimpleState::Shutdown => PeerConnectionState::Shutdown, + PeerConnectionSimpleState::Listening(_) => PeerConnectionState::Listening(info), + PeerConnectionSimpleState::Failed(err) => PeerConnectionState::Failed(err), + }; + + let context = PeerConnectionContext { + context, + message_sink_address: InprocAddress::random(), + peer_address, + direction, + linger: Linger::Indefinitely, + id: ConnectionId::default(), + curve_encryption: CurveEncryption::default(), + socks_address: None, + max_msg_size: 1024 * 1024, + max_retry_attempts: 1, + }; + PeerConnectionWorker { + context, + identity, + receiver, + sender: thread_ctl.get_sender().clone(), + connection_state: Arc::new(RwLock::new(connection_state)), + connection_stats: Arc::new(RwLock::new(PeerConnectionStats::new())), + monitor_addr: InprocAddress::random(), + retry_count: 1, + paused: false, + } + } + + #[test] + fn transition_connected() { + // Transition outbound to connected + let subject = transition_connected_setup(Direction::Outbound, PeerConnectionSimpleState::Connecting, None); + subject.transition_connected().unwrap(); + { + let lock = subject.connection_state.read().unwrap(); + match (&*lock).into() { + PeerConnectionSimpleState::Connected(_) => {}, + s => panic!("Unexpected state '{:?}'", s), + } + } + + // Transition connecting inbound without identity + let subject = transition_connected_setup(Direction::Inbound, PeerConnectionSimpleState::Connecting, None); + subject.transition_connected().unwrap(); + { + let lock = subject.connection_state.read().unwrap(); + match (&*lock).into() { + PeerConnectionSimpleState::Connecting => {}, + s => panic!("Unexpected state '{:?}'", s), + } + } + + // Transition connecting inbound with identity + let subject = transition_connected_setup( + Direction::Inbound, + PeerConnectionSimpleState::Connecting, + Some(Vec::new()), + ); + subject.transition_connected().unwrap(); + { + let lock = subject.connection_state.read().unwrap(); + match (&*lock).into() { + PeerConnectionSimpleState::Connected(_) => {}, + s => panic!("Unexpected state '{:?}'", s), + } + } + + // Transition listening inbound without identity + let subject = transition_connected_setup(Direction::Inbound, PeerConnectionSimpleState::Listening(None), None); + subject.transition_connected().unwrap(); + { + let lock = subject.connection_state.read().unwrap(); + match (&*lock).into() { + PeerConnectionSimpleState::Listening(None) => {}, + s => panic!("Unexpected state '{:?}'", s), + } + } + + // Transition listening inbound with identity + let subject = transition_connected_setup( + Direction::Inbound, + PeerConnectionSimpleState::Listening(None), + Some(Vec::new()), + ); + subject.transition_connected().unwrap(); + { + let lock = subject.connection_state.read().unwrap(); + match (&*lock).into() { + PeerConnectionSimpleState::Connected(_) => {}, + s => panic!("Unexpected state '{:?}'", s), + } + } + + // Transition listening outbound with identity + let subject = transition_connected_setup( + Direction::Outbound, + PeerConnectionSimpleState::Listening(None), + Some(Vec::new()), + ); + match subject.transition_connected().unwrap_err() { + ConnectionError::PeerError(PeerConnectionError::StateError(_)) => {}, + err => panic!("Unexpected error: {:?}", err), + } + + // Transition connected to connected + let subject = transition_connected_setup(Direction::Inbound, PeerConnectionSimpleState::Connected(None), None); + subject.transition_connected().unwrap(); + { + let lock = subject.connection_state.read().unwrap(); + match (&*lock).into() { + PeerConnectionSimpleState::Connected(_) => {}, + s => panic!("Unexpected state '{:?}'", s), + } + } + // Transition from other states + let subject = transition_connected_setup(Direction::Inbound, PeerConnectionSimpleState::Initial, None); + match subject.transition_connected().unwrap_err() { + ConnectionError::PeerError(PeerConnectionError::StateError(_)) => {}, + err => panic!("Unexpected error: {:?}", err), + } + } +} diff --git a/comms/src/connection/types.rs b/comms/src/connection/types.rs new file mode 100644 index 0000000000..eecec36a72 --- /dev/null +++ b/comms/src/connection/types.rs @@ -0,0 +1,89 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::connection::ConnectionError; +use std::fmt; + +/// The types of socket available +pub enum SocketType { + Request, + Reply, + Router, + Dealer, + Pub, + Sub, + Push, + Pull, + Pair, +} + +/// Result type used by `comms::connection` module +pub type Result = std::result::Result; + +/// Represents the linger behavior of a connection. This can, depending on the chosen behavior, +/// allow a connection to finish sending messages before disconnecting. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Linger { + /// Linger until all messages have been sent + Indefinitely, + /// Don't linger, close the connection immediately + Never, + /// Linger for the specified time (in milliseconds) before disconnecting. + Timeout(u32), +} + +impl Default for Linger { + fn default() -> Self { + Linger::Never + } +} + +/// Direction of the connection +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum Direction { + /// Connection listens for incoming connections + Inbound, + /// Connection establishes an outbound connection + Outbound, +} + +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Used to select the method to use when establishing the connection. +pub enum SocketEstablishment { + /// Select bind or connect based on connection [Direction](./enum.Direction.html) + Auto, + /// Always bind on the socket + Bind, + /// Always connect on the socket + Connect, +} + +impl Default for SocketEstablishment { + fn default() -> Self { + SocketEstablishment::Auto + } +} diff --git a/comms/src/connection/zmq/context.rs b/comms/src/connection/zmq/context.rs new file mode 100644 index 0000000000..45f9bb803d --- /dev/null +++ b/comms/src/connection/zmq/context.rs @@ -0,0 +1,57 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use zmq; + +use crate::connection::{types::SocketType, zmq::ZmqError}; + +/// Thin wrapper of a [0MQ context]. +/// +/// [0MQ context]: http://api.zeromq.org/2-1:zmq#toc3 +#[derive(Clone, Default)] +pub struct ZmqContext(zmq::Context); + +impl ZmqContext { + pub fn new() -> Self { + Self(zmq::Context::new()) + } + + pub fn socket(&self, socket_type: SocketType) -> Result { + use SocketType::*; + + let zmq_socket_type = match socket_type { + Request => zmq::REQ, + Reply => zmq::REP, + Router => zmq::ROUTER, + Dealer => zmq::DEALER, + Pub => zmq::PUB, + Sub => zmq::SUB, + Push => zmq::PUSH, + Pull => zmq::PULL, + Pair => zmq::PAIR, + }; + + self.0 + .socket(zmq_socket_type) + .map_err(|e| ZmqError::SocketError(format!("{}", e))) + } +} diff --git a/comms/src/connection/zmq/curve_keypair.rs b/comms/src/connection/zmq/curve_keypair.rs new file mode 100644 index 0000000000..c2c43b9292 --- /dev/null +++ b/comms/src/connection/zmq/curve_keypair.rs @@ -0,0 +1,139 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::connection::{types::Result, ConnectionError}; +use clear_on_drop::clear::Clear; +use serde::{Deserialize, Serialize}; +use zmq; + +//---------------------------------- Curve Encryption --------------------------------------------// + +/// Represents settings for asymmetric curve encryption. Every socket with encryption enabled +/// must either act as a server or client. +#[derive(Clone)] +pub enum CurveEncryption { + /// No encryption + None, + /// Act as a server which accepts all connections which have a public key corresponding to the + /// given secret key. + Server { secret_key: CurveSecretKey }, + /// Act as a client which connects to a server with a given server public key and a client keypair. + Client { + secret_key: CurveSecretKey, + public_key: CurvePublicKey, + server_public_key: CurvePublicKey, + }, +} + +impl CurveEncryption { + /// Generates a Curve25519 public/private keypair + pub fn generate_keypair() -> Result<(CurveSecretKey, CurvePublicKey)> { + let keypair = zmq::CurveKeyPair::new().map_err(|e| { + ConnectionError::CurveKeypairError(format!("Unable to generate new Curve25519 keypair: {}", e)) + })?; + + Ok((CurveSecretKey(keypair.secret_key), CurvePublicKey(keypair.public_key))) + } +} + +impl Default for CurveEncryption { + fn default() -> Self { + CurveEncryption::None + } +} + +//---------------------------------- Curve Secret Key --------------------------------------------// + +/// Represents a Curve25519 secret key +#[derive(Clone)] +pub struct CurveSecretKey(pub(crate) [u8; 32]); + +impl CurveSecretKey { + pub fn is_zero(&self) -> bool { + self.0.iter().all(|b| *b == 0) + } + + pub fn into_inner(self) -> [u8; 32] { + self.0 + } +} + +impl Default for CurveSecretKey { + fn default() -> Self { + Self([0u8; 32]) + } +} + +impl Drop for CurveSecretKey { + fn drop(&mut self) { + self.0.clear(); + } +} + +//---------------------------------- Curve Public Key --------------------------------------------// +#[derive(Clone, Serialize, Deserialize, Debug)] +/// Represents a Curve25519 public key +pub struct CurvePublicKey(pub(crate) [u8; 32]); + +impl CurvePublicKey { + pub fn is_zero(&self) -> bool { + self.0.iter().all(|b| *b == 0) + } + + pub fn into_inner(self) -> [u8; 32] { + self.0 + } +} + +impl Default for CurvePublicKey { + fn default() -> Self { + Self([0u8; 32]) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + // Optimisations can cause this test to erroneously fail in release mode. The value is zeroed out on drop though. + #[cfg(debug_assertions)] + fn clears_secret_key_on_drop() { + use std::slice; + let ptr; + { + let sk = CurveEncryption::generate_keypair().unwrap().0; + ptr = sk.0.as_ptr() + } + + let zero = &[0u8; 32]; + unsafe { + assert_eq!(zero, slice::from_raw_parts(ptr, 32)); + } + } + + #[test] + fn default_is_zero() { + assert!(CurveSecretKey::default().is_zero()); + assert!(CurvePublicKey::default().is_zero()); + } +} diff --git a/comms/src/connection/zmq/endpoint.rs b/comms/src/connection/zmq/endpoint.rs new file mode 100644 index 0000000000..153a385b62 --- /dev/null +++ b/comms/src/connection/zmq/endpoint.rs @@ -0,0 +1,35 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// Any type that can be represented as a ZeroMQ endpoint string +pub trait ZmqEndpoint { + /// Convert to a ZeroMQ endpoint string + fn to_zmq_endpoint(&self) -> String; +} + +impl ZmqEndpoint for &T +where T: ZmqEndpoint +{ + fn to_zmq_endpoint(&self) -> String { + (*self).to_zmq_endpoint() + } +} diff --git a/comms/src/connection/zmq/error.rs b/comms/src/connection/zmq/error.rs new file mode 100644 index 0000000000..7fd34fcad2 --- /dev/null +++ b/comms/src/connection/zmq/error.rs @@ -0,0 +1,31 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; + +#[derive(Debug, Error, Eq, PartialEq)] +pub enum ZmqError { + /// Inproc address is malformed + MalformedInprocAddress, + #[error(msg_embedded, no_from, non_std)] + SocketError(String), +} diff --git a/comms/src/connection/zmq/inproc_address.rs b/comms/src/connection/zmq/inproc_address.rs new file mode 100644 index 0000000000..c30f624b7a --- /dev/null +++ b/comms/src/connection/zmq/inproc_address.rs @@ -0,0 +1,103 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::connection::zmq::{ZmqEndpoint, ZmqError}; +use rand::{distributions::Alphanumeric, EntropyRng, Rng}; +use std::{fmt, iter, str::FromStr}; + +const DEFAULT_INPROC: &str = "inproc://default"; + +/// Represents a zMQ inproc address. More information [here](http://api.zeromq.org/2-1:zmq-inproc). +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct InprocAddress(String); + +impl InprocAddress { + /// Generate a random InprocAddress. + pub fn random() -> Self { + let mut rng = EntropyRng::new(); + let rand_str: String = iter::repeat(()).map(|_| rng.sample(Alphanumeric)).take(8).collect(); + Self(format!("inproc://{}", rand_str)) + } + + pub fn is_default(&self) -> bool { + self.0 == DEFAULT_INPROC + } +} + +impl Default for InprocAddress { + fn default() -> Self { + Self(DEFAULT_INPROC.to_owned()) + } +} + +impl FromStr for InprocAddress { + type Err = ZmqError; + + fn from_str(s: &str) -> Result { + if s.len() > 9 && s.starts_with("inproc://") { + Ok(InprocAddress(s.to_owned())) + } else { + Err(ZmqError::MalformedInprocAddress) + } + } +} + +impl fmt::Display for InprocAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl ZmqEndpoint for InprocAddress { + fn to_zmq_endpoint(&self) -> String { + self.0.to_string() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_str() { + let addr = "inproc://扩".parse::().unwrap(); + assert_eq!("inproc://扩", addr.to_zmq_endpoint()); + + let result = "inporc://abc".parse::(); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!(ZmqError::MalformedInprocAddress, err); + + let result = "inproc://".parse::(); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!(ZmqError::MalformedInprocAddress, err); + } + + #[test] + fn default() { + let addr = InprocAddress::default(); + assert!(addr.is_default()); + let addr = InprocAddress::random(); + assert!(!addr.is_default()); + } +} diff --git a/comms/src/connection/zmq/mod.rs b/comms/src/connection/zmq/mod.rs new file mode 100644 index 0000000000..2ae901f0b7 --- /dev/null +++ b/comms/src/connection/zmq/mod.rs @@ -0,0 +1,36 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod curve_keypair; + +mod context; +mod endpoint; +mod error; +mod inproc_address; + +pub use self::{ + context::ZmqContext, + curve_keypair::{CurveEncryption, CurvePublicKey, CurveSecretKey}, + endpoint::ZmqEndpoint, + error::ZmqError, + inproc_address::InprocAddress, +}; diff --git a/comms/src/connection_manager/connections.rs b/comms/src/connection_manager/connections.rs new file mode 100644 index 0000000000..a5da5ae3d0 --- /dev/null +++ b/comms/src/connection_manager/connections.rs @@ -0,0 +1,339 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use log::*; + +use super::{ + error::ConnectionManagerError, + repository::{ConnectionRepository, Repository}, + types::PeerConnectionJoinHandle, + Result, +}; + +use crate::{connection::PeerConnection, peer_manager::node_id::NodeId}; + +use crate::connection::ConnectionError; +use std::{ + collections::HashMap, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, + time::Duration, +}; +use tari_utilities::thread_join::ThreadJoinWithTimeout; + +const LOG_TARGET: &str = "comms::connection_manager::connections"; + +/// Set the maximum waiting time for LivePeerConnections threads to join +const THREAD_JOIN_TIMEOUT_IN_MS: Duration = Duration::from_millis(100); + +/// Stores, and establishes the live peer connections +pub(super) struct LivePeerConnections { + repository: RwLock, + connection_thread_handles: RwLock>, + max_connections: usize, +} + +impl Default for LivePeerConnections { + fn default() -> Self { + Self { + repository: RwLock::new(ConnectionRepository::default()), + connection_thread_handles: RwLock::new(HashMap::new()), + max_connections: 100, + } + } +} + +impl LivePeerConnections { + #[cfg(test)] + pub fn new() -> Self { + Default::default() + } + + /// Create a new live peer connection + pub fn with_max_connections(max_connections: usize) -> Self { + Self { + max_connections, + ..Default::default() + } + } + + /// Get a connection by node id + pub fn get_connection(&self, node_id: &NodeId) -> Option> { + self.atomic_read(|lock| lock.get(node_id)) + } + + /// Get an active connection by node id + pub fn get_active_connection(&self, node_id: &NodeId) -> Option> { + self.atomic_read(|lock| { + lock.get(node_id) + .filter(|conn| conn.is_active()) + .map(|conn| conn.clone()) + }) + } + + /// Get number of active connections + pub fn get_active_connection_count(&self) -> usize { + self.atomic_read(|repo| repo.count_where(|conn| conn.is_active())) + } + + /// Add a connection to live peer connections + pub fn add_connection( + &self, + node_id: NodeId, + conn: Arc, + handle: PeerConnectionJoinHandle, + ) -> Result<()> + { + self.cleanup_inactive_connections(); + + self.atomic_write(|mut repo| { + let active_count = repo.count_where(|conn| conn.is_active()); + // If we're full drop the connection which has least recently been used + if active_count >= self.max_connections { + let recent_list = repo.sorted_recent_activity(); + if let Some(node_id) = recent_list.last().map(|(node_id, _)| node_id.clone().clone()) { + let conn = repo.remove(&node_id).expect( + "Invariant check: Unable to remove connection that was returned from \ + ConnectionRepository::sorted_recent_activity", + ); + + conn.shutdown() + .map_err(|err| ConnectionManagerError::ConnectionShutdownFailed(err))?; + } + } + + acquire_write_lock!(self.connection_thread_handles).insert(node_id.clone(), handle); + repo.insert(node_id, conn); + Ok(()) + }) + } + + /// Removes inactive connections from the live connection list + fn cleanup_inactive_connections(&self) { + self.atomic_write(|mut repo| { + // Drain the connections, and immediately drop them + let entries = repo.drain_filter(|(_, conn)| !conn.is_active() && !conn.is_initial()); + debug!(target: LOG_TARGET, "Discarding {} inactive connections", entries.len()); + }); + } + + /// If the connection exists, it is removed, shut down and returned. Otherwise + /// `ConnectionManagerError::PeerConnectionNotFound` is returned + pub fn shutdown_connection(&self, node_id: &NodeId) -> Result<(Arc, PeerConnectionJoinHandle)> { + self.atomic_write(|mut repo| { + let conn = repo + .remove(node_id) + .ok_or(ConnectionManagerError::PeerConnectionNotFound) + .map(|conn| conn.clone())?; + + let handle = acquire_write_lock!(self.connection_thread_handles) + .remove(node_id) + .expect( + "Invariant check: the peer connection join handle was not found. This is a bug as each peer \ + connection should have an associated join handle.", + ); + + debug!(target: LOG_TARGET, "Dropping connection for NodeID={}", node_id); + + conn.shutdown().map_err(ConnectionManagerError::ConnectionError)?; + + Ok((conn, handle)) + }) + } + + /// Send a shutdown signal to all peer connections, returning their worker thread handles + pub fn shutdown_all(self) -> HashMap { + info!(target: LOG_TARGET, "Shutting down all peer connections"); + self.atomic_read(|repo| { + repo.for_each(|conn| { + let _ = conn.shutdown(); + }); + }); + + acquire_lock!(self.connection_thread_handles, into_inner) + } + + /// Send a shutdown signal to all peer connections, and wait for all of them to + /// shut down, returning the result of the shutdown. + pub fn shutdown_joined(self) -> Vec> { + let handles = self.shutdown_all(); + + let mut results = vec![]; + for (_, handle) in handles.into_iter() { + results.push( + handle + .timeout_join(THREAD_JOIN_TIMEOUT_IN_MS) + .map_err(ConnectionError::ThreadJoinError) + .or_else(|err| { + error!(target: LOG_TARGET, "Failed to join: {:?}", err); + Err(err) + }), + ); + } + + results + } + + /// Returns true if the maximum number of connections has been reached, otherwise false + pub fn has_reached_max_active_connections(&self) -> bool { + let conn_count = self.get_active_connection_count(); + assert!( + conn_count <= self.max_connections, + "Invariant check: the active connection count is more than the max allowed connections. This is a bug as \ + active connections should never exceed max_connections." + ); + conn_count == self.max_connections + } + + /// Returns the number of connections (active and inactive) contained in the connection + /// repository. + #[cfg(test)] + pub fn repository_len(&self) -> usize { + let lock = acquire_read_lock!(self.repository); + lock.len() + } + + fn atomic_write(&self, f: F) -> T + where F: FnOnce(RwLockWriteGuard) -> T { + let lock = acquire_write_lock!(self.repository); + f(lock) + } + + fn atomic_read(&self, f: F) -> T + where F: FnOnce(RwLockReadGuard) -> T { + let lock = acquire_read_lock!(self.repository); + f(lock) + } +} + +#[cfg(test)] +mod test { + use super::*; + use rand::OsRng; + use std::thread; + use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + + fn make_join_handle() -> PeerConnectionJoinHandle { + thread::spawn(move || Ok(())) + } + + fn make_node_id() -> NodeId { + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut OsRng::new().unwrap()); + NodeId::from_key(&pk).unwrap() + } + + #[test] + fn new() { + let connections = LivePeerConnections::new(); + assert_eq!(0, connections.get_active_connection_count()); + } + + #[test] + fn with_max_connections() { + let connections = LivePeerConnections::with_max_connections(10); + assert_eq!(0, connections.get_active_connection_count()); + assert_eq!(10, connections.max_connections); + } + + #[test] + fn crud() { + let connections = LivePeerConnections::new(); + + let node_id = make_node_id(); + let conn = Arc::new(PeerConnection::new()); + let join_handle = make_join_handle(); + + connections.add_connection(node_id.clone(), conn, join_handle).unwrap(); + assert_eq!( + 1, + acquire_read_lock!(connections.connection_thread_handles) + .values() + .count() + ); + connections.get_connection(&node_id).unwrap(); + connections.shutdown_connection(&node_id).unwrap(); + assert_eq!( + 0, + acquire_read_lock!(connections.connection_thread_handles) + .values() + .count() + ); + + assert_eq!(0, connections.get_active_connection_count()); + } + + #[test] + fn drop_connection_fail() { + let connections = LivePeerConnections::new(); + let node_id = make_node_id(); + match connections.shutdown_connection(&node_id) { + Err(ConnectionManagerError::PeerConnectionNotFound) => {}, + Err(err) => panic!("Unexpected error: {:?}", err), + Ok(_) => panic!("Unexpected Ok result"), + } + } + + #[test] + fn shutdown() { + let connections = LivePeerConnections::new(); + + for _i in 0..3 { + let node_id = make_node_id(); + let conn = Arc::new(PeerConnection::new()); + let join_handle = make_join_handle(); + + connections.add_connection(node_id, conn, join_handle).unwrap(); + } + + let results = connections.shutdown_joined(); + assert_eq!(3, results.len()); + assert!(results.iter().all(|r| r.is_ok())); + } + + #[test] + fn has_reached_max_active_connections() { + let connections = LivePeerConnections::with_max_connections(2); + + let add_active_conn = |node_id| { + let (conn, rx) = PeerConnection::new_with_connecting_state_for_test(); + let join_handle = make_join_handle(); + + (connections.add_connection(node_id, Arc::new(conn), join_handle), rx) + }; + + let mut receivers = Vec::new(); + let mut node_ids = Vec::new(); + for _ in 0..3 { + let node_id = make_node_id(); + let (res, rx) = add_active_conn(node_id.clone()); + node_ids.push(node_id); + receivers.push(rx); + res.unwrap(); + } + + assert_eq!(connections.repository_len(), 2); + assert!(connections.get_connection(&node_ids[0]).is_none()); + assert!(connections.get_connection(&node_ids[1]).is_some()); + assert!(connections.get_connection(&node_ids[2]).is_some()); + + drop(receivers); + } +} diff --git a/comms/src/connection_manager/error.rs b/comms/src/connection_manager/error.rs new file mode 100644 index 0000000000..e6f983fd02 --- /dev/null +++ b/comms/src/connection_manager/error.rs @@ -0,0 +1,80 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::ConnectionError, + control_service::{messages::RejectReason, ControlServiceError}, + message::MessageError, + peer_manager::{node_identity::NodeIdentityError, PeerManagerError}, +}; +use derive_error::Error; +use tari_utilities::{ + ciphers::cipher::CipherError, + message_format::MessageFormatError, + thread_join::ThreadError, + ByteArrayError, +}; + +#[derive(Error, Debug)] +pub enum ConnectionManagerError { + /// There are no available peer connection ports + NoAvailablePeerConnectionPort, + /// The peer connection could not be found + PeerConnectionNotFound, + /// The peer could not be found + PeerNotFound, + // Error establishing connection + ConnectionError(ConnectionError), + #[error(no_from)] + CurveEncryptionGenerateError(ConnectionError), + MessageFormatError(MessageFormatError), + MessageError(MessageError), + /// The global node identity has not been set + GlobalNodeIdentityNotSet, + SharedSecretSerializationError(ByteArrayError), + CipherError(CipherError), + PeerManagerError(PeerManagerError), + /// The connection could not connect within the maximum number of attempts + MaxConnnectionAttemptsExceeded, + /// Problem creating or loading datastore + DatastoreError, + /// Connection timed out before it was able to connect + TimeoutBeforeConnected, + /// The maximum number of peer connections has been reached + MaxConnectionsReached, + /// Failed to shutdown a peer connection + #[error(no_from)] + ConnectionShutdownFailed(ConnectionError), + PeerConnectionThreadError(ThreadError), + #[error(msg_embedded, non_std, no_from)] + ControlServicePingPongFailed(String), + #[error(msg_embedded, non_std, no_from)] + SendRequestConnectionFailed(String), + /// Failed to receive a connection request outcome message + ConnectionRequestOutcomeRecvFail, + /// The request to establish a peer connection was rejected by the destination peer's control port + ConnectionRejected(RejectReason), + /// Failed to receive a connection request outcome before the timeout + ConnectionRequestOutcomeTimeout, + ControlServiceError(ControlServiceError), + NodeIdentityError(NodeIdentityError), +} diff --git a/comms/src/connection_manager/establisher.rs b/comms/src/connection_manager/establisher.rs new file mode 100644 index 0000000000..2e6c79ea07 --- /dev/null +++ b/comms/src/connection_manager/establisher.rs @@ -0,0 +1,312 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{error::ConnectionManagerError, types::PeerConnectionJoinHandle, Result}; +use crate::{ + connection::{ + curve_keypair::{CurvePublicKey, CurveSecretKey}, + net_address::ip::SocketAddress, + peer_connection::ConnectionId, + types::{Direction, Linger}, + Connection, + CurveEncryption, + EstablishedConnection, + InprocAddress, + NetAddress, + PeerConnection, + PeerConnectionContextBuilder, + ZmqContext, + }, + control_service::ControlServiceClient, + peer_manager::{NodeIdentity, Peer, PeerManager}, +}; +use log::*; +use std::{net::IpAddr, sync::Arc, time::Duration}; + +const LOG_TARGET: &str = "comms::connection_manager::establisher"; + +/// Configuration for peer connections which are produced by the ConnectionEstablisher +/// These are the common properties which are shared across all peer connections +#[derive(Clone)] +pub struct PeerConnectionConfig { + /// Maximum number of peer connections. Newer connections will be rejected until there are + /// less than `max_connections` active connections. + pub max_connections: usize, + /// Maximum size of inbound messages - messages larger than this will be dropped + pub max_message_size: u64, + /// The number of connection attempts to make to one address before giving up + pub max_connect_retries: u16, + /// The address of the SOCKS proxy to use for this connection + pub socks_proxy_address: Option, + /// The address to forward all the messages received from this peer connection + pub message_sink_address: InprocAddress, + /// The host to bind to when creating inbound connections + pub host: IpAddr, + /// The length of time to wait for the requested peer connection to be established before timing out. + /// Depending on the network, this should be long enough to allow a single back-and-forth + /// communication between peers. + pub peer_connection_establish_timeout: Duration, +} + +impl Default for PeerConnectionConfig { + fn default() -> Self { + Self { + max_connections: 100, + max_message_size: 1024 * 1024, + max_connect_retries: 5, + socks_proxy_address: None, + message_sink_address: Default::default(), + peer_connection_establish_timeout: Duration::from_secs(10), + host: "0.0.0.0".parse().unwrap(), + } + } +} + +/// ## ConnectionEstablisher +/// +/// This component is responsible for creating encrypted connections to peers and updating +/// the peer stats for failed/successful connection attempts. This component does not hold any +/// connections, but returns them so that the caller may use them as needed. This component does +/// not complete the peer connection protocol, it simply creates connections with some reliability. +pub struct ConnectionEstablisher { + context: ZmqContext, + config: PeerConnectionConfig, + node_identity: Arc, + peer_manager: Arc, +} + +impl ConnectionEstablisher { + /// Create a new ConnectionEstablisher. + pub fn new( + context: ZmqContext, + node_identity: Arc, + config: PeerConnectionConfig, + peer_manager: Arc, + ) -> Self + { + Self { + context, + node_identity, + config, + peer_manager, + } + } + + /// Returns the peer connection config + pub fn get_config(&self) -> &PeerConnectionConfig { + &self.config + } + + /// Attempt to establish a control service connection to one of the peer's addresses + /// + /// ### Arguments + /// - `peer`: `&Peer` - The peer to connect to + pub fn connect_control_service_client(&self, peer: &Peer) -> Result { + let config = &self.config; + + self.peer_manager + .reset_connection_attempts(&peer.node_id) + .map_err(ConnectionManagerError::PeerManagerError)?; + + info!( + target: LOG_TARGET, + "Starting {} attempt(s) to connect to control port for NodeId={}", + peer.addresses.len(), + peer.node_id + ); + + let maybe_client = self.attempt_control_port_connection_for_peer(&peer, || { + let address = self + .peer_manager + .get_best_net_address(&peer.node_id) + .map_err(ConnectionManagerError::PeerManagerError)?; + debug!(target: LOG_TARGET, "Attempting to connect to {}", address); + + let conn = Connection::new(&self.context, Direction::Outbound) + .set_name(format!("out-control-port-conn-{}", peer.node_id).as_str()) + .set_linger(Linger::Never) + .set_backlog(1) + .set_socks_proxy_addr(config.socks_proxy_address.clone()) + .set_max_message_size(Some(config.max_message_size)) + .establish(&address) + .map_err(ConnectionManagerError::ConnectionError)?; + + Ok((conn, address)) + })?; + + // Reset the connection attempts for this peer + // TODO(sdbondi): This is a good reason why peer manager shouldn't be managing connection + // attempts. Peer manager should be simplified a little to return an iterator + // sorted from 'best' to 'worst' net address without having to modify shared + // state. + self.peer_manager + .reset_connection_attempts(&peer.node_id) + .map_err(ConnectionManagerError::PeerManagerError)?; + + match maybe_client { + Some(client) => Ok(client), + None => Err(ConnectionManagerError::MaxConnnectionAttemptsExceeded), + } + } + + fn attempt_control_port_connection_for_peer( + &self, + peer: &Peer, + connection_factory: impl Fn() -> Result<(EstablishedConnection, NetAddress)>, + ) -> Result> + { + let num_attempts = peer.addresses.len(); + let mut current_attempts = 1; + loop { + let (conn, address) = connection_factory()?; + let client = ControlServiceClient::new(Arc::clone(&self.node_identity), peer.public_key.clone(), conn); + + if let Some(_) = client.ping_pong(self.config.peer_connection_establish_timeout).ok() { + self.peer_manager + .mark_successful_connection_attempt(&address) + .map_err(ConnectionManagerError::PeerManagerError)?; + + break Ok(Some(client)); + } + + self.peer_manager + .mark_failed_connection_attempt(&address) + .map_err(ConnectionManagerError::PeerManagerError)?; + + if current_attempts >= num_attempts { + break Ok(None); + } + + current_attempts += 1; + } + } + + /// Create a new outbound PeerConnection + /// + /// ### Arguments + /// `conn_id`: [ConnectionId] - The id to use for the connection + /// `address`: [NetAddress] - The [NetAddress] to connect to + /// `curve_public_key`: [&NetAddress] - The Curve25519 public key of the destination connection + /// + /// Returns an Arc<[PeerConnection]> in `Connected` state and the [std::thread::JoinHandle] of the + /// [PeerConnection] worker thread or an error. + pub fn establish_outbound_peer_connection( + &self, + conn_id: ConnectionId, + address: NetAddress, + curve_public_key: CurvePublicKey, + ) -> Result<(Arc, PeerConnectionJoinHandle)> + { + debug!(target: LOG_TARGET, "Establishing outbound connection to {}", address); + let (secret_key, public_key) = CurveEncryption::generate_keypair()?; + + let context = self + .new_context_builder() + .set_id(conn_id) + .set_direction(Direction::Outbound) + .set_address(address) + .set_curve_encryption(CurveEncryption::Client { + secret_key, + public_key, + server_public_key: curve_public_key, + }) + .build()?; + + let mut connection = PeerConnection::new(); + let worker_handle = connection.start(context)?; + connection + .wait_connected_or_failure(&self.config.peer_connection_establish_timeout) + .or_else(|err| { + error!(target: LOG_TARGET, "Outbound connection failed: {:?}", err); + Err(ConnectionManagerError::ConnectionError(err)) + })?; + + let connection = Arc::new(connection); + + Ok((connection, worker_handle)) + } + + /// Establish a new inbound peer connection. + /// + /// ### Arguments + /// `conn_id`: [ConnectionId] - The id to use for the connection + /// `curve_secret_key`: [&CurveSecretKey] - The zmq Curve25519 secret key for the connection + /// + /// Returns an Arc<[PeerConnection]> in `Listening` state and the [std::thread::JoinHandle] of the + /// [PeerConnection] worker thread or an error. + pub fn establish_inbound_peer_connection( + &self, + conn_id: ConnectionId, + curve_secret_key: CurveSecretKey, + ) -> Result<(Arc, PeerConnectionJoinHandle)> + { + // Providing port 0 tells the OS to allocate a port for us + let address = NetAddress::IP((self.config.host, 0).into()); + debug!(target: LOG_TARGET, "Establishing inbound connection to {}", address); + + let context = self + .new_context_builder() + .set_id(conn_id) + .set_direction(Direction::Inbound) + .set_address(address) + .set_curve_encryption(CurveEncryption::Server { + secret_key: curve_secret_key, + }) + .build()?; + + let mut connection = PeerConnection::new(); + let worker_handle = connection.start(context)?; + connection + .wait_listening_or_failure(&self.config.peer_connection_establish_timeout) + .or_else(|err| { + error!(target: LOG_TARGET, "Unable to establish inbound connection: {:?}", err); + Err(ConnectionManagerError::ConnectionError(err)) + })?; + + debug!( + target: LOG_TARGET, + "Inbound connection established on (NetAddress={:?}, SocketAddress={:?})", + connection.get_address(), + connection.get_connected_address() + ); + + let connection = Arc::new(connection); + + Ok((connection, worker_handle)) + } + + fn new_context_builder(&self) -> PeerConnectionContextBuilder { + let config = &self.config; + + let mut builder = PeerConnectionContextBuilder::new() + .set_context(&self.context) + .set_max_msg_size(config.max_message_size) + .set_message_sink_address(config.message_sink_address.clone()) + .set_max_retry_attempts(config.max_connect_retries); + + if let Some(ref addr) = config.socks_proxy_address { + builder = builder.set_socks_proxy(addr.clone()); + } + + builder + } +} diff --git a/comms/src/connection_manager/manager.rs b/comms/src/connection_manager/manager.rs new file mode 100644 index 0000000000..449908e5a0 --- /dev/null +++ b/comms/src/connection_manager/manager.rs @@ -0,0 +1,443 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{ + connections::LivePeerConnections, + establisher::ConnectionEstablisher, + protocol::PeerConnectionProtocol, + ConnectionManagerError, + EstablishLockResult, + PeerConnectionConfig, + Result, +}; +use crate::{ + connection::{ + zmq::InprocAddress, + ConnectionError, + CurveEncryption, + CurvePublicKey, + PeerConnection, + PeerConnectionState, + ZmqContext, + }, + control_service::messages::RejectReason, + peer_manager::{NodeId, NodeIdentity, Peer, PeerManager}, +}; +use log::*; +use std::{ + collections::HashMap, + result, + sync::{Arc, Mutex}, + time::Duration, +}; +use tari_utilities::thread_join::thread_join::ThreadJoinWithTimeout; + +const LOG_TARGET: &str = "comms::connection_manager::manager"; + +pub struct ConnectionManager { + node_identity: Arc, + connections: LivePeerConnections, + establisher: Arc, + peer_manager: Arc, + establish_locks: Mutex>>>, +} + +impl ConnectionManager { + /// Create a new connection manager + pub fn new( + zmq_context: ZmqContext, + node_identity: Arc, + peer_manager: Arc, + config: PeerConnectionConfig, + ) -> Self + { + Self { + connections: LivePeerConnections::with_max_connections(config.max_connections), + establisher: Arc::new(ConnectionEstablisher::new( + zmq_context, + Arc::clone(&node_identity), + config, + Arc::clone(&peer_manager), + )), + node_identity, + peer_manager, + establish_locks: Mutex::new(HashMap::new()), + } + } + + /// Attempt to establish a connection to a given NodeId. If the connection exists + /// the existing connection is returned. + pub fn establish_connection_to_node_id(&self, node_id: &NodeId) -> Result> { + match self.peer_manager.find_with_node_id(node_id) { + Ok(peer) => self.establish_connection_to_peer(&peer), + Err(err) => Err(ConnectionManagerError::PeerManagerError(err)), + } + } + + /// Attempt to establish a connection to a given peer. If the connection exists + /// the existing connection is returned. + pub fn establish_connection_to_peer(&self, peer: &Peer) -> Result> { + self.with_establish_lock(&peer.node_id, || self.attempt_connection_to_peer(peer)) + } + + fn attempt_connection_to_peer(&self, peer: &Peer) -> Result> { + let maybe_conn = self.connections.get_connection(&peer.node_id); + let peer_conn = match maybe_conn { + Some(conn) => { + let state = conn.get_state(); + + match state { + PeerConnectionState::Initial | + PeerConnectionState::Disconnected | + PeerConnectionState::Shutdown => { + warn!( + target: LOG_TARGET, + "Peer connection state is '{}'. Attempting to reestablish connection to peer.", state + ); + // Ignore not found error when dropping + let _ = self.connections.shutdown_connection(&peer.node_id); + self.initiate_peer_connection(peer)? + }, + PeerConnectionState::Failed(err) => { + warn!( + target: LOG_TARGET, + "Peer connection for NodeId={} in failed state. Error({:?}) Attempting to reestablish.", + peer.node_id, + err + ); + // Ignore not found error when dropping + self.connections.shutdown_connection(&peer.node_id)?; + self.initiate_peer_connection(peer)? + }, + // Already have an active connection, just return it + PeerConnectionState::Listening(Some(address)) => { + debug!( + target: LOG_TARGET, + "Waiting for NodeId={} to connect at {}...", peer.node_id, address + ); + return Ok(conn); + }, + PeerConnectionState::Listening(None) => { + debug!( + target: LOG_TARGET, + "Listening on non-tcp socket for NodeId={}...", peer.node_id + ); + return Ok(conn); + }, + PeerConnectionState::Connecting => { + debug!(target: LOG_TARGET, "Still connecting to {}...", peer.node_id); + return Ok(conn); + }, + PeerConnectionState::Connected(Some(address)) => { + debug!("Connection already established to {}.", address); + return Ok(conn); + }, + PeerConnectionState::Connected(None) => { + debug!("Connection already established to non-TCP socket"); + return Ok(conn); + }, + } + }, + None => { + debug!( + target: LOG_TARGET, + "Peer connection does not exist for NodeId={}", peer.node_id + ); + + self.initiate_peer_connection(peer)? + }, + }; + + Ok(peer_conn.clone()) + } + + /// Establish an inbound connection for the given peer and pass it (and it's `CurvePublicKey`) to a callback. + /// That callback will determine whether the connection should be added to the live connection list. This + /// enables you to for instance, implement a connection protocol which decides if the connection manager + /// ultimately accepts the peer connection. + /// + /// ## Arguments + /// + /// - `peer`: &Peer - Create an inbound connection for this peer + /// - `with_connection`: This callback is called with the new connection. If `Ok(Some(connection))` is returned, the + /// connection is added to the live connection list, otherwise it is discarded + pub(crate) fn with_new_inbound_connection( + &self, + peer: &Peer, + with_connection: impl FnOnce(Arc, CurvePublicKey) -> result::Result>, E>, + ) -> Result<()> + where + E: Into, + { + // If we have reached the maximum connections, we won't allow new connections to be requested + if self.connections.has_reached_max_active_connections() { + return Err(ConnectionManagerError::MaxConnectionsReached); + } + + let (secret_key, public_key) = CurveEncryption::generate_keypair()?; + + let (conn, join_handle) = self + .establisher + .establish_inbound_peer_connection(peer.node_id.clone().into(), secret_key)?; + + match with_connection(conn, public_key).map_err(Into::into)? { + Some(conn) => { + self.connections + .add_connection(peer.node_id.clone(), conn, join_handle)?; + }, + None => {}, + } + + Ok(()) + } + + /// Sends shutdown signals to all PeerConnections + pub fn shutdown(self) -> Vec> { + self.connections.shutdown_joined() + } + + /// Try to acquire an establish lock for the node ID. If a lock exists for the Node ID, + /// then return `EstablishLockResult::Collision` is returned. + pub fn try_acquire_establish_lock(&self, node_id: &NodeId, func: impl FnOnce() -> T) -> EstablishLockResult { + if acquire_lock!(self.establish_locks).contains_key(node_id) { + EstablishLockResult::Collision + } else { + self.with_establish_lock(node_id, || { + let res = func(); + EstablishLockResult::Ok(res) + }) + } + } + + /// Lock a critical section for the given node id during connection establishment + pub fn with_establish_lock(&self, node_id: &NodeId, func: impl FnOnce() -> T) -> T { + // Return the lock for the given node id. If no lock exists create a new one and return it. + let nid_lock = { + let mut establish_locks = acquire_lock!(self.establish_locks); + match establish_locks.get(node_id) { + Some(lock) => lock.clone(), + None => { + let new_lock = Arc::new(Mutex::new(())); + establish_locks.insert(node_id.clone(), new_lock.clone()); + new_lock + }, + } + }; + + // Lock the lock for the NodeId + let _nid_lock_guard = acquire_lock!(nid_lock); + let ret = func(); + // Remove establish lock once done to release memory. This is safe because the function has already + // established the connection, so any subsequent calls will return the existing connection. + { + let mut establish_locks = acquire_lock!(self.establish_locks); + establish_locks.remove(node_id); + } + ret + } + + /// Get the peer manager + pub(crate) fn peer_manager(&self) -> &PeerManager { + &self.peer_manager + } + + /// Shutdown a given peer's [PeerConnection] and return it if one exists, + /// otherwise None is returned. + /// + /// [PeerConnection]: ../../connection/peer_connection/index.html + pub(crate) fn shutdown_connection_for_peer(&self, peer: &Peer) -> Result>> { + match self.connections.shutdown_connection(&peer.node_id) { + Ok((conn, handle)) => { + handle + .timeout_join(Duration::from_millis(3000)) + .map_err(ConnectionManagerError::PeerConnectionThreadError)?; + Ok(Some(conn)) + }, + Err(ConnectionManagerError::PeerConnectionNotFound) => Ok(None), + Err(err) => Err(err), + } + } + + /// Return a connection for a peer if one exists, otherwise None is returned + pub(crate) fn get_connection(&self, peer: &Peer) -> Option> { + self.connections.get_connection(&peer.node_id) + } + + /// Return the number of _active_ peer connections currently managed by this instance + pub fn get_active_connection_count(&self) -> usize { + self.connections.get_active_connection_count() + } + + pub fn get_message_sink_address(&self) -> &InprocAddress { + &self.establisher.get_config().message_sink_address + } + + fn initiate_peer_connection(&self, peer: &Peer) -> Result> { + let protocol = PeerConnectionProtocol::new(&self.node_identity, &self.establisher); + self.peer_manager + .reset_connection_attempts(&peer.node_id) + .map_err(ConnectionManagerError::PeerManagerError)?; + + protocol + .negotiate_peer_connection(peer) + .and_then(|(new_conn, join_handle)| { + let config = self.establisher.get_config(); + debug!( + target: LOG_TARGET, + "[{:?}] Waiting {}s for peer connection acceptance from remote peer ", + new_conn.get_address(), + config.peer_connection_establish_timeout.as_secs(), + ); + + // Wait for peer connection to transition to connected state before continuing + new_conn + .wait_connected_or_failure(&config.peer_connection_establish_timeout) + .or_else(|err| { + info!( + target: LOG_TARGET, + "Peer did not accept the connection within {:?} [NodeId={}] : {:?}", + config.peer_connection_establish_timeout, + peer.node_id, + err, + ); + Err(ConnectionManagerError::ConnectionError(err)) + })?; + debug!( + target: LOG_TARGET, + "[{:?}] Connection established. Adding to active peer connections.", + new_conn.get_address(), + ); + + self.connections + .add_connection(peer.node_id.clone(), Arc::clone(&new_conn), join_handle)?; + + Ok(new_conn) + }) + .or_else(|err| match err { + ConnectionManagerError::ConnectionRejected(reason) => self.handle_connection_rejection(peer, reason), + _ => { + warn!( + target: LOG_TARGET, + "Failed to establish peer connection to NodeId={}", peer.node_id + ); + warn!( + target: LOG_TARGET, + "Failed connection error for NodeId={}: {:?}", peer.node_id, err + ); + Err(err) + }, + }) + } + + /// The peer is telling us that we already have a connection. This can occur if the connection has been made + /// by the remote peer while attempting to connect to it. Let's look for a connection and if we have one + fn handle_connection_rejection(&self, peer: &Peer, reason: RejectReason) -> Result> { + match reason { + RejectReason::ExistingConnection => self + .connections + .get_active_connection(&peer.node_id) + .ok_or(ConnectionManagerError::PeerConnectionNotFound), + _ => Err(ConnectionManagerError::ConnectionRejected(reason)), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + connection::{InprocAddress, NetAddress, ZmqContext}, + peer_manager::PeerFlags, + types::CommsPublicKey, + }; + use rand::rngs::OsRng; + use std::{thread, time::Duration}; + use tari_crypto::keys::PublicKey; + use tari_storage::HMapDatabase; + + fn setup() -> (ZmqContext, Arc, Arc) { + let context = ZmqContext::new(); + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + + let peer_manager = Arc::new(PeerManager::new(HMapDatabase::new()).unwrap()); + + (context, node_identity, peer_manager) + } + + fn create_peer(address: NetAddress) -> Peer { + let (_, pk) = CommsPublicKey::random_keypair(&mut OsRng::new().unwrap()); + let node_id = NodeId::from_key(&pk).unwrap(); + Peer::new(pk, node_id, address.into(), PeerFlags::empty()) + } + + #[test] + fn get_active_connection_count() { + let (context, node_identity, peer_manager) = setup(); + let manager = ConnectionManager::new(context, node_identity, peer_manager, PeerConnectionConfig { + peer_connection_establish_timeout: Duration::from_secs(5), + max_message_size: 1024, + host: "127.0.0.1".parse().unwrap(), + max_connect_retries: 3, + max_connections: 10, + message_sink_address: InprocAddress::random(), + socks_proxy_address: None, + }); + + assert_eq!(manager.get_active_connection_count(), 0); + } + + #[test] + fn shutdown_connection_for_peer() { + let (context, node_identity, peer_manager) = setup(); + let manager = ConnectionManager::new(context, node_identity, peer_manager, PeerConnectionConfig { + peer_connection_establish_timeout: Duration::from_secs(5), + max_message_size: 1024, + host: "127.0.0.1".parse().unwrap(), + max_connect_retries: 3, + max_connections: 10, + message_sink_address: InprocAddress::random(), + socks_proxy_address: None, + }); + + assert_eq!(manager.get_active_connection_count(), 0); + + let address = "127.0.0.1:43456".parse::().unwrap(); + let peer = create_peer(address.clone()); + + assert!(manager.shutdown_connection_for_peer(&peer).unwrap().is_none()); + + let (peer_conn, rx) = PeerConnection::new_with_connecting_state_for_test(); + let peer_conn = Arc::new(peer_conn); + let join_handle = thread::spawn(|| Ok(())); + manager + .connections + .add_connection(peer.node_id.clone(), peer_conn, join_handle) + .unwrap(); + + match manager.shutdown_connection_for_peer(&peer).unwrap() { + Some(_) => {}, + None => panic!("shutdown_connection_for_peer did not return active peer connection"), + } + + drop(rx); + } +} diff --git a/comms/src/connection_manager/mod.rs b/comms/src/connection_manager/mod.rs new file mode 100644 index 0000000000..3cc0256781 --- /dev/null +++ b/comms/src/connection_manager/mod.rs @@ -0,0 +1,107 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! # ConnectionManager +//! +//! Responsible for establishing and managing active connections to peers. +//! +//! It consists of a number of components each with their own concern. +//! +//! - [ConnectionManager] +//! +//! The public interface for connection management. This uses the other components +//! to manage peer connections for tari_comms. +//! +//! - [LivePeerConnections] +//! +//! A container for [PeerConnection]s which have been created by the [ConnectionManager]. +//! +//! - [ConnectionEstablisher] +//! +//! Responsible for creating [PeerConnection]s. This is basically a factory for [PeerConnection]s +//! which first checks that the connection is connected before passing it back to the caller. +//! +//! - [PeerConnectionProtocol] +//! +//! Uses the [ConnectionEstablisher] to connect to a given peer's [ControlService], +//! open an inbound [PeerConnection] and send an [RequestConnection] message with +//! to the peer's [ControlService]. +//! +//! ```edition2018 +//! # use std::time::Duration; +//! # use std::sync::Arc; +//! # use tari_comms::connection_manager::{ConnectionManager, PeerConnectionConfig}; +//! # use tari_comms::peer_manager::{PeerManager, NodeIdentity}; +//! # use tari_comms::connection::{ZmqContext, InprocAddress}; +//! # use rand::OsRng; +//! # use tari_storage::lmdb_store::LMDBBuilder; +//! # use lmdb_zero::db; +//! # use tari_storage::LMDBWrapper; +//! +//! let node_identity = Arc::new(NodeIdentity::random(&mut OsRng::new().unwrap(), "127.0.0.1:9000".parse().unwrap()).unwrap()); +//! +//! let context = ZmqContext::new(); +//! +//! let database_name = "cm_peer_database"; +//! let datastore = LMDBBuilder::new() +//! .set_path("/tmp/") +//! .set_environment_size(10) +//! .set_max_number_of_databases(1) +//! .add_database(database_name, lmdb_zero::db::CREATE) +//! .build().unwrap(); +//! let peer_database = datastore.get_handle(database_name).unwrap(); +//! let peer_database = LMDBWrapper::new(Arc::new(peer_database)); +//! let peer_manager = Arc::new(PeerManager::new(peer_database).unwrap()); +//! +//! let manager = ConnectionManager::new(context, node_identity, peer_manager, PeerConnectionConfig { +//! peer_connection_establish_timeout: Duration::from_secs(5), +//! max_message_size: 1024, +//! max_connections: 10, +//! host: "127.0.0.1".parse().unwrap(), +//! max_connect_retries: 3, +//! message_sink_address: InprocAddress::random(), +//! socks_proxy_address: None, +//! }); +//! +//! // No active connections +//! assert_eq!(manager.get_active_connection_count(), 0); +//! ``` +//! +//! [ConnectionManager]: ./manager/struct.ConnectionManager.html +//! [LivePeerConnections]: ./connections/struct.LivePeerConnections.html +//! [ControlService]: ../control_service/index.html +//! [RequestConnection]: ../message/p2p/struct.RequestConnection.html +//! [Connecti]: ./connections/struct.LivePeerConnections.html +//! [PeerConnection]: ../connection/peer_connection/struct.PeerConnection.html +//! [ConnectionEstablisher]: ./establisher/struct.ConnectionEstablisher.html +mod connections; +mod error; +pub mod establisher; +mod manager; +mod protocol; +mod repository; +mod types; + +pub(crate) use self::types::EstablishLockResult; +pub use self::{error::ConnectionManagerError, establisher::PeerConnectionConfig, manager::ConnectionManager}; + +type Result = std::result::Result; diff --git a/comms/src/connection_manager/protocol.rs b/comms/src/connection_manager/protocol.rs new file mode 100644 index 0000000000..ad2d3873df --- /dev/null +++ b/comms/src/connection_manager/protocol.rs @@ -0,0 +1,130 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{establisher::ConnectionEstablisher, types::PeerConnectionJoinHandle, ConnectionManagerError, Result}; +use crate::{ + connection::{CurvePublicKey, NetAddress, PeerConnection}, + control_service::messages::ConnectRequestOutcome, + peer_manager::{NodeIdentity, Peer}, +}; +use log::*; +use std::sync::Arc; + +const LOG_TARGET: &str = "comms::connection_manager::protocol"; + +pub(super) struct PeerConnectionProtocol<'e, 'ni> { + node_identity: &'ni Arc, + establisher: &'e ConnectionEstablisher, +} + +impl<'e, 'ni> PeerConnectionProtocol<'e, 'ni> { + pub fn new(node_identity: &'ni Arc, establisher: &'e ConnectionEstablisher) -> Self { + Self { + node_identity, + establisher, + } + } + + /// Send Establish connection message to the peers control port to request a connection + pub fn negotiate_peer_connection(&self, peer: &Peer) -> Result<(Arc, PeerConnectionJoinHandle)> { + info!(target: LOG_TARGET, "[NodeId={}] Negotiating connection", peer.node_id); + + // 1. Establish control service connection + let control_client = self.establisher.connect_control_service_client(&peer)?; + info!( + target: LOG_TARGET, + "[NodeId={}] Established peer control port connection at address {:?}", + peer.node_id, + control_client.connection().get_connected_address() + ); + + // 2. Send a request to connect + control_client + .send_request_connection( + self.node_identity.control_service_address()?, + self.node_identity.identity.node_id.clone(), + ) + .map_err(|err| ConnectionManagerError::SendRequestConnectionFailed(format!("{:?}", err)))?; + + debug!( + target: LOG_TARGET, + "[NodeId={}] RequestConnection message sent", peer.node_id + ); + + let config = self.establisher.get_config(); + // 3. Receive a request to connect outcome + control_client + .receive_message(config.peer_connection_establish_timeout) + .map_err(|_| ConnectionManagerError::ConnectionRequestOutcomeRecvFail)? + // Abort! Did not receive a connection outcome before the timeout + .ok_or(ConnectionManagerError::ConnectionRequestOutcomeTimeout) + .and_then(|msg| match msg { + ConnectRequestOutcome::Accepted { + curve_public_key, + address, + } => { + info!( + target: LOG_TARGET, + "[NodeId={}] RequestConnection accepted by destination peer's control port", peer.node_id + ); + + // Connect to the requested peer connection and send a identify frame + let (new_peer_conn, join_handle) = + self.establish_requested_peer_connection(peer, curve_public_key, address)?; + + Ok((new_peer_conn, join_handle)) + }, + ConnectRequestOutcome::Rejected(reason) => { + info!( + target: LOG_TARGET, + "[NodeId={}] RequestConnection REJECTED by destination peer's control port. Reason: {}", + peer.node_id, + reason + ); + + // Abort! The connection request was rejected + Err(ConnectionManagerError::ConnectionRejected(reason)) + }, + }) + } + + fn establish_requested_peer_connection( + &self, + peer: &Peer, + curve_public_key: CurvePublicKey, + address: NetAddress, + ) -> Result<(Arc, PeerConnectionJoinHandle)> + { + debug!( + target: LOG_TARGET, + "[NodeId={}] Connecting to given peer connection at address {}", peer.node_id, address + ); + + let (conn, join_handle) = self.establisher.establish_outbound_peer_connection( + peer.node_id.clone().into(), + address, + curve_public_key, + )?; + + Ok((conn, join_handle)) + } +} diff --git a/comms/src/connection_manager/repository.rs b/comms/src/connection_manager/repository.rs new file mode 100644 index 0000000000..2eeae9ef53 --- /dev/null +++ b/comms/src/connection_manager/repository.rs @@ -0,0 +1,218 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{connection::PeerConnection, peer_manager::node_id::NodeId}; +use chrono::Utc; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +lazy_static! { + static ref PORT_ALLOCATIONS: RwLock> = RwLock::new(vec![]); +} + +pub trait Repository { + fn get(&self, id: &I) -> Option>; + fn has(&self, id: &I) -> bool; + fn size(&self) -> usize; + fn insert(&mut self, id: I, value: Arc); + fn remove(&mut self, id: &I) -> Option>; +} + +impl Repository for ConnectionRepository { + fn get(&self, node_id: &NodeId) -> Option> { + self.entries.get(node_id).cloned() + } + + fn has(&self, node_id: &NodeId) -> bool { + self.entries.contains_key(node_id) + } + + fn size(&self) -> usize { + self.entries.values().count() + } + + fn insert(&mut self, node_id: NodeId, entry: Arc) { + self.entries.insert(node_id, entry); + } + + fn remove(&mut self, node_id: &NodeId) -> Option> { + self.entries.remove(node_id) + } +} + +#[derive(Default)] +pub(super) struct ConnectionRepository { + entries: HashMap>, +} + +impl ConnectionRepository { + #[cfg(test)] + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn count_where

(&self, predicate: P) -> usize + where P: FnMut(&&Arc) -> bool { + self.entries.values().filter(predicate).count() + } + + pub fn for_each(&self, mut f: impl FnMut(&Arc)) { + for entry in self.entries.values() { + f(entry); + } + } + + pub fn drain_filter(&mut self, predicate: F) -> Vec<(NodeId, Arc)> + where F: FnMut(&(&NodeId, &Arc)) -> bool { + let to_remove = self + .entries + .iter() + .filter(predicate) + .map(|(node_id, _)| node_id.clone()) + .collect::>(); + + to_remove + .into_iter() + .map(|node_id| { + let conn = self + .entries + .remove(&node_id) + .expect("Invariant (drain_filter): node_id key to be removed from entries was not found"); + + (node_id, conn) + }) + .collect::>() + } + + pub fn sorted_recent_activity(&self) -> Vec<(&NodeId, &Arc)> { + let now = Utc::now().naive_utc(); + + let mut items = self.entries.iter().collect::>(); + items.sort_by(|(_, a), (_, b)| { + let a_duration = now.signed_duration_since(a.last_activity()); + let b_duration = now.signed_duration_since(b.last_activity()); + a_duration.cmp(&b_duration) + }); + items + } +} + +#[cfg(test)] +mod test { + use super::*; + use rand::OsRng; + use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + + fn make_node_id() -> NodeId { + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut OsRng::new().unwrap()); + NodeId::from_key(&pk).unwrap() + } + + fn make_repo_with_connections(n: usize) -> (ConnectionRepository, Vec) { + let mut repo = ConnectionRepository::default(); + let mut node_ids = vec![]; + for _i in 0..n { + let node_id = make_node_id(); + let conn = Arc::new(PeerConnection::new()); + repo.insert(node_id.clone(), conn.clone()); + node_ids.push(node_id); + } + (repo, node_ids) + } + + #[test] + fn insert_get_remove() { + let (mut repo, node_ids) = make_repo_with_connections(2); + let unknown_node_id = make_node_id(); + + // Retrieve + assert!(repo.has(&node_ids[0])); + assert!(repo.has(&node_ids[1])); + assert!(!repo.has(&unknown_node_id)); + + assert!(repo.get(&node_ids[0]).is_some()); + assert!(repo.get(&node_ids[1]).is_some()); + assert!(repo.get(&unknown_node_id).is_none()); + + // Remove + assert!(repo.remove(&node_ids[0]).is_some()); + assert!(repo.remove(&unknown_node_id).is_none()); + + assert!(!repo.has(&node_ids[0])); + assert!(repo.has(&node_ids[1])); + assert!(!repo.has(&unknown_node_id)); + } + + #[test] + fn for_each() { + let (repo, _) = make_repo_with_connections(3); + + let mut count = 0; + repo.for_each(|_| { + count += 1; + }); + + assert_eq!(3, count); + } + + #[test] + fn count_where() { + let (repo, _) = make_repo_with_connections(4); + + let mut count = 0; + let total = repo.count_where(|_| { + count += 1; + count % 2 == 0 + }); + + assert_eq!(2, total); + } + + #[test] + fn drain_filter() { + let (mut repo, _) = make_repo_with_connections(4); + + // Get a copy of the expected node ids from the drain + let cloned = repo.entries.clone(); + let node_ids = cloned.keys().collect::>(); + let drained_node_id1 = node_ids[1].clone(); + let drained_node_id2 = node_ids[3].clone(); + let kept_node_id1 = node_ids[0]; + let kept_node_id2 = node_ids[2]; + + let mut i = 0; + let drained = repo.drain_filter(|_| { + i += 1; + i % 2 == 0 + }); + + assert_eq!(drained.len(), 2); + assert_eq!(drained[0].0, drained_node_id1); + assert_eq!(drained[1].0, drained_node_id2); + + assert_eq!(repo.entries.len(), 2); + assert!(repo.entries.contains_key(kept_node_id1)); + assert!(repo.entries.contains_key(kept_node_id2)); + } +} diff --git a/comms/src/connection_manager/types.rs b/comms/src/connection_manager/types.rs new file mode 100644 index 0000000000..30827f1553 --- /dev/null +++ b/comms/src/connection_manager/types.rs @@ -0,0 +1,33 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::thread::JoinHandle; + +use crate::connection; + +pub type PeerConnectionJoinHandle = JoinHandle>; + +#[must_use] +pub enum EstablishLockResult { + Ok(T), + Collision, +} diff --git a/comms/src/consts.rs b/comms/src/consts.rs new file mode 100644 index 0000000000..7780220025 --- /dev/null +++ b/comms/src/consts.rs @@ -0,0 +1,34 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::time::Duration; + +/// The maximum number of messages that can be stored using the MessageCache of the DHT +pub const DHT_MSG_CACHE_STORAGE_CAPACITY: usize = 1000; +/// The time-to-live duration used by the MessageCache for tracking received and handled messages +pub const DHT_MSG_CACHE_TTL: Duration = Duration::from_secs(300); +/// The number of neighbouring nodes that a received message will be forwarded to +pub const DHT_FORWARD_NODE_COUNT: usize = 8; + +/// The default length of the underlying pub/sub buffer using to publish comms messages. +/// This const is used in the CommsBuilder. +pub const COMMS_BUILDER_IMS_DEFAULT_PUB_SUB_BUFFER_LENGTH: usize = 100; diff --git a/comms/src/control_service/client.rs b/comms/src/control_service/client.rs new file mode 100644 index 0000000000..6e52b70042 --- /dev/null +++ b/comms/src/control_service/client.rs @@ -0,0 +1,198 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{ + messages::{ControlServiceRequestType, Ping, Pong, RequestPeerConnection}, + ControlServiceError, +}; +use crate::{ + connection::{Direction, EstablishedConnection, NetAddress}, + control_service::messages::ControlServiceResponseType, + message::{Message, MessageEnvelope, MessageFlags, MessageHeader, NodeDestination}, + peer_manager::{NodeId, NodeIdentity}, + types::CommsPublicKey, +}; +use std::{convert::TryInto, sync::Arc, time::Duration}; +use tari_utilities::message_format::MessageFormat; + +/// # ControlServiceClient +/// +/// This abstracts communication messages that can be sent and received to/from a [ControlService]. +/// +/// [ControlService]: ../service/struct.ControlService.html +pub struct ControlServiceClient { + connection: EstablishedConnection, + dest_public_key: CommsPublicKey, + node_identity: Arc, +} + +impl ControlServiceClient { + /// Create a new control service client + pub fn new( + node_identity: Arc, + dest_public_key: CommsPublicKey, + connection: EstablishedConnection, + ) -> Self + { + Self { + node_identity, + connection, + dest_public_key, + } + } + + /// Get a reference to the underlying connection + pub fn connection(&self) -> &EstablishedConnection { + &self.connection + } + + /// Send a Ping message + pub fn send_ping(&self) -> Result<(), ControlServiceError> { + self.send_msg(ControlServiceRequestType::Ping, Ping {}) + } + + /// Send a Ping message and wait until the given timeout for a Pong message. + pub fn ping_pong(&self, timeout: Duration) -> Result, ControlServiceError> { + self.send_msg(ControlServiceRequestType::Ping, Ping {})?; + + match self.receive_raw_message(timeout)? { + Some(msg) => { + let header = msg.deserialize_header()?; + match header.message_type { + ControlServiceResponseType::Pong => Ok(Some(msg.deserialize_message()?)), + _ => Err(ControlServiceError::ClientUnexpectedReply), + } + }, + None => Ok(None), + } + } + + /// Wait until the given timeout for any MessageFormat message _T_. + pub fn receive_message(&self, timeout: Duration) -> Result, ControlServiceError> + where T: MessageFormat { + match self.receive_raw_message(timeout)? { + Some(msg) => { + let message = msg.deserialize_message()?; + Ok(Some(message)) + }, + None => Ok(None), + } + } + + /// Wait until the given timeout for a raw [Message]. The [Message] signature is validated, otherwise + /// an error is returned. + /// + /// [Message]: ../../message/message/struct.Message.html + pub fn receive_raw_message(&self, timeout: Duration) -> Result, ControlServiceError> { + match connection_try!(self.connection.receive(timeout.as_millis() as u32)) { + Some(mut frames) => { + if self.connection.direction() == &Direction::Inbound { + frames.drain(0..1); + } + let envelope: MessageEnvelope = frames.try_into()?; + let header = envelope.deserialize_header()?; + if header.verify_signatures(envelope.body_frame().clone())? { + let msg = + envelope.deserialize_encrypted_body(&self.node_identity.secret_key, &self.dest_public_key)?; + Ok(Some(msg)) + } else { + Err(ControlServiceError::InvalidMessageSignature) + } + }, + None => Ok(None), + } + } + + /// Send a [RequestPeerConnection] message. + /// + /// [RequestPeerConnection]: ../messages/struct.RequestPeerConnection.html + pub fn send_request_connection( + &self, + control_service_address: NetAddress, + node_id: NodeId, + ) -> Result<(), ControlServiceError> + { + let msg = RequestPeerConnection { + control_service_address, + node_id, + }; + self.send_msg(ControlServiceRequestType::RequestPeerConnection, msg) + } + + fn send_msg(&self, message_type: ControlServiceRequestType, msg: T) -> Result<(), ControlServiceError> + where T: MessageFormat { + let envelope = self.construct_envelope(message_type, msg)?; + + self.connection + .send(envelope.into_frame_set()) + .map_err(ControlServiceError::ConnectionError) + } + + fn construct_envelope( + &self, + message_type: ControlServiceRequestType, + msg: T, + ) -> Result + where + T: MessageFormat, + { + let header = MessageHeader::new(message_type)?; + let msg = Message::from_message_format(header, msg)?; + + MessageEnvelope::construct( + &self.node_identity, + self.dest_public_key.clone(), + NodeDestination::PublicKey(self.dest_public_key.clone()), + msg.to_binary()?, + MessageFlags::ENCRYPTED, + ) + .map_err(ControlServiceError::MessageError) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::connection::{Connection, Direction, InprocAddress, ZmqContext}; + use rand::rngs::OsRng; + use tari_crypto::keys::PublicKey; + + #[test] + fn construct_envelope() { + let addr = InprocAddress::random(); + let context = ZmqContext::new(); + let conn = Connection::new(&context, Direction::Outbound).establish(&addr).unwrap(); + let node_identity = Arc::new(NodeIdentity::random_for_test(Some("127.0.0.1:9000".parse().unwrap()))); + let (_, public_key) = CommsPublicKey::random_keypair(&mut OsRng::new().unwrap()); + + let client = ControlServiceClient::new(node_identity.clone(), public_key.clone(), conn); + let envelope = client + .construct_envelope(ControlServiceRequestType::Ping, Ping {}) + .unwrap(); + + let header = envelope.deserialize_header().unwrap(); + assert_eq!(header.origin_source, node_identity.identity.public_key); + assert_eq!(header.peer_source, node_identity.identity.public_key); + assert_eq!(header.dest, NodeDestination::PublicKey(public_key)); + assert_eq!(header.flags, MessageFlags::ENCRYPTED); + } +} diff --git a/comms/src/control_service/error.rs b/comms/src/control_service/error.rs new file mode 100644 index 0000000000..c1766d2af1 --- /dev/null +++ b/comms/src/control_service/error.rs @@ -0,0 +1,62 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::{ConnectionError, NetAddressError}, + message::MessageError, + peer_manager::{node_identity::NodeIdentityError, PeerManagerError}, +}; +use derive_error::Error; +use tari_utilities::{ciphers::cipher::CipherError, message_format::MessageFormatError, thread_join::ThreadError}; + +#[derive(Debug, Error)] +pub enum ControlServiceError { + #[error(no_from)] + BindFailed(ConnectionError), + MessageError(MessageError), + /// Received an invalid message which cannot be handled + MessageFormatError(MessageFormatError), + /// Failed to send control message to worker + ControlMessageSendFailed, + // Failed to join on worker thread + WorkerThreadJoinFailed(ThreadError), + PeerManagerError(PeerManagerError), + ConnectionError(ConnectionError), + /// The worker thread failed to start + WorkerThreadFailedToStart, + /// Received an unencrypted message. Discarding it. + ReceivedUnencryptedMessage, + CipherError(CipherError), + /// Peer is banned, refusing connection request + PeerBanned, + /// Received message with an invalid signature + InvalidMessageSignature, + // Client Errors + /// Received an unexpected reply + ClientUnexpectedReply, + NetAddressError(NetAddressError), + /// The connection address could not be established + ConnectionAddressNotEstablished, + #[error(non_std, no_from, msg_embedded)] + ConnectionProtocolFailed(String), + NodeIdentityError(NodeIdentityError), +} diff --git a/comms/src/control_service/messages.rs b/comms/src/control_service/messages.rs new file mode 100644 index 0000000000..542bf3b0ca --- /dev/null +++ b/comms/src/control_service/messages.rs @@ -0,0 +1,95 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::{net_address::NetAddress, zmq::CurvePublicKey}, + peer_manager::NodeId, +}; +use derive_error::Error; +use serde::{Deserialize, Serialize}; + +/// Control service request message types +#[derive(Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum ControlServiceRequestType { + RequestPeerConnection, + Ping, +} + +/// Control service response message types +#[derive(Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum ControlServiceResponseType { + AcceptPeerConnection, + RejectPeerConnection, + Pong, + ConnectRequestOutcome, +} + +/// Details required to connect to the new [PeerConnection] +/// +/// [PeerConnection]: ../../connection/peer_connection/index.html +#[derive(Serialize, Deserialize, Debug)] +pub struct PeerConnectionDetails { + pub server_key: CurvePublicKey, + pub address: NetAddress, +} + +/// Represents an outcome for the request to establish a new [PeerConnection]. +/// +/// [PeerConnection]: ../../connection/peer_connection/index.html +#[derive(Serialize, Deserialize, Debug)] +pub enum ConnectRequestOutcome { + /// Accept response to a request to open a peer connection from a remote peer. + Accepted { + /// The zeroMQ Curve public key to use for the peer connection + curve_public_key: CurvePublicKey, + /// The address to which to connect + address: NetAddress, + }, + /// Reject response to a request to open a peer connection from a remote peer. + Rejected(RejectReason), +} + +/// Represents the reason for a peer connection request being rejected +#[derive(Error, Serialize, Deserialize, Debug)] +pub enum RejectReason { + /// Peer already has an existing active peer connection + ExistingConnection, + /// A connection collision has been detected, foreign node should abandon the connection attempt + CollisionDetected, +} + +/// This represents a request to open a peer connection +/// to a remote peer. +#[derive(Serialize, Deserialize, Debug)] +pub struct RequestPeerConnection { + pub control_service_address: NetAddress, + /// The node id of this node + pub node_id: NodeId, +} + +/// Sent to the control service to test liveness +#[derive(Serialize, Deserialize, Debug)] +pub struct Ping; + +/// Sent from the control service in response to a Ping to indicate liveness +#[derive(Serialize, Deserialize, Debug)] +pub struct Pong; diff --git a/comms/src/control_service/mod.rs b/comms/src/control_service/mod.rs new file mode 100644 index 0000000000..4b2a462c67 --- /dev/null +++ b/comms/src/control_service/mod.rs @@ -0,0 +1,106 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! # Control Service +//! +//! The control service listens on the configured address for [RequestConnection] messages +//! and decides whether to connect to the requested address. +//! +//! Once a control port connection has been established. The protocol for establishing a +//! peer connection is as follows: +//! +//! Node A Node B +//! + + +//! | ReqConn(n_id, addr) | +//! | +-------------------> | +//! | | Create Inbound PeerConnection +//! | Accept(c_pk, addr) | +//! | <------------------+ | --- +//! | | | Either Accept or Reject +//! | Reject(reason) | | +//! | <------------------+ | --- +//! | | +//! | | +//! | Connect to PeerConn | +//! | +-------------------> | +//! | | +//! + + +//! +//! ```edition2018 +//! # use tari_comms::{connection::*, control_service::*, dispatcher::*, connection_manager::*, peer_manager::*, types::*}; +//! # use std::{time::Duration, sync::Arc}; +//! # use std::collections::HashMap; +//! # use rand::OsRng; +//! # use tari_storage::lmdb_store::LMDBBuilder; +//! # use lmdb_zero::db; +//! # use tari_storage::LMDBWrapper; +//! +//! let node_identity = Arc::new(NodeIdentity::random(&mut OsRng::new().unwrap(), "127.0.0.1:9000".parse().unwrap()).unwrap()); +//! +//! let context = ZmqContext::new(); +//! let listener_address = "127.0.0.1:9000".parse::().unwrap(); +//! +//! let database_name = "cs_peer_database"; +//! let datastore = LMDBBuilder::new() +//! .set_path("/tmp/") +//! .set_environment_size(10) +//! .set_max_number_of_databases(2) +//! .add_database(database_name, lmdb_zero::db::CREATE) +//! .build().unwrap(); +//! let peer_database = datastore.get_handle(database_name).unwrap(); +//! let peer_database = LMDBWrapper::new(Arc::new(peer_database)); +//! let peer_manager = Arc::new(PeerManager::new(peer_database).unwrap()); +//! +//! let conn_manager = Arc::new(ConnectionManager::new(context.clone(), node_identity.clone(), peer_manager.clone(), PeerConnectionConfig { +//! max_message_size: 1024, +//! max_connect_retries: 1, +//! max_connections: 100, +//! socks_proxy_address: None, +//! message_sink_address: InprocAddress::random(), +//! host: "127.0.0.1".parse().unwrap(), +//! peer_connection_establish_timeout: Duration::from_secs(4), +//! })); +//! +//! let service = ControlService::with_default_config( +//! context, +//! node_identity, +//! ) +//! .serve(conn_manager) +//! .unwrap(); +//! +//! service.shutdown().unwrap(); +//! ``` +//! +//! [RequestConnection]: ./messages/struct.RequestConnection.html +mod client; +mod error; +pub mod messages; +mod service; +mod types; +mod worker; + +pub use self::{ + client::ControlServiceClient, + error::ControlServiceError, + messages::ControlServiceRequestType, + service::{ControlService, ControlServiceConfig, ControlServiceHandle}, +}; diff --git a/comms/src/control_service/service.rs b/comms/src/control_service/service.rs new file mode 100644 index 0000000000..acc71cd5cf --- /dev/null +++ b/comms/src/control_service/service.rs @@ -0,0 +1,146 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{ + error::ControlServiceError, + types::{ControlMessage, Result}, + worker::ControlServiceWorker, +}; +use crate::{ + connection::{net_address::ip::SocketAddress, NetAddress, ZmqContext}, + connection_manager::ConnectionManager, + peer_manager::NodeIdentity, + types::DEFAULT_LISTENER_ADDRESS, +}; +use log::*; +use std::{ + sync::{mpsc::SyncSender, Arc}, + thread, + time::Duration, +}; +use tari_utilities::thread_join::ThreadJoinWithTimeout; + +const LOG_TARGET: &str = "comms::control_service::service"; + +/// Configuration for [ControlService] +#[derive(Clone)] +pub struct ControlServiceConfig { + /// Which address to open a port + pub listener_address: NetAddress, + /// Optional SOCKS proxy + pub socks_proxy_address: Option, + /// The timeout for the peer to connect to the inbound connection. + /// If this timeout expires the peer connection will be shut down and discarded. + pub requested_connection_timeout: Duration, +} + +impl Default for ControlServiceConfig { + fn default() -> Self { + let listener_address = DEFAULT_LISTENER_ADDRESS.parse::().unwrap(); + ControlServiceConfig { + listener_address, + socks_proxy_address: None, + requested_connection_timeout: Duration::from_secs(5), + } + } +} + +/// The service responsible for establishing new [PeerConnection]s. +/// When `serve` is called, a worker thread starts up which listens for +/// connections on the configured `listener_address`. +pub struct ControlService { + context: ZmqContext, + config: ControlServiceConfig, + node_identity: Arc, +} + +impl ControlService { + pub fn with_default_config(context: ZmqContext, node_identity: Arc) -> Self { + Self { + context, + config: Default::default(), + node_identity, + } + } +} + +impl ControlService { + pub fn new(context: ZmqContext, node_identity: Arc, config: ControlServiceConfig) -> Self { + Self { + context, + config, + node_identity, + } + } + + pub fn serve(self, connection_manager: Arc) -> Result { + let config = self.config; + Ok(ControlServiceWorker::start(self.context.clone(), self.node_identity, config, connection_manager)?.into()) + } +} + +/// This is retured from the `ControlService::serve` method. It s a thread-safe +/// handle which can send control messages to the [ControlService] worker. +#[derive(Debug)] +pub struct ControlServiceHandle { + handle: thread::JoinHandle>, + sender: SyncSender, +} + +impl ControlServiceHandle { + /// Send a [ControlMessage::Shutdown] message to the worker thread. + pub fn shutdown(&self) -> Result<()> { + warn!(target: LOG_TARGET, "CONTROL SERVICE SHUTDOWN"); + self.sender + .send(ControlMessage::Shutdown) + .map_err(|_| ControlServiceError::ControlMessageSendFailed) + } + + pub fn timeout_join(self, timeout: Duration) -> Result<()> { + self.handle + .timeout_join(timeout) + .map_err(ControlServiceError::WorkerThreadJoinFailed) + } +} + +impl From<(thread::JoinHandle>, SyncSender)> for ControlServiceHandle { + fn from((handle, sender): (thread::JoinHandle>, SyncSender)) -> Self { + Self { handle, sender } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn control_service_has_default() { + let context = ZmqContext::new(); + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let control_service = ControlService::with_default_config(context, node_identity); + assert_eq!( + control_service.config.listener_address, + DEFAULT_LISTENER_ADDRESS.parse::().unwrap() + ); + assert!(control_service.config.socks_proxy_address.is_none()); + } +} diff --git a/infrastructure/comms/src/peer_manager/peer.rs b/comms/src/control_service/types.rs similarity index 86% rename from infrastructure/comms/src/peer_manager/peer.rs rename to comms/src/control_service/types.rs index afefe056af..a9b3b7211c 100644 --- a/infrastructure/comms/src/peer_manager/peer.rs +++ b/comms/src/control_service/types.rs @@ -20,18 +20,13 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::connection::NetAddress; +use super::error::ControlServiceError; -pub enum PeerType { - BaseNode, - ValidatorNode, - Wallet, - TokenWallet, +/// Control Messages for the control service worker +#[derive(Debug)] +pub enum ControlMessage { + Shutdown, } -#[allow(dead_code)] -pub struct Peer { - // TODO: Add fields - peer_type: PeerType, - addresses: Vec, -} +/// ControlService result type +pub type Result = std::result::Result; diff --git a/comms/src/control_service/worker.rs b/comms/src/control_service/worker.rs new file mode 100644 index 0000000000..edaa218eb6 --- /dev/null +++ b/comms/src/control_service/worker.rs @@ -0,0 +1,503 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{ + error::ControlServiceError, + messages::{ControlServiceRequestType, RequestPeerConnection}, + service::ControlServiceConfig, + types::{ControlMessage, Result}, +}; +use crate::{ + connection::{ + connection::EstablishedConnection, + types::Direction, + Connection, + ConnectionError, + CurvePublicKey, + NetAddress, + ZmqContext, + }, + connection_manager::{ConnectionManager, EstablishLockResult}, + control_service::messages::{ConnectRequestOutcome, ControlServiceResponseType, Pong, RejectReason}, + message::{ + Frame, + FrameSet, + Message, + MessageEnvelope, + MessageEnvelopeHeader, + MessageFlags, + MessageHeader, + NodeDestination, + }, + peer_manager::{NodeId, NodeIdentity, Peer, PeerFlags, PeerManagerError}, + types::{CommsCipher, CommsPublicKey}, +}; +use log::*; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + convert::TryInto, + sync::{ + mpsc::{sync_channel, Receiver, SyncSender}, + Arc, + }, + thread, + time::Duration, +}; +use tari_crypto::keys::DiffieHellmanSharedSecret; +use tari_utilities::{byte_array::ByteArray, ciphers::cipher::Cipher, message_format::MessageFormat}; + +const LOG_TARGET: &str = "comms::control_service::worker"; +/// The maximum message size allowed for the control service. +/// Messages will transparently drop if this size is exceeded. +const CONTROL_SERVICE_MAX_MSG_SIZE: u64 = 1024; // 1kb + +/// Set the allocated stack size for each ControlServiceWorker thread +const THREAD_STACK_SIZE: usize = 256 * 1024; // 256kb + +/// The [ControlService] worker is responsible for handling incoming messages +/// to the control port and dispatching them using the message dispatcher. +pub struct ControlServiceWorker { + config: ControlServiceConfig, + receiver: Receiver, + is_running: bool, + connection_manager: Arc, + node_identity: Arc, + listener: EstablishedConnection, +} + +impl ControlServiceWorker { + /// Start the worker + /// + /// # Arguments + /// - `context` - Connection context + /// - `config` - ControlServiceConfig + /// - `connection_manager` - the `ConnectionManager` + pub fn start( + context: ZmqContext, + node_identity: Arc, + config: ControlServiceConfig, + connection_manager: Arc, + ) -> Result<(thread::JoinHandle>, SyncSender)> + { + let (sender, receiver) = sync_channel(5); + + let handle = thread::Builder::new() + .name("control-service-worker-thread".to_string()) + .stack_size(THREAD_STACK_SIZE) + .spawn(move || { + info!( + target: LOG_TARGET, + "Control service starting on {}...", config.listener_address + ); + + let listener = Self::establish_listener(&context, &config)?; + let mut worker = Self::new(node_identity, config, connection_manager, receiver, listener); + + loop { + match worker.run() { + Ok(_) => { + info!(target: LOG_TARGET, "Control service exiting loop."); + break; + }, + + Err(err) => { + error!(target: LOG_TARGET, "Worker exited with an error: {:?}", err); + info!(target: LOG_TARGET, "Restarting control service after 1 second."); + thread::sleep(Duration::from_millis(1000)); + }, + } + } + + Ok(()) + }) + .map_err(|_| ControlServiceError::WorkerThreadFailedToStart)?; + + Ok((handle, sender)) + } + + fn new( + node_identity: Arc, + config: ControlServiceConfig, + connection_manager: Arc, + receiver: Receiver, + listener: EstablishedConnection, + ) -> Self + { + Self { + config, + connection_manager, + is_running: true, + node_identity, + receiver, + listener, + } + } + + fn run(&mut self) -> Result<()> { + debug!(target: LOG_TARGET, "Control service started"); + loop { + // Read incoming messages + if let Some(frames) = connection_try!(self.listener.receive(100)) { + debug!(target: LOG_TARGET, "Received {} frames", frames.len()); + match self.process_message(frames) { + Ok(_) => info!(target: LOG_TARGET, "Message processed"), + Err(err) => error!(target: LOG_TARGET, "Error when processing message: {:?}", err), + } + } + + // Process control messages + self.process_control_messages()?; + + if !self.is_running { + break; + } + } + + Ok(()) + } + + fn process_control_messages(&mut self) -> Result<()> { + if let Some(msg) = self.receiver.recv_timeout(Duration::from_millis(5)).ok() { + debug!(target: LOG_TARGET, "Received control message: {:?}", msg); + match msg { + ControlMessage::Shutdown => { + info!(target: LOG_TARGET, "Shutting down control service"); + self.is_running = false; + }, + } + } + Ok(()) + } + + fn process_message(&self, mut frames: FrameSet) -> Result<()> { + if frames.is_empty() { + // This case should never happen as ZMQ_ROUTER adds the identity frame + warn!(target: LOG_TARGET, "Received empty frames from socket."); + return Ok(()); + } + + let envelope: MessageEnvelope = frames + .drain(1..) + .collect::() + .try_into() + .map_err(ControlServiceError::MessageError)?; + + let identity_frame = frames + .pop() + .expect("Should not happen: drained all frames but the first, but then could not pop the first frame."); + + let envelope_header = envelope.deserialize_header()?; + if !envelope_header.flags.contains(MessageFlags::ENCRYPTED) { + return Err(ControlServiceError::ReceivedUnencryptedMessage); + } + + let maybe_peer = self.get_peer(&envelope_header.peer_source)?; + if maybe_peer.map(|p| p.is_banned()).unwrap_or(false) { + return Err(ControlServiceError::PeerBanned); + } + + let decrypted_body = self.decrypt_body(envelope.body_frame(), &envelope_header.origin_source)?; + let message = + Message::from_binary(decrypted_body.as_bytes()).map_err(ControlServiceError::MessageFormatError)?; + + debug!(target: LOG_TARGET, "Handling message"); + self.handle_message(envelope_header, identity_frame, message) + } + + fn handle_message( + &self, + envelope_header: MessageEnvelopeHeader, + identity_frame: Frame, + msg: Message, + ) -> Result<()> + { + let header = msg.deserialize_header().map_err(ControlServiceError::MessageError)?; + + match header.message_type { + ControlServiceRequestType::Ping => self.handle_ping(envelope_header, identity_frame), + ControlServiceRequestType::RequestPeerConnection => { + self.handle_request_connection(envelope_header, identity_frame, msg.deserialize_message()?) + }, + } + } + + fn handle_ping(&self, envelope_header: MessageEnvelopeHeader, identity_frame: Frame) -> Result<()> { + debug!(target: LOG_TARGET, "Got ping message"); + self.send_reply( + &envelope_header.peer_source, + identity_frame, + ControlServiceResponseType::Pong, + Pong {}, + ) + } + + fn handle_request_connection( + &self, + envelope_header: MessageEnvelopeHeader, + identity_frame: Frame, + message: RequestPeerConnection, + ) -> Result<()> + { + debug!( + target: LOG_TARGET, + "RequestConnection message received for NodeId {}", message.node_id + ); + + let pm = &self.connection_manager.peer_manager(); + let public_key = &envelope_header.peer_source; + let peer = match pm.find_with_public_key(&public_key) { + Ok(peer) => { + if peer.is_banned() { + return Err(ControlServiceError::PeerBanned); + } + + pm.update_peer( + &peer.public_key, + None, + Some(vec![message.control_service_address.clone()]), + None, + )?; + + peer + }, + Err(PeerManagerError::PeerNotFoundError) => { + let node_id = &message.node_id; + + let peer = Peer::new( + public_key.clone(), + node_id.clone(), + message.control_service_address.clone().into(), + PeerFlags::empty(), + ); + + pm.add_peer(peer.clone()) + .map_err(ControlServiceError::PeerManagerError)?; + peer + }, + Err(err) => return Err(ControlServiceError::PeerManagerError(err)), + }; + + // TODO: SECURITY The node ID is not a verified value at this point (PeerNotFoundError branch above). + // An attacker can insert any node id they want to get information about other peers connections + // to this node. For instance, if they already have an active connection. + // The public key should be used as that is validated by the message signature. + + let conn_manager = &self.connection_manager; + let establish_lock_result = conn_manager.try_acquire_establish_lock(&peer.node_id, || { + self.establish_connection_protocol(&peer, &envelope_header, identity_frame.clone()) + }); + + match establish_lock_result { + EstablishLockResult::Ok(result) => result, + EstablishLockResult::Collision => { + warn!( + target: LOG_TARGET, + "COLLISION DETECTED: this node is attempting to connect to the same node which is asking to \ + connect." + ); + if self.should_reject_collision(&peer.node_id) { + warn!( + target: LOG_TARGET, + "This connection attempt should be rejected. Rejecting the request to connect" + ); + self.reject_connection(&envelope_header, identity_frame, RejectReason::CollisionDetected)?; + Ok(()) + } else { + conn_manager.with_establish_lock(&peer.node_id, || { + self.establish_connection_protocol(&peer, &envelope_header, identity_frame) + }) + } + }, + } + } + + fn establish_connection_protocol( + &self, + peer: &Peer, + envelope_header: &MessageEnvelopeHeader, + identity_frame: Frame, + ) -> Result<()> + { + let conn_manager = &self.connection_manager; + if let Some(conn) = conn_manager.get_connection(peer) { + if conn.is_active() { + debug!( + target: LOG_TARGET, + "Already have active connection to peer. Rejecting the request for connection." + ); + self.reject_connection(&envelope_header, identity_frame, RejectReason::ExistingConnection)?; + return Ok(()); + } + } + + conn_manager + .with_new_inbound_connection(&peer, |new_inbound_conn, curve_public_key| { + let address = new_inbound_conn + .get_address() + .ok_or(ControlServiceError::ConnectionAddressNotEstablished)?; + + debug!( + target: LOG_TARGET, + "[NodeId={}] Inbound peer connection established on address {}", peer.node_id, address + ); + + // Create an address which can be connected to externally + let our_host = self.node_identity.control_service_address()?.host(); + let external_address = address + .maybe_port() + .map(|port| format!("{}:{}", our_host, port)) + .or(Some(our_host)) + .unwrap() + .parse() + .map_err(ControlServiceError::NetAddressError)?; + + debug!( + target: LOG_TARGET, + "Accepting peer connection request for NodeId={:?} on address {}", peer.node_id, external_address + ); + + self.accept_connection_request(&envelope_header, identity_frame, curve_public_key, external_address)?; + + match new_inbound_conn.wait_connected_or_failure(&self.config.requested_connection_timeout) { + Ok(_) => { + debug!( + target: LOG_TARGET, + "Connection to peer connection for NodeId {} succeeded", peer.node_id, + ); + + Ok(Some(new_inbound_conn)) + }, + Err(ConnectionError::Timeout) => Ok(None), + Err(err) => Err(ControlServiceError::ConnectionError(err)), + } + }) + .map_err(|err| ControlServiceError::ConnectionProtocolFailed(format!("{}", err)))?; + + Ok(()) + } + + fn should_reject_collision(&self, node_id: &NodeId) -> bool { + &self.node_identity.identity.node_id < node_id + } + + fn reject_connection( + &self, + envelope_header: &MessageEnvelopeHeader, + identity: Frame, + reason: RejectReason, + ) -> Result<()> + { + self.send_reply( + &envelope_header.peer_source, + identity, + ControlServiceResponseType::ConnectRequestOutcome, + ConnectRequestOutcome::Rejected(reason), + ) + } + + fn accept_connection_request( + &self, + envelope_header: &MessageEnvelopeHeader, + identity: Frame, + curve_public_key: CurvePublicKey, + address: NetAddress, + ) -> Result<()> + { + self.send_reply( + &envelope_header.peer_source, + identity, + ControlServiceResponseType::ConnectRequestOutcome, + ConnectRequestOutcome::Accepted { + curve_public_key, + address, + }, + ) + } + + fn get_peer(&self, public_key: &CommsPublicKey) -> Result> { + let peer_manager = &self.connection_manager.peer_manager(); + match peer_manager.find_with_public_key(public_key) { + Ok(peer) => Ok(Some(peer)), + Err(PeerManagerError::PeerNotFoundError) => Ok(None), + Err(err) => Err(ControlServiceError::PeerManagerError(err)), + } + } + + fn construct_envelope( + &self, + dest_public_key: &CommsPublicKey, + message_type: MT, + msg: T, + flags: MessageFlags, + ) -> Result + where + T: MessageFormat, + MT: Serialize + DeserializeOwned, + MT: MessageFormat, + { + let header = MessageHeader::new(message_type)?; + let msg = Message::from_message_format(header, msg).map_err(ControlServiceError::MessageError)?; + + MessageEnvelope::construct( + &self.node_identity, + dest_public_key.clone(), + NodeDestination::PublicKey(dest_public_key.clone()), + msg.to_binary().map_err(ControlServiceError::MessageFormatError)?, + flags, + ) + .map_err(ControlServiceError::MessageError) + } + + fn send_reply( + &self, + dest_public_key: &CommsPublicKey, + identity_frame: Frame, + message_type: ControlServiceResponseType, + msg: T, + ) -> Result<()> + where + T: MessageFormat, + { + let envelope = self.construct_envelope(dest_public_key, message_type, msg, MessageFlags::ENCRYPTED)?; + let mut frames = vec![identity_frame]; + + frames.extend(envelope.into_frame_set()); + + self.listener.send(frames).map_err(ControlServiceError::ConnectionError) + } + + fn decrypt_body(&self, body: &Frame, public_key: &CommsPublicKey) -> Result { + let ecdh_shared_secret = CommsPublicKey::shared_secret(&self.node_identity.secret_key, public_key).to_vec(); + CommsCipher::open_with_integral_nonce(&body, &ecdh_shared_secret).map_err(ControlServiceError::CipherError) + } + + fn establish_listener(context: &ZmqContext, config: &ControlServiceConfig) -> Result { + debug!(target: LOG_TARGET, "Binding on address: {}", config.listener_address); + Connection::new(&context, Direction::Inbound) + .set_name("Control Service Listener") + .set_receive_hwm(10) + .set_max_message_size(Some(CONTROL_SERVICE_MAX_MSG_SIZE)) + .set_socks_proxy_addr(config.socks_proxy_address.clone()) + .establish(&config.listener_address) + .map_err(ControlServiceError::BindFailed) + } +} diff --git a/comms/src/dispatcher/dispatcher.rs b/comms/src/dispatcher/dispatcher.rs new file mode 100644 index 0000000000..af7349a65f --- /dev/null +++ b/comms/src/dispatcher/dispatcher.rs @@ -0,0 +1,217 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::inbound_message_service::inbound_message_publisher::PublisherError; +use derive_error::Error; +use std::{collections::HashMap, error::Error, hash::Hash}; + +#[derive(Debug, Error, Clone)] +pub enum DispatchError { + /// A dispatch route was not defined for the specific message type + MessageHandlerNotDefined, + #[error(msg_embedded, non_std, no_from)] + ResolveFailed(String), + #[error(msg_embedded, non_std, no_from)] + HandlerError(String), + PublisherError(PublisherError), +} + +impl DispatchError { + pub fn resolve_failed() -> impl Fn(E) -> Self + where E: Error { + |err| DispatchError::ResolveFailed(format!("Dispatch resolve failed: {}", err)) + } + + pub fn handler_error() -> impl Fn(E) -> Self + where E: Error { + |err| DispatchError::HandlerError(format!("Handler error: {}", err)) + } +} + +#[derive(Debug, Error, Clone)] +pub enum HandlerError { + #[error(msg_embedded, non_std, no_from)] + Failed(String), +} + +impl HandlerError { + pub fn failed() -> impl Fn(E) -> Self + where E: Error { + |err| HandlerError::Failed(format!("Handler failed with error '{}'", err)) + } +} + +/// The signature of a handler function +type HandlerFunc = fn(msg: M) -> Result<(), E>; + +/// The trait bound for type parameter K on the dispatcher i.e the route key. +/// K must be Eq + Hash + {able to be sent across threads} + +/// 'static (all references must live as long as the program BUT there are no references +/// so therefore this is simply to satisfy the closure on `thread::spawn`) +/// This saves us from having to duplicate these trait bounds whenever we +/// want specify the type parameter K (like a type alias). +pub trait DispatchableKey: Eq + Hash + Send + Sync + 'static {} + +/// Implement this trait for all types which satisfy it's trait bounds. +impl DispatchableKey for T where T: Eq + Hash + Send + Sync + 'static {} + +/// A message type resolver. The resolver is called with the dispatched message. +/// The resolver should then decide which dispatch key should be used. +pub trait DispatchResolver //: Send + 'static +// where K: DispatchableKey +{ + fn resolve(&self, msg: &M) -> Result; +} + +/// Dispatcher pattern. Links handler function to "keys" which are resolved +/// be a given type implementing [DispatchResolver]. +/// +/// ## Type Parameters +/// `K` - The route key +/// `M` - The type which is passed into the handler +/// `R` - The resolver type +/// `E` - The type of error returned from the handler +pub struct Dispatcher +where R: DispatchResolver +{ + handlers: HashMap>, + catch_all: Option>, + resolver: R, +} + +impl Dispatcher +where + K: DispatchableKey, + R: DispatchResolver, + E: Error, +{ + /// Construct a new MessageDispatcher with no defined dispatch routes + pub fn new(resolver: R) -> Dispatcher { + Dispatcher { + handlers: HashMap::new(), + resolver, + catch_all: None, + } + } + + /// This function allows a new dispatch route to be specified and added to the handlers, all received messaged that + /// are of the dispatch type will be routed to the specified handler_function + pub fn route(mut self, path_key: K, handler: HandlerFunc) -> Self { + self.handlers.insert(path_key, handler); + self + } + + /// Set the handler to use if no other handlers match + pub fn catch_all(mut self, handler: HandlerFunc) -> Self { + self.catch_all = Some(handler); + self + } + + /// This function can be used to forward a message to the correct function handler + pub fn dispatch(&self, msg: M) -> Result<(), DispatchError> { + let route_type = self.resolver.resolve(&msg)?; + self.handlers + .get(&route_type) + .or_else(|| self.catch_all.as_ref()) + .ok_or(DispatchError::MessageHandlerNotDefined) + .and_then(|handler| { + handler(msg).map_err(|err| DispatchError::HandlerError(format!("Handler error: {:?}", err))) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_route_and_dispatch() { + #[derive(Debug, Hash, Eq, PartialEq)] + pub enum DispatchType { + Unknown, + Type1, + Type2, + Type3, + } + + pub struct Message { + pub data: String, + } + + pub struct TestResolver; + + impl DispatchResolver for TestResolver { + fn resolve(&self, msg: &Message) -> Result { + // Here you would usually look at the header for a message type + Ok(match msg.data.as_ref() { + "Type1" => DispatchType::Type1, + "Type2" => DispatchType::Type2, + _ => DispatchType::Type3, + }) + } + } + // Create a common variable to determine which handler function was called by the dispatcher + static mut CALLED_FN_TYPE: DispatchType = DispatchType::Unknown; + + fn test_fn1(_msg_data: Message) -> Result<(), DispatchError> { + unsafe { + CALLED_FN_TYPE = DispatchType::Type1; + } + Ok(()) + } + + fn test_fn2(_msg_data: Message) -> Result<(), DispatchError> { + unsafe { + CALLED_FN_TYPE = DispatchType::Type2; + } + Ok(()) + } + + fn test_fn3(_msg_data: Message) -> Result<(), DispatchError> { + unsafe { + CALLED_FN_TYPE = DispatchType::Type3; + } + Ok(()) + } + + let resolver = TestResolver {}; + + let message_dispatcher = Dispatcher::new(resolver) + .route(DispatchType::Type1, test_fn1) + .route(DispatchType::Type2, test_fn2) + .route(DispatchType::Type3, test_fn3); + // Test dispatch to default route + let msg_data = Message { data: "".to_string() }; + assert!(message_dispatcher.dispatch(msg_data).is_ok()); + unsafe { + assert_eq!(CALLED_FN_TYPE, DispatchType::Type3); + } + // Test dispatch to specified type route + let msg_data = Message { + data: "Type2".to_string(), + }; + assert!(message_dispatcher.dispatch(msg_data).is_ok()); + unsafe { + assert_eq!(CALLED_FN_TYPE, DispatchType::Type2); + } + } +} diff --git a/infrastructure/comms/src/server/mod.rs b/comms/src/dispatcher/mod.rs similarity index 75% rename from infrastructure/comms/src/server/mod.rs rename to comms/src/dispatcher/mod.rs index 8825987f71..92b0ccf776 100644 --- a/infrastructure/comms/src/server/mod.rs +++ b/comms/src/dispatcher/mod.rs @@ -19,12 +19,14 @@ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +//! # Dispatcher +//! +//! [HandlerFunc]s and associated "keys" are given to the dispatcher using the `route` method. +//! When dispatching a message, the key is resolver using the given [DispatchResolver] implementation. +//! The associated [HandlerFunc] is retrieved and called with the given type as the first parameter. +//! +//! [DispatchResolver]: ./dispatcher/trait.DispatchResolver.html +//! [HandlerFunc]: ./dispatcher/type.HandlerFunc.html +mod dispatcher; -/// Builder pattern for constructing a Server instance -pub struct ServerBuilder {} - -/// Inbound connections to this node -/// This server will listen on a ZMQ_ROUTER socket and accept multiple encrypted inbound connections. -/// It will then pass the data using ZMQ_DEALER to a configurable number of worker threads. -/// Workers will be listening for messages using a ZMQ_REP inproc socket. -struct Server {} \ No newline at end of file +pub use self::dispatcher::*; diff --git a/comms/src/domain_subscriber.rs b/comms/src/domain_subscriber.rs new file mode 100644 index 0000000000..869c074f2c --- /dev/null +++ b/comms/src/domain_subscriber.rs @@ -0,0 +1,197 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use crate::{futures::StreamExt, message::InboundMessage, peer_manager::PeerNodeIdentity, types::CommsPublicKey}; +use derive_error::Error; +use futures::{executor::block_on, future::select, stream::FusedStream, Stream}; +use std::fmt::Debug; +use tari_utilities::message_format::MessageFormat; + +#[derive(Debug, Error, PartialEq)] +pub enum DomainSubscriberError { + /// Subscription stream ended + SubscriptionStreamEnded, + /// Error reading from the stream + StreamError, + /// Message deserialization error + MessageError, + /// Subscription Reader is not initialized + SubscriptionReaderNotInitialized, +} + +/// Information about the message received +#[derive(Debug, Clone)] +pub struct MessageInfo { + pub peer_source: PeerNodeIdentity, + pub origin_source: CommsPublicKey, +} +pub struct SyncDomainSubscription { + subscription: Option, +} +impl SyncDomainSubscription +where S: Stream + Unpin + FusedStream +{ + pub fn new(stream: S) -> Self { + SyncDomainSubscription { + subscription: Some(stream), + } + } + + pub fn receive_messages(&mut self) -> Result, DomainSubscriberError> + where T: MessageFormat { + let subscription = self.subscription.take(); + + match subscription { + Some(mut s) => { + let (stream_messages, stream_complete): (Vec, bool) = block_on(async { + let mut result = Vec::new(); + let mut complete = false; + loop { + select!( + item = s.next() => { + if let Some(item) = item { + result.push(item) + } + }, + complete => { + complete = true; + break + }, + default => break, + ); + } + (result, complete) + }); + + let mut messages = Vec::new(); + + for m in stream_messages { + messages.push(( + MessageInfo { + peer_source: m.peer_source, + origin_source: m.origin_source, + }, + m.message + .deserialize_message() + .map_err(|_| DomainSubscriberError::MessageError)?, + )); + } + + if !stream_complete { + self.subscription = Some(s); + } + + return Ok(messages); + }, + None => return Err(DomainSubscriberError::SubscriptionStreamEnded), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + message::Message, + peer_manager::NodeIdentity, + pub_sub_channel::{pubsub_channel, TopicPayload}, + }; + use futures::{executor::block_on, future::select, stream::TryStreamExt, SinkExt}; + use serde::{Deserialize, Serialize}; + use std::sync::Arc; + #[test] + fn topic_pub_sub() { + let (mut publisher, subscriber_factory) = pubsub_channel(10); + + #[derive(Serialize, Deserialize, Debug, Clone)] + struct Dummy { + a: u32, + b: String, + } + + let node_id = NodeIdentity::random_for_test(None); + + let messages = vec![ + ("Topic1".to_string(), Dummy { + a: 1u32, + b: "one".to_string(), + }), + ("Topic2".to_string(), Dummy { + a: 2u32, + b: "two".to_string(), + }), + ("Topic1".to_string(), Dummy { + a: 3u32, + b: "three".to_string(), + }), + ("Topic2".to_string(), Dummy { + a: 4u32, + b: "four".to_string(), + }), + ("Topic1".to_string(), Dummy { + a: 5u32, + b: "five".to_string(), + }), + ("Topic2".to_string(), Dummy { + a: 6u32, + b: "size".to_string(), + }), + ("Topic1".to_string(), Dummy { + a: 7u32, + b: "seven".to_string(), + }), + ]; + + let serialized_messages = messages.iter().map(|m| { + TopicPayload::new( + m.0.clone(), + InboundMessage::new( + node_id.identity.clone(), + node_id.identity.public_key.clone(), + Message::from_message_format(m.0.clone(), m.1.clone()).unwrap(), + ), + ) + }); + + block_on(async { + for m in serialized_messages { + publisher.send(m).await.unwrap(); + } + }); + drop(publisher); + + let mut domain_sub = + SyncDomainSubscription::new(subscriber_factory.get_subscription("Topic1".to_string()).fuse()); + + let messages = domain_sub.receive_messages::().unwrap(); + + assert_eq!( + domain_sub.receive_messages::().unwrap_err(), + DomainSubscriberError::SubscriptionStreamEnded + ); + + assert_eq!(messages.len(), 4); + assert_eq!(messages[0].1.a, 1); + assert_eq!(messages[1].1.a, 3); + assert_eq!(messages[2].1.a, 5); + assert_eq!(messages[3].1.a, 7); + } +} diff --git a/comms/src/inbound_message_service/comms_msg_handlers.rs b/comms/src/inbound_message_service/comms_msg_handlers.rs new file mode 100644 index 0000000000..5b4594878d --- /dev/null +++ b/comms/src/inbound_message_service/comms_msg_handlers.rs @@ -0,0 +1,236 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + consts::DHT_FORWARD_NODE_COUNT, + dispatcher::{DispatchError, DispatchResolver, DispatchableKey}, + message::{InboundMessage, Message, MessageContext, MessageFlags, MessageHeader, NodeDestination}, + outbound_message_service::BroadcastStrategy, + types::MessageDispatcher, +}; +use log::*; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::Debug; + +const LOG_TARGET: &str = "comms::inbound_message_service::handlers"; + +/// The comms_msg_dispatcher will determine the type of message and forward it to the the correct handler +#[derive(Eq, PartialEq, Hash, Clone, Debug)] +pub enum CommsDispatchType { + // Messages of this type must be handled + Handle, + // Messages of this type must be forwarded to peers + Forward, + // Messages of this type can be ignored and discarded + Discard, +} + +/// Specify what handler function should be called for messages with different comms level dispatch types +pub fn construct_comms_msg_dispatcher() -> MessageDispatcher> +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + MessageDispatcher::new(InboundMessageServiceResolver {}) + .route(CommsDispatchType::Handle, handler_handle) + .route(CommsDispatchType::Forward, handler_forward) + .route(CommsDispatchType::Discard, handler_discard) +} + +#[derive(Clone)] +pub struct InboundMessageServiceResolver; + +impl DispatchResolver> for InboundMessageServiceResolver +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + /// The dispatch type is determined from the content of the MessageContext, which is used to dispatch the message to + /// the correct handler + fn resolve(&self, message_context: &MessageContext) -> Result { + let message_envelope = &message_context.message_envelope; + + // Check destination of message + let message_envelope_header = message_envelope + .deserialize_header() + .map_err(|e| DispatchError::HandlerError(format!("{}", e)))?; + + // Verify source node message signature + if !message_envelope_header + .verify_signatures(message_envelope.body_frame().clone()) + .map_err(|e| DispatchError::HandlerError(format!("{}", e)))? + { + return Ok(CommsDispatchType::Discard); + } + + let node_identity = &message_context.node_identity; + let peer_manager = &message_context.peer_manager; + + match message_envelope_header.dest { + NodeDestination::Unknown => Ok(CommsDispatchType::Handle), + NodeDestination::PublicKey(dest_public_key) => { + if node_identity.identity.public_key == dest_public_key { + Ok(CommsDispatchType::Handle) + } else if message_context.forwardable { + Ok(CommsDispatchType::Forward) + } else { + Ok(CommsDispatchType::Discard) + } + }, + NodeDestination::NodeId(dest_node_id) => { + if peer_manager + .in_network_region(&dest_node_id, &node_identity.identity.node_id, DHT_FORWARD_NODE_COUNT) + .map_err(|e| DispatchError::HandlerError(format!("{}", e)))? + { + Ok(CommsDispatchType::Handle) + } else if message_context.forwardable { + Ok(CommsDispatchType::Forward) + } else { + Ok(CommsDispatchType::Discard) + } + }, + } + } +} + +fn handler_handle(message_context: MessageContext) -> Result<(), DispatchError> +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + let envelope = &message_context.message_envelope; + debug!( + target: LOG_TARGET, + "Received message, wire format version {:x?}", + envelope.version_frame() + ); + + // Check encryption and retrieved Message + let message_envelope_header = envelope + .deserialize_header() + .map_err(|e| DispatchError::HandlerError(format!("{}", e)))?; + + debug!( + target: LOG_TARGET, + "Handling message with origin signature {:x?}", message_envelope_header.origin_signature + ); + let node_identity = &message_context.node_identity; + let message: Message; + if message_envelope_header.flags.contains(MessageFlags::ENCRYPTED) { + debug!(target: LOG_TARGET, "Attempting to decrypt message"); + match message_context + .message_envelope + .deserialize_encrypted_body(&node_identity.secret_key, &message_envelope_header.origin_source) + { + Ok(decrypted_message_body) => { + debug!(target: LOG_TARGET, "Message successfully decrypted"); + message = decrypted_message_body; + }, + Err(_) => { + if message_envelope_header.dest == NodeDestination::Unknown { + debug!( + target: LOG_TARGET, + "Unable to decrypt message with unknown recipient, forwarding..." + ); + // Message might have been for this node if it was able to decrypt it + if message_context.forwardable { + return handler_forward(message_context); + } else { + return handler_discard(message_context); + } + } else { + warn!(target: LOG_TARGET, "Unable to decrypt message addressed to this node"); + // Message was for this node but could not be decrypted + return handler_discard(message_context); + } + }, + } + } else { + debug!(target: LOG_TARGET, "Message not encrypted"); + message = message_context + .message_envelope + .deserialize_body() + .map_err(DispatchError::handler_error())?; + }; + + // Construct InboundMessage and dispatch to handler services using domain message broker + let header: MessageHeader = message.deserialize_header().unwrap(); //.map_err(DispatchError::handler_error())?; + + debug!(target: LOG_TARGET, "Received message type: {:?}", header.message_type); + let domain_message_context = InboundMessage::new( + message_context.peer.into(), + message_envelope_header.origin_source, + message, + ); + + debug!( + target: LOG_TARGET, + "Dispatching message type: {:?}", header.message_type + ); + + let imp = acquire_write_lock!(message_context.inbound_message_publisher); + imp.publish(header.message_type, domain_message_context)?; + Ok(()) +} + +fn handler_forward(message_context: MessageContext) -> Result<(), DispatchError> +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + // Forward message using appropriate broadcast strategy based on the destination provided in the header + let envelope = message_context.message_envelope; + let message_envelope_header = envelope + .deserialize_header() + .map_err(|e| DispatchError::HandlerError(format!("{}", e)))?; + let broadcast_strategy = BroadcastStrategy::forward( + message_context.node_identity.identity.node_id.clone(), + &message_context.peer_manager, + message_envelope_header.dest, + vec![ + message_envelope_header.origin_source, + message_envelope_header.peer_source, + ], + ) + .map_err(|e| DispatchError::HandlerError(format!("{}", e)))?; + + debug!(target: LOG_TARGET, "Forwarding message"); + message_context + .outbound_message_service + .forward_message(broadcast_strategy, envelope) + .map_err(|e| DispatchError::HandlerError(format!("{}", e))) +} + +fn handler_discard(_message_context: MessageContext) -> Result<(), DispatchError> +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + // TODO: Add logic for discarding a message + + Ok(()) +} diff --git a/comms/src/inbound_message_service/error.rs b/comms/src/inbound_message_service/error.rs new file mode 100644 index 0000000000..278d148ead --- /dev/null +++ b/comms/src/inbound_message_service/error.rs @@ -0,0 +1,47 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::{error::ConnectionError, DealerProxyError}, + inbound_message_service::message_cache::MessageCacheError, +}; +use derive_error::Error; +use tari_utilities::thread_join::ThreadError; + +/// Error type for InboundMessageService subsystem +#[derive(Debug, Error)] +pub enum InboundError { + /// Failed to connect to inbound socket + InboundConnectionError(ConnectionError), + DealerProxyError(DealerProxyError), + MessageCacheError(MessageCacheError), + #[error(msg_embedded, non_std, no_from)] + ControlSendError(String), + /// Unable to send a control message as the control sync sender is undefined + ControlSenderUndefined, + /// Could not join the InboundMessageWorker thread + ThreadJoinError(ThreadError), + /// The thread handle is undefined and could have not been properly created + ThreadHandleUndefined, + /// Inbound message worker thread failed to start + ThreadInitializationError, +} diff --git a/comms/src/inbound_message_service/inbound_message_publisher.rs b/comms/src/inbound_message_service/inbound_message_publisher.rs new file mode 100644 index 0000000000..005520e500 --- /dev/null +++ b/comms/src/inbound_message_service/inbound_message_publisher.rs @@ -0,0 +1,83 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::pub_sub_channel::{TopicPayload, TopicPublisher}; +use bus_queue::Publisher; +use derive_error::Error; +use futures::{executor::block_on, prelude::*}; +use log::*; +use std::{fmt::Debug, sync::Mutex}; + +const LOG_TARGET: &str = "comms::inbound_message_service::inbound_message_publisher"; + +#[derive(Clone, Debug, Error)] +pub enum PublisherError { + /// The Thread Safety has been breached and data access has become poisoned + PoisonedAccess, + /// Publisher is None inside Mutex, indicates dead lock + PublisherLock, + /// Publisher could not send message + PublisherSendError, +} + +pub struct InboundMessagePublisher +where + MType: Send + Sync + Debug, + T: Clone + Send + Sync, +{ + publisher: Mutex>>, +} + +impl InboundMessagePublisher +where + MType: Send + Sync + 'static + Debug, + T: Clone + Send + Sync + 'static, +{ + pub fn new(publisher: Publisher>) -> InboundMessagePublisher { + info!(target: LOG_TARGET, "Inbound Message Publisher created"); + InboundMessagePublisher { + publisher: Mutex::new(Some(publisher)), + } + } + + pub fn publish(&self, message_type: MType, message: T) -> Result<(), PublisherError> { + // TODO This mutex should not be required and is only present due the IMS workers being in their own threads. + // Future refactor will remove the need for the lock and this Option container + let mut publisher_lock = self.publisher.lock().map_err(|_| PublisherError::PoisonedAccess)?; + match publisher_lock.take() { + Some(mut p) => { + info!( + target: LOG_TARGET, + "Inbound message of type {:?} about to be published", message_type + ); + + block_on(async { p.send(TopicPayload::new(message_type, message)).await }) + .map_err(|_| PublisherError::PublisherSendError)?; + + *publisher_lock = Some(p); + + Ok(()) + }, + None => Err(PublisherError::PublisherLock), + } + } +} diff --git a/comms/src/inbound_message_service/inbound_message_service.rs b/comms/src/inbound_message_service/inbound_message_service.rs new file mode 100644 index 0000000000..ab5fc60763 --- /dev/null +++ b/comms/src/inbound_message_service/inbound_message_service.rs @@ -0,0 +1,299 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{error::InboundError, inbound_message_worker::*}; +use crate::{ + connection::{ + peer_connection::ControlMessage, + zmq::{InprocAddress, ZmqContext}, + }, + dispatcher::DispatchableKey, + inbound_message_service::inbound_message_publisher::InboundMessagePublisher, + message::{InboundMessage, MessageContext}, + outbound_message_service::outbound_message_service::OutboundMessageService, + peer_manager::{peer_manager::PeerManager, NodeIdentity}, + types::MessageDispatcher, +}; +use log::*; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + fmt::Debug, + sync::{mpsc::SyncSender, Arc, RwLock}, + thread::JoinHandle, + time::Duration, +}; +use tari_utilities::thread_join::ThreadJoinWithTimeout; + +const LOG_TARGET: &str = "comms::inbound_message_service"; + +/// Set the maximum waiting time for InboundMessageWorker thread to join +const THREAD_JOIN_TIMEOUT_IN_MS: Duration = Duration::from_millis(100); + +#[derive(Clone, Copy)] +pub struct InboundMessageServiceConfig { + /// Timeout used for receiving messages from the message queue + pub worker_timeout_in_ms: Duration, + /// Timeout used for listening for control messages + pub control_timeout_in_ms: Duration, +} + +impl Default for InboundMessageServiceConfig { + fn default() -> Self { + InboundMessageServiceConfig { + worker_timeout_in_ms: Duration::from_millis(100), + control_timeout_in_ms: Duration::from_millis(5), + } + } +} + +/// The InboundMessageService manages the inbound message queue. The messages received from different peers are written +/// to, and accumulate in, the inbound message queue. The InboundMessageWorker will then retrieve messages from the +/// queue and dispatch them using the dispatcher, that will check signatures and decrypt the message before being sent +/// to the InboundMessageBroker. The InboundMessageBroker will then send it to the correct handler services. +pub struct InboundMessageService +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + config: InboundMessageServiceConfig, + context: ZmqContext, + node_identity: Arc, + message_queue_address: InprocAddress, + message_dispatcher: Arc>>, + inbound_message_publisher: Arc>>, + outbound_message_service: Arc, + peer_manager: Arc, + worker_thread_handle: Option>, + worker_control_sender: Option>, +} + +impl InboundMessageService +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + /// Creates a new InboundMessageService that will receive message on the message_queue_address that it will then + /// dispatch + pub fn new( + config: InboundMessageServiceConfig, + context: ZmqContext, + node_identity: Arc, + message_queue_address: InprocAddress, + message_dispatcher: Arc>>, + inbound_message_publisher: Arc>>, + outbound_message_service: Arc, + peer_manager: Arc, + ) -> Self + { + InboundMessageService { + config, + context: context.clone(), + node_identity, + message_queue_address, + message_dispatcher, + inbound_message_publisher, + outbound_message_service, + peer_manager, + worker_thread_handle: None, + worker_control_sender: None, + } + } + + /// Spawn an InboundMessageWorker for the InboundMessageService + pub fn start(&mut self) -> Result<(), InboundError> { + info!(target: LOG_TARGET, "Starting inbound message service"); + let worker = InboundMessageWorker::new( + self.config, + self.context.clone(), + self.node_identity.clone(), + self.message_queue_address.clone(), + self.message_dispatcher.clone(), + self.inbound_message_publisher.clone(), + self.outbound_message_service.clone(), + self.peer_manager.clone(), + ); + let (worker_thread_handle, worker_sync_sender) = worker.start()?; + self.worker_thread_handle = Some(worker_thread_handle); + self.worker_control_sender = Some(worker_sync_sender); + Ok(()) + } + + /// Tell the underlying worker thread to shut down + pub fn shutdown(self) -> Result<(), InboundError> { + self.worker_control_sender + .ok_or(InboundError::ControlSenderUndefined)? + .send(ControlMessage::Shutdown) + .map_err(|e| InboundError::ControlSendError(format!("Failed to send control message: {:?}", e)))?; + self.worker_thread_handle + .ok_or(InboundError::ThreadHandleUndefined)? + .timeout_join(THREAD_JOIN_TIMEOUT_IN_MS) + .map_err(InboundError::ThreadJoinError)?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + connection::{ + zmq::{InprocAddress, ZmqContext}, + Connection, + Direction, + NetAddress, + }, + futures::StreamExt, + inbound_message_service::comms_msg_handlers::*, + message::{ + InboundMessage, + Message, + MessageData, + MessageEnvelope, + MessageFlags, + MessageHeader, + NodeDestination, + }, + peer_manager::{peer_manager::PeerManager, NodeIdentity, Peer, PeerFlags}, + pub_sub_channel::pubsub_channel, + }; + use crossbeam_channel as channel; + use futures::{executor::block_on, future::select}; + use serde::{Deserialize, Serialize}; + use std::{sync::Arc, thread, time::Duration}; + use tari_storage::HMapDatabase; + use tari_utilities::message_format::MessageFormat; + fn pause() { + thread::sleep(Duration::from_millis(5)); + } + + fn create_message_data_buffer(node_identity: Arc, message_envelope_body: Message) -> Vec> { + let dest_public_key = node_identity.identity.public_key.clone(); // Send to self + let message_envelope = MessageEnvelope::construct( + &node_identity, + dest_public_key.clone(), + NodeDestination::Unknown, + message_envelope_body.to_binary().unwrap(), + MessageFlags::NONE, + ) + .unwrap(); + let message_data = MessageData::new(node_identity.identity.node_id.clone(), true, message_envelope); + message_data.clone().into_frame_set() + } + + #[test] + fn test_message_queue() { + let context = ZmqContext::new(); + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + + // Create a client that will write message to the inbound message pool + let message_queue_address = InprocAddress::random(); + let client_connection = Connection::new(&context, Direction::Outbound) + .establish(&message_queue_address) + .unwrap(); + + #[derive(Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] + pub enum DomainBrokerType { + Type1, + } + + // Create MessageDispatcher, InboundMessagePublisher, PeerManager, OutboundMessageService and + let message_dispatcher = Arc::new(construct_comms_msg_dispatcher::()); + + const TEST_MESSAGE_COUNT: usize = 3; + let (publisher, subscriber) = pubsub_channel(TEST_MESSAGE_COUNT); + let imp = InboundMessagePublisher::new(publisher); + let mut message_subscription = subscriber.get_subscription(DomainBrokerType::Type1).fuse(); + let inbound_message_publisher = Arc::new(RwLock::new(imp)); + + let (message_sender, _) = channel::unbounded(); + let peer_manager = Arc::new(PeerManager::new(HMapDatabase::new()).unwrap()); + // Add peer to peer manager + let peer = Peer::new( + node_identity.identity.public_key.clone(), + node_identity.identity.node_id.clone(), + "127.0.0.1:9000".parse::().unwrap().into(), + PeerFlags::empty(), + ); + peer_manager.add_peer(peer).unwrap(); + let outbound_message_service = + Arc::new(OutboundMessageService::new(node_identity.clone(), message_sender, peer_manager.clone()).unwrap()); + let ims_config = InboundMessageServiceConfig::default(); + let mut inbound_message_service = InboundMessageService::new( + ims_config, + context, + node_identity.clone(), + message_queue_address, + message_dispatcher, + inbound_message_publisher, + outbound_message_service, + peer_manager, + ); + inbound_message_service.start().unwrap(); + + // Submit Messages to the InboundMessageService + pause(); + let mut message_envelope_body_list = Vec::new(); + for i in 0..TEST_MESSAGE_COUNT { + // Construct a test message + let message_header = MessageHeader::new(DomainBrokerType::Type1).unwrap(); + // Messages with the same message body will be discarded by the DuplicateMsgCache + let message_body = format!("Test Message Body {}", i).to_string().as_bytes().to_vec(); + let message_envelope_body = Message::from_message_format(message_header, message_body).unwrap(); + message_envelope_body_list.push(message_envelope_body.clone()); + let message_data_buffer = create_message_data_buffer(node_identity.clone(), message_envelope_body); + + client_connection.send(&message_data_buffer).unwrap(); + } + + // Check that all messages reached subscribers + std::thread::sleep(Duration::from_millis(1000)); + + let msgs: Vec = block_on(async { + let mut result = Vec::new(); + + loop { + select!( + item = message_subscription.next() => {if let Some(i) = item {result.push(i)}}, + default => break, + ); + } + result + }); + + assert_eq!(msgs.len(), TEST_MESSAGE_COUNT); + for m in msgs.iter() { + assert!(message_envelope_body_list.contains(&m.message)); + } + + // Test shutdown control + let _ = inbound_message_service.shutdown(); + std::thread::sleep(Duration::from_millis(200)); + + let message_header = MessageHeader::new(DomainBrokerType::Type1).unwrap(); + let message_body = "Test Message Body".as_bytes().to_vec(); + let message_envelope_body = Message::from_message_format(message_header, message_body).unwrap(); + let message_data_buffer = create_message_data_buffer(node_identity.clone(), message_envelope_body); + assert!(client_connection.send(&message_data_buffer).is_err()); + } +} diff --git a/comms/src/inbound_message_service/inbound_message_worker.rs b/comms/src/inbound_message_service/inbound_message_worker.rs new file mode 100644 index 0000000000..e6fe0fec36 --- /dev/null +++ b/comms/src/inbound_message_service/inbound_message_worker.rs @@ -0,0 +1,406 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::error::InboundError; +use crate::{ + connection::{ + peer_connection::ControlMessage, + zmq::{InprocAddress, ZmqContext}, + Connection, + ConnectionError, + Direction, + SocketEstablishment, + }, + dispatcher::DispatchableKey, + inbound_message_service::{ + inbound_message_publisher::InboundMessagePublisher, + inbound_message_service::InboundMessageServiceConfig, + MessageCache, + MessageCacheConfig, + }, + message::{Frame, FrameSet, InboundMessage, MessageContext, MessageData}, + outbound_message_service::outbound_message_service::OutboundMessageService, + peer_manager::{peer_manager::PeerManager, NodeId, NodeIdentity, Peer}, + types::MessageDispatcher, +}; +use log::*; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + convert::TryFrom, + fmt::Debug, + sync::{ + mpsc::{sync_channel, Receiver, SyncSender}, + Arc, + RwLock, + }, + thread, +}; + +const LOG_TARGET: &str = "comms::inbound_message_service::worker"; + +/// Set the allocated stack size for the InboundMessageWorker thread +const THREAD_STACK_SIZE: usize = 256 * 1024; // 256kb + +/// The InboundMessageWorker retrieve messages from the inbound message queue, creates a MessageContext for the message +/// that is then dispatch using the dispatcher. +pub struct InboundMessageWorker +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + config: InboundMessageServiceConfig, + context: ZmqContext, + node_identity: Arc, + message_queue_address: InprocAddress, + message_dispatcher: Arc>>, + inbound_message_publisher: Arc>>, + outbound_message_service: Arc, + peer_manager: Arc, + msg_cache: MessageCache, + control_receiver: Option>, + is_running: bool, +} + +impl InboundMessageWorker +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + /// Setup a new InboundMessageWorker that will read incoming messages and dispatch them using the message_dispatcher + /// and inbound_message_broker + pub fn new( + config: InboundMessageServiceConfig, + context: ZmqContext, + node_identity: Arc, + message_queue_address: InprocAddress, + message_dispatcher: Arc>>, + inbound_message_publisher: Arc>>, + outbound_message_service: Arc, + peer_manager: Arc, + ) -> Self + { + InboundMessageWorker { + config, + context, + node_identity, + message_queue_address, + message_dispatcher, + inbound_message_publisher, + outbound_message_service, + peer_manager, + msg_cache: MessageCache::new(MessageCacheConfig::default()), + control_receiver: None, + is_running: false, + } + } + + fn lookup_peer(&self, node_id: &NodeId) -> Option { + self.peer_manager.find_with_node_id(node_id).ok() + } + + // Main loop of worker thread + fn start_worker(&mut self) -> Result<(), InboundError> { + let inbound_connection = Connection::new(&self.context, Direction::Inbound) + .set_socket_establishment(SocketEstablishment::Bind) + .establish(&self.message_queue_address) + .map_err(InboundError::InboundConnectionError)?; + + // Retrieve, process and dispatch messages + loop { + // Check for control messages + self.process_control_messages(); + + if self.is_running { + match inbound_connection.receive(self.config.worker_timeout_in_ms.as_millis() as u32) { + Ok(mut frame_set) => { + // This strips off the two ZeroMQ Identity frames introduced by the transmission to this worker + debug!(target: LOG_TARGET, "Received {} frames", frame_set.len()); + let frame_set: FrameSet = frame_set.drain(1..).collect(); + + match MessageData::try_from(frame_set) { + Ok(message_data) => { + if !self.msg_cache.contains(message_data.message_envelope.body_frame()) { + self.msg_cache + .insert(message_data.message_envelope.body_frame().clone())?; + + let peer = match self.lookup_peer(&message_data.source_node_id) { + Some(peer) => peer, + None => { + warn!( + target: LOG_TARGET, + "Received unknown node id from peer connection. Discarding message \ + from NodeId={:?}", + message_data.source_node_id + ); + continue; + }, + }; + + let message_context = MessageContext::new( + self.node_identity.clone(), + peer, + message_data.forwardable, + message_data.message_envelope, + self.outbound_message_service.clone(), + self.peer_manager.clone(), + self.inbound_message_publisher.clone(), + ); + self.message_dispatcher.dispatch(message_context).unwrap_or_else(|e| { + warn!( + target: LOG_TARGET, + "Could not dispatch message to handler - Error: {:?}", e + ); + }); + } else { + debug!(target: LOG_TARGET, "Duplicate message discarded"); + } + }, + Err(e) => { + // if unable to deserialize the MessageHeader then MUST discard the + // message + warn!( + target: LOG_TARGET, + "Message discarded as it could not be deserialised - Error: {:?}", e + ); + }, + } + }, + Err(ConnectionError::Timeout) => (), + Err(e) => { + error!( + target: LOG_TARGET, + "Error receiving messages from message queue - Error: {}", e + ); + }, + }; + } else { + break; + } + } + Ok(()) + } + + /// Start the InboundMessageWorker thread, setup the control channel and start retrieving and dispatching incoming + /// messages to handlers + pub fn start(mut self) -> Result<(thread::JoinHandle<()>, SyncSender), InboundError> { + self.is_running = true; + let (control_sync_sender, control_receiver) = sync_channel(5); + self.control_receiver = Some(control_receiver); + + let thread_handle = thread::Builder::new() + .name("inbound-message-worker-thread".to_string()) + .stack_size(THREAD_STACK_SIZE) + .spawn(move || match self.start_worker() { + Ok(_) => (), + Err(e) => { + error!(target: LOG_TARGET, "Error starting inbound message worker: {:?}", e); + }, + }) + .map_err(|_| InboundError::ThreadInitializationError)?; + Ok((thread_handle, control_sync_sender)) + } + + /// Check for control messages to manage worker thread + fn process_control_messages(&mut self) { + match &self.control_receiver { + Some(control_receiver) => { + if let Some(control_msg) = control_receiver.recv_timeout(self.config.control_timeout_in_ms).ok() { + debug!(target: LOG_TARGET, "Received control message: {:?}", control_msg); + match control_msg { + ControlMessage::Shutdown => { + info!(target: LOG_TARGET, "Shutting down worker"); + self.is_running = false; + }, + _ => {}, + } + } + }, + None => warn!(target: LOG_TARGET, "Control receive not available for worker"), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + connection::{Connection, Direction, NetAddress}, + futures::StreamExt, + inbound_message_service::comms_msg_handlers::construct_comms_msg_dispatcher, + message::{InboundMessage, Message, MessageEnvelope, MessageFlags, MessageHeader, NodeDestination}, + peer_manager::{peer_manager::PeerManager, NodeIdentity, PeerFlags}, + pub_sub_channel::pubsub_channel, + }; + use crossbeam_channel as channel; + use futures::{executor::block_on, future::select}; + use serde::{Deserialize, Serialize}; + use std::{ + sync::Arc, + time::{self, Duration}, + }; + use tari_storage::HMapDatabase; + use tari_utilities::{message_format::MessageFormat, thread_join::ThreadJoinWithTimeout}; + fn pause() { + thread::sleep(Duration::from_millis(5)); + } + + #[test] + fn test_dispatch_to_multiple_service_handlers() { + let context = ZmqContext::new(); + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + + #[derive(Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] + pub enum DomainBrokerType { + Type1, + Type2, + } + + let message_queue_address = InprocAddress::random(); + let message_dispatcher = Arc::new(construct_comms_msg_dispatcher::()); + + let (publisher, subscriber) = pubsub_channel(10); + let imp = InboundMessagePublisher::new(publisher); + let mut message_subscription_type1 = subscriber.get_subscription(DomainBrokerType::Type1).fuse(); + let mut message_subscription_type2 = subscriber.get_subscription(DomainBrokerType::Type2).fuse(); + let inbound_message_publisher = Arc::new(RwLock::new(imp)); + + let peer_manager = Arc::new(PeerManager::new(HMapDatabase::new()).unwrap()); + // Add peer to peer manager + let peer = Peer::new( + node_identity.identity.public_key.clone(), + node_identity.identity.node_id.clone(), + "127.0.0.1:9000".parse::().unwrap().into(), + PeerFlags::empty(), + ); + let (message_sender, _) = channel::unbounded(); + peer_manager.add_peer(peer).unwrap(); + let outbound_message_service = + Arc::new(OutboundMessageService::new(node_identity.clone(), message_sender, peer_manager.clone()).unwrap()); + let ims_config = InboundMessageServiceConfig::default(); + let worker = InboundMessageWorker::new( + ims_config, + context.clone(), + node_identity.clone(), + message_queue_address.clone(), + message_dispatcher, + inbound_message_publisher, + outbound_message_service, + peer_manager, + ); + let (thread_handle, control_sync_sender) = worker.start().unwrap(); + // Give worker sufficient time to spinup thread and create a socket + std::thread::sleep(time::Duration::from_millis(100)); + + // Create a client that will send messages to the message queue + let client_connection = Connection::new(&context, Direction::Outbound) + .establish(&message_queue_address) + .unwrap(); + + // Construct test message 1 + let message_header = MessageHeader::new(DomainBrokerType::Type1).unwrap(); + let message_body = "Test Message Body1".as_bytes().to_vec(); + let message_envelope_body1 = Message::from_message_format(message_header, message_body).unwrap(); + let dest_public_key = node_identity.identity.public_key.clone(); // Send to self + let message_envelope = MessageEnvelope::construct( + &node_identity, + dest_public_key.clone(), + NodeDestination::Unknown, + message_envelope_body1.to_binary().unwrap(), + MessageFlags::NONE, + ) + .unwrap(); + let message_data1 = MessageData::new( + NodeId::from_key(&node_identity.identity.public_key).unwrap(), + true, + message_envelope, + ); + let mut message1_frame_set = Vec::new(); + message1_frame_set.extend(message_data1.clone().into_frame_set()); + + // Construct test message 2 + let message_header = MessageHeader::new(DomainBrokerType::Type2).unwrap(); + let message_body = "Test Message Body2".as_bytes().to_vec(); + let message_envelope_body2 = Message::from_message_format(message_header, message_body).unwrap(); + let message_envelope = MessageEnvelope::construct( + &node_identity, + dest_public_key.clone(), + NodeDestination::Unknown, + message_envelope_body2.to_binary().unwrap(), + MessageFlags::NONE, + ) + .unwrap(); + let message_data2 = MessageData::new( + NodeId::from_key(&node_identity.identity.public_key).unwrap(), + true, + message_envelope, + ); + let mut message2_frame_set = Vec::new(); + message2_frame_set.extend(message_data2.clone().into_frame_set()); + + // Submit Messages to the Worker + pause(); + client_connection.send(message1_frame_set.clone()).unwrap(); + client_connection.send(message2_frame_set.clone()).unwrap(); + // Send duplicate message + client_connection.send(message2_frame_set).unwrap(); + + // Retrieve messages at handler services + std::thread::sleep(Duration::from_millis(100)); + + let msgs_type1: Vec = block_on(async { + let mut result = Vec::new(); + + loop { + select!( + item = message_subscription_type1.next() => {if let Some(i) = item {result.push(i)}}, + default => break, + ); + } + result + }); + assert_eq!(msgs_type1.len(), 1); + assert_eq!(msgs_type1[0].message, message_envelope_body1); + + let msgs_type2: Vec = block_on(async { + let mut result = Vec::new(); + + loop { + select!( + item = message_subscription_type2.next() => {if let Some(i) = item {result.push(i)}}, + default => break, + ); + } + result + }); + // Should only be 1 message as the duplicate must be rejected + assert_eq!(msgs_type2.len(), 1); + assert_eq!(msgs_type2[0].message, message_envelope_body2); + + // Test worker clean shutdown + control_sync_sender.send(ControlMessage::Shutdown).unwrap(); + std::thread::sleep(time::Duration::from_millis(200)); + let _ = thread_handle.timeout_join(Duration::from_millis(3000)); + assert!(client_connection.send(message1_frame_set).is_err()); + } +} diff --git a/comms/src/inbound_message_service/message_cache.rs b/comms/src/inbound_message_service/message_cache.rs new file mode 100644 index 0000000000..3467efe2a5 --- /dev/null +++ b/comms/src/inbound_message_service/message_cache.rs @@ -0,0 +1,129 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::consts::{DHT_MSG_CACHE_STORAGE_CAPACITY, DHT_MSG_CACHE_TTL}; +use derive_error::Error; +use std::{hash::Hash, time::Duration}; +use ttl_cache::TtlCache; + +#[derive(Debug, Error)] +pub enum MessageCacheError { + /// A duplicate entry existed in the message cache + DuplicateEntry, +} + +#[derive(Clone, Copy)] +pub struct MessageCacheConfig { + /// The maximum number of messages that can be tracked using the MessageCache + storage_capacity: usize, + /// The Time-to-live for each stored message + msg_ttl: Duration, +} + +impl Default for MessageCacheConfig { + fn default() -> Self { + MessageCacheConfig { + storage_capacity: DHT_MSG_CACHE_STORAGE_CAPACITY, + msg_ttl: DHT_MSG_CACHE_TTL, + } + } +} + +/// The MessageCache is used to track handled messages to ensure that processing resources are not wasted on +/// duplicate messages and that these duplicate messages are not sent to services or propagate through the network. +pub struct MessageCache { + config: MessageCacheConfig, + cache: TtlCache, +} + +impl MessageCache +where K: Eq + Hash +{ + /// Create a new MessageCache with the specified configuration + pub fn new(config: MessageCacheConfig) -> Self { + Self { + config, + cache: TtlCache::new(config.storage_capacity), + } + } + + /// Insert a new message into the MessageCache with a time-to-live starting at the insertion time. It will + /// return a DuplicateEntry Error if the message has already been added into the cache. + pub fn insert(&mut self, msg: K) -> Result<(), MessageCacheError> { + match self.cache.insert(msg, (), self.config.msg_ttl) { + Some(_) => Err(MessageCacheError::DuplicateEntry), + None => Ok(()), + } + } + + /// Check if the message is available in the MessageCache + pub fn contains(&self, msg: &K) -> bool { + self.cache.contains_key(msg) + } +} +#[cfg(test)] +mod test { + use super::*; + use std::{thread, time::Duration}; + + #[test] + fn test_msg_rlu_and_ttl() { + let mut msg_cache: MessageCache = MessageCache::new(MessageCacheConfig { + storage_capacity: 3, + msg_ttl: Duration::from_millis(100), + }); + let msg1 = "msg1".to_string(); + let msg2 = "msg2".to_string(); + let msg3 = "msg3".to_string(); + let msg4 = "msg4".to_string(); + + msg_cache.insert(msg1.clone()).unwrap(); + assert!(msg_cache.contains(&msg1)); + assert!(!msg_cache.contains(&msg2)); + + msg_cache.insert(msg2.clone()).unwrap(); + assert!(msg_cache.contains(&msg1)); + assert!(msg_cache.contains(&msg2)); + assert!(!msg_cache.contains(&msg3)); + + msg_cache.insert(msg3.clone()).unwrap(); + assert!(msg_cache.contains(&msg1)); + assert!(msg_cache.contains(&msg2)); + assert!(msg_cache.contains(&msg3)); + assert!(!msg_cache.contains(&msg4)); + + thread::sleep(Duration::from_millis(50)); + msg_cache.insert(msg4.clone()).unwrap(); + + // Due to storage limits, msg1 was removed when msg4 was added + assert!(!msg_cache.contains(&msg1)); + assert!(msg_cache.contains(&msg2)); + assert!(msg_cache.contains(&msg3)); + assert!(msg_cache.contains(&msg4)); + + // msg2 and msg3 would have reached their ttl thresholds + thread::sleep(Duration::from_millis(51)); + assert!(!msg_cache.contains(&msg2)); + assert!(!msg_cache.contains(&msg3)); + assert!(msg_cache.contains(&msg4)); + } +} diff --git a/comms/src/inbound_message_service/mod.rs b/comms/src/inbound_message_service/mod.rs new file mode 100644 index 0000000000..02ae58ec4b --- /dev/null +++ b/comms/src/inbound_message_service/mod.rs @@ -0,0 +1,53 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! # Inbound Message Service +//! +//! The inbound message service is responsible for receiving messages from the active [PeerConnection]s +//! and fair-dealing them to one of the worker threads for processing. +//! +//! Worker thread will perform the following tasks: +//! +//! 1. Validate the message signature against the sender's public key. +//! 2. Attempt to decrypt the message with a ECDH shared secret if the MessageFlags::ENCRYPTED flag is set. +//! 3. Check the destination [NodeId] or [CommsPublicKey] +//! 3. Should steps 1-3 fail, forward or discard the message as necessary. See [comms_msg_handlers]. +//! 4. Otherwise, dispatch the message to one of the configured message broker routes. See [InboundMessageBroker] +//! +//! [PeerConnection]: ../connection/peer_connection/index.html +//! [comms_msg_handlers]: ./comms_msg_handlers/struct.InboundMessageServiceResolver.html +//! [InboundMessageBroker]: ./inbound_message_broker/struct.InboundMessageBroker.html +pub mod comms_msg_handlers; +pub mod error; +pub mod inbound_message_publisher; +pub mod inbound_message_service; +pub mod inbound_message_worker; +pub mod message_cache; + +use crate::{message::InboundMessage, pub_sub_channel::TopicSubscriptionFactory}; + +pub use self::{ + error::InboundError, + message_cache::{MessageCache, MessageCacheConfig}, +}; + +pub type InboundTopicSubscriptionFactory = TopicSubscriptionFactory; diff --git a/comms/src/lib.rs b/comms/src/lib.rs new file mode 100644 index 0000000000..729bcdb263 --- /dev/null +++ b/comms/src/lib.rs @@ -0,0 +1,35 @@ +//! # Tari Comms +//! +//! The Tari network messaging library. +//! +//! See [CommsBuilder] for more information on using this library. +//! +//! [CommsBuilder]: ./builder/index.html +#![feature(checked_duration_since)] + +#[macro_use] +extern crate futures; + +#[macro_use] +extern crate lazy_static; + +#[macro_use] +mod macros; + +pub mod builder; +#[macro_use] +pub mod connection; +pub mod connection_manager; +mod consts; +pub mod control_service; +pub mod dispatcher; +pub mod domain_subscriber; +pub mod inbound_message_service; +pub mod message; +pub mod outbound_message_service; +pub mod peer_manager; +pub mod pub_sub_channel; +pub mod types; +mod utils; + +pub use self::builder::CommsBuilder; diff --git a/comms/src/macros.rs b/comms/src/macros.rs new file mode 100644 index 0000000000..baac40831e --- /dev/null +++ b/comms/src/macros.rs @@ -0,0 +1,61 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// Creates a setter function used with the builder pattern +macro_rules! setter { + ($func:ident, $name: ident, Option<$type: ty>) => { + pub fn $func(mut self, val: $type) -> Self { + self.$name = Some(val); + self + } + }; + ($func:ident, $name: ident, $type: ty) => { + pub fn $func(mut self, val: $type) -> Self { + self.$name = val; + self + } + }; +} + +macro_rules! acquire_lock { + ($e:expr, $m:ident) => { + match $e.$m() { + Ok(lock) => lock, + Err(poisoned) => poisoned.into_inner(), + } + }; + ($e:expr) => { + acquire_lock!($e, lock) + }; +} + +macro_rules! acquire_write_lock { + ($e:expr) => { + acquire_lock!($e, write) + }; +} + +macro_rules! acquire_read_lock { + ($e:expr) => { + acquire_lock!($e, read) + }; +} diff --git a/comms/src/message/domain_message_context.rs b/comms/src/message/domain_message_context.rs new file mode 100644 index 0000000000..5d1a47f335 --- /dev/null +++ b/comms/src/message/domain_message_context.rs @@ -0,0 +1,45 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{message::message::Message, peer_manager::PeerNodeIdentity, types::CommsPublicKey}; +use serde::{Deserialize, Serialize}; + +/// The InboundMessage is the container that will be dispatched to the domain handlers. It contains the received +/// message and source identity after the comms level envelope has been removed. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InboundMessage { + pub peer_source: PeerNodeIdentity, + pub origin_source: CommsPublicKey, + pub message: Message, +} + +impl InboundMessage { + /// Construct a new InboundMessage that consist of the peer connection information and the received message + /// header and body + pub fn new(peer_source: PeerNodeIdentity, origin_source: CommsPublicKey, message: Message) -> Self { + InboundMessage { + peer_source, + origin_source, + message, + } + } +} diff --git a/comms/src/message/envelope.rs b/comms/src/message/envelope.rs new file mode 100644 index 0000000000..bcf45460ad --- /dev/null +++ b/comms/src/message/envelope.rs @@ -0,0 +1,455 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::Message; +use crate::{ + message::{error::MessageError, Frame, FrameSet, MessageFlags, NodeDestination}, + peer_manager::NodeIdentity, + types::{CommsCipher, CommsPublicKey, MESSAGE_PROTOCOL_VERSION, WIRE_PROTOCOL_VERSION}, + utils::crypto, +}; +use rand::OsRng; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use tari_crypto::keys::{DiffieHellmanSharedSecret, PublicKey}; +use tari_utilities::{ciphers::cipher::Cipher, message_format::MessageFormat}; + +const FRAMES_PER_MESSAGE: usize = 3; + +/// Generate the challenge for the origin signature +fn origin_challenge(dest: NodeDestination, mut body: Vec) -> Result, MessageError> { + let mut challenge = dest.to_binary().map_err(MessageError::MessageFormatError)?; + challenge.append(&mut body); + Ok(challenge) +} + +/// Generate the challenge for the peer signature +fn peer_challenge(origin_signature: Vec, mut body: Vec) -> Result, MessageError> { + let mut challenge = origin_signature; + challenge.append(&mut body); + Ok(challenge) +} + +/// Generate a signature for the origin that confirms the dest and body +fn origin_signature( + node_identity: &NodeIdentity, + dest: NodeDestination, + body: Vec, +) -> Result, MessageError> +{ + let origin_signature = crypto::sign( + &mut OsRng::new().unwrap(), + node_identity.secret_key.clone(), + &origin_challenge(dest, body)?, + ) + .map_err(MessageError::SchnorrSignatureError)?; + origin_signature.to_binary().map_err(MessageError::MessageFormatError) +} + +/// Generate a signature for the peer that confirms the origin_source and body +fn peer_signature( + node_identity: &NodeIdentity, + origin_signature: Vec, + body: Vec, +) -> Result, MessageError> +{ + let peer_signature = crypto::sign( + &mut OsRng::new().unwrap(), + node_identity.secret_key.clone(), + &peer_challenge(origin_signature, body)?, + ) + .map_err(MessageError::SchnorrSignatureError)?; + peer_signature.to_binary().map_err(MessageError::MessageFormatError) +} + +/// Represents data that every message contains. +/// As described in [RFC-0172](https://rfc.tari.com/RFC-0172_PeerToPeerMessagingProtocol.html#messaging-structure) +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct MessageEnvelopeHeader { + pub version: u8, + pub origin_source: CommsPublicKey, + pub peer_source: CommsPublicKey, + pub dest: NodeDestination, + pub origin_signature: Vec, + pub peer_signature: Vec, + pub flags: MessageFlags, +} + +impl MessageEnvelopeHeader { + /// Verify that the signature provided is valid for the given body + pub fn verify_signatures(&self, body: Frame) -> Result { + let origin_verif = crypto::verify( + &self.origin_source, + self.origin_signature.as_slice(), + origin_challenge(self.dest.clone(), body.clone())?, + )?; + let peer_verif = crypto::verify( + &self.peer_source, + self.peer_signature.as_slice(), + peer_challenge(self.origin_signature.clone(), body)?, + )?; + Ok(origin_verif & peer_verif) + } +} + +/// Represents a message which is about to go on or has just come off the wire. +/// As described in [RFC-0172](https://rfc.tari.com/RFC-0172_PeerToPeerMessagingProtocol.html#messaging-structure) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageEnvelope { + frames: FrameSet, +} + +impl MessageEnvelope { + /// Create a new MessageEnvelope from four frames + pub fn new(version: Frame, header: Frame, body: Frame) -> Self { + MessageEnvelope { + frames: vec![version, header, body], + } + } + + /// Sign a message, construct a MessageEnvelopeHeader and return the resulting MessageEnvelope + pub fn construct( + node_identity: &NodeIdentity, + dest_public_key: CommsPublicKey, + dest: NodeDestination, + mut body: Frame, + flags: MessageFlags, + ) -> Result + { + if flags.contains(MessageFlags::ENCRYPTED) { + body = encrypt_envelope_body(&node_identity.secret_key, &dest_public_key, &body)? + } + + let origin_signature = origin_signature(node_identity, dest.clone(), body.clone())?; + let peer_signature = peer_signature(node_identity, origin_signature.clone(), body.clone())?; + + let header = MessageEnvelopeHeader { + version: MESSAGE_PROTOCOL_VERSION, + origin_source: node_identity.identity.public_key.clone(), + peer_source: node_identity.identity.public_key.clone(), + dest, + origin_signature, + peer_signature, + flags, + }; + + Ok(Self::new( + vec![WIRE_PROTOCOL_VERSION], + header.to_binary().map_err(MessageError::MessageFormatError)?, + body, + )) + } + + /// Modify and sign a forwarded MessageEnvelope + pub fn forward_construct( + node_identity: &NodeIdentity, + message_envelope: MessageEnvelope, + ) -> Result + { + let mut message_envelope_header = message_envelope.deserialize_header()?; + message_envelope_header.peer_source = node_identity.identity.public_key.clone(); + message_envelope_header.peer_signature = peer_signature( + node_identity, + message_envelope_header.origin_signature.clone(), + message_envelope.body_frame().clone(), + )?; + + Ok(Self::new( + vec![WIRE_PROTOCOL_VERSION], + message_envelope_header + .to_binary() + .map_err(MessageError::MessageFormatError)?, + message_envelope.body_frame().clone(), + )) + } + + /// Returns the frame that is expected to be version frame + pub fn version_frame(&self) -> &Frame { + &self.frames[0] + } + + /// Returns the frame that is expected to be header frame + pub fn header_frame(&self) -> &Frame { + &self.frames[1] + } + + /// Returns the [MessageEnvelopeHeader] deserialized from the header frame + pub fn deserialize_header(&self) -> Result { + MessageEnvelopeHeader::from_binary(self.header_frame()).map_err(Into::into) + } + + /// Returns the frame that is expected to be body frame + pub fn body_frame(&self) -> &Frame { + &self.frames[2] + } + + /// Returns the frame that is expected to be body frame + pub fn decrypted_body_frame( + &self, + dest_secret_key: &PK::K, + source_public_key: &PK, + ) -> Result + where + PK: PublicKey + DiffieHellmanSharedSecret, + { + let ecdh_shared_secret = PK::shared_secret(dest_secret_key, source_public_key).to_vec(); + let decrypted_frame: Frame = CommsCipher::open_with_integral_nonce(self.body_frame(), &ecdh_shared_secret) + .map_err(MessageError::CipherError)?; + Ok(decrypted_frame) + } + + /// Returns the Message deserialized from the body frame + pub fn deserialize_body(&self) -> Result { + Message::from_binary(self.body_frame()).map_err(Into::into) + } + + /// Returns the decrypted and deserialized Message from the body frame + pub fn deserialize_encrypted_body( + &self, + dest_secret_key: &PK::K, + source_public_key: &PK, + ) -> Result + where + PK: PublicKey + DiffieHellmanSharedSecret, + { + Message::from_binary(&self.decrypted_body_frame(dest_secret_key, source_public_key)?).map_err(Into::into) + } + + /// This struct is consumed and the contained FrameSet is returned. + pub fn into_frame_set(self) -> FrameSet { + self.frames + } +} + +impl TryFrom for MessageEnvelope { + type Error = MessageError; + + /// Returns a MessageEnvelope from a FrameSet + fn try_from(frames: FrameSet) -> Result { + if frames.len() != FRAMES_PER_MESSAGE { + return Err(MessageError::MalformedMultipart); + } + + Ok(MessageEnvelope { frames }) + } +} + +/// Encrypt the message_envelope_body with the generated shared secret +fn encrypt_envelope_body( + source_secret_key: &PK::K, + dest_public_key: &PK, + message_body: &Frame, +) -> Result +where + PK: PublicKey + DiffieHellmanSharedSecret, +{ + let ecdh_shared_secret = PK::shared_secret(source_secret_key, dest_public_key).to_vec(); + CommsCipher::seal_with_integral_nonce(message_body, &ecdh_shared_secret).map_err(MessageError::CipherError) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::message::{MessageEnvelopeHeader, MessageFlags, NodeDestination}; + use rand; + use std::{convert::TryInto, sync::Arc}; + use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + use tari_utilities::hex::to_hex; + + #[test] + fn try_from_valid() { + let example = vec![vec![1u8], vec![2u8], vec![3u8]]; + + let raw_message: Result = example.try_into(); + + assert!(raw_message.is_ok()); + let envelope = raw_message.unwrap(); + assert_eq!(envelope.version_frame(), &[1u8]); + assert_eq!(envelope.header_frame(), &[2u8]); + assert_eq!(envelope.body_frame(), &[3u8]); + } + + #[test] + fn try_from_invalid() { + let example = vec![vec![1u8], vec![2u8]]; + + let raw_message: Result = example.try_into(); + + assert!(raw_message.is_err()); + let error = raw_message.err().unwrap(); + match error { + MessageError::MalformedMultipart => {}, + _ => panic!("Unexpected MessageError {:?}", error), + } + } + + #[test] + fn header() { + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut rand::OsRng::new().unwrap()); + let header = MessageEnvelopeHeader { + version: 0, + origin_source: pk.clone(), + peer_source: pk, + dest: NodeDestination::Unknown, + origin_signature: vec![0], + peer_signature: vec![0], + flags: MessageFlags::ENCRYPTED, + }; + + let envelope = MessageEnvelope::new(vec![0u8], header.to_binary().unwrap(), vec![0u8]); + + assert_eq!(header, envelope.deserialize_header().unwrap()); + } + + fn make_test_message_frame() -> Frame { + let message_header = "Test Message Header".as_bytes().to_vec(); + let message_body = "Test Message Body".as_bytes().to_vec(); + let message_envelope_body = Message::from_message_format(message_header, message_body).unwrap(); + message_envelope_body.to_binary().unwrap() + } + + #[test] + fn construct() { + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let dest_public_key = &node_identity.identity.public_key; + + let message_envelope_body_frame = make_test_message_frame(); + + let envelope = MessageEnvelope::construct( + &node_identity, + dest_public_key.clone(), + NodeDestination::Unknown, + message_envelope_body_frame.clone(), + MessageFlags::NONE, + ) + .unwrap(); + assert_eq!("00", to_hex(envelope.version_frame())); + let header = MessageEnvelopeHeader::from_binary(envelope.header_frame()).unwrap(); + assert_eq!(dest_public_key, &header.origin_source); + assert_eq!(MessageFlags::NONE, header.flags); + assert_eq!(NodeDestination::Unknown, header.dest); + assert!(!header.origin_signature.is_empty()); + assert_eq!(&message_envelope_body_frame, envelope.body_frame()); + } + + #[test] + fn forward_construct() { + let origin_node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let peer_node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let dest_node_identity = Arc::new(NodeIdentity::random_for_test(None)); + + // Original MessageEnvelope + let message_envelope_body_frame = make_test_message_frame(); + let origin_envelope = MessageEnvelope::construct( + &origin_node_identity, + dest_node_identity.identity.public_key.clone(), + NodeDestination::Unknown, + message_envelope_body_frame.clone(), + MessageFlags::ENCRYPTED, + ) + .unwrap(); + + // Forwarded MessageEnvelope + let peer_envelope = MessageEnvelope::forward_construct(&peer_node_identity, origin_envelope).unwrap(); + let peer_header = MessageEnvelopeHeader::from_binary(peer_envelope.header_frame()).unwrap(); + + assert_eq!(peer_header.origin_source, origin_node_identity.identity.public_key); + assert_eq!(peer_header.peer_source, peer_node_identity.identity.public_key); + assert_eq!( + peer_envelope + .decrypted_body_frame( + &dest_node_identity.secret_key, + &origin_node_identity.identity.public_key + ) + .unwrap(), + message_envelope_body_frame + ); + assert!(peer_header + .verify_signatures(peer_envelope.body_frame().clone()) + .unwrap()); + } + + #[test] + fn construct_encrypted() { + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let dest_public_key = &node_identity.identity.public_key; + + let message_envelope_body_frame = make_test_message_frame(); + let envelope = MessageEnvelope::construct( + &node_identity, + dest_public_key.clone(), + NodeDestination::Unknown, + message_envelope_body_frame.clone(), + MessageFlags::ENCRYPTED, + ) + .unwrap(); + + assert_ne!(&message_envelope_body_frame, envelope.body_frame()); + } + + #[test] + fn envelope_decrypt_message_body() { + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let dest_secret_key = node_identity.secret_key.clone(); + let dest_public_key = &node_identity.identity.public_key; + + let message_envelope_body_frame = make_test_message_frame(); + let envelope = MessageEnvelope::construct( + &node_identity, + dest_public_key.clone(), + NodeDestination::Unknown, + message_envelope_body_frame.clone(), + MessageFlags::ENCRYPTED, + ) + .unwrap(); + + assert_eq!( + envelope + .deserialize_encrypted_body(&dest_secret_key, &node_identity.identity.public_key) + .unwrap(), + Message::from_binary(&message_envelope_body_frame).unwrap() + ); + } + + #[test] + fn message_header_verify_signature() { + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let dest_public_key = &node_identity.identity.public_key; + + let message_envelope_body_frame = make_test_message_frame(); + let envelope = MessageEnvelope::construct( + &node_identity, + dest_public_key.clone(), + NodeDestination::Unknown, + message_envelope_body_frame.clone(), + MessageFlags::NONE, + ) + .unwrap(); + + let header = envelope.deserialize_header().unwrap(); + let mut body = envelope.body_frame().clone(); + assert!(header.verify_signatures(body.clone()).unwrap()); + + body.push(0); + assert!(!header.verify_signatures(body.clone()).unwrap()); + } +} diff --git a/comms/src/message/error.rs b/comms/src/message/error.rs new file mode 100644 index 0000000000..b130820f20 --- /dev/null +++ b/comms/src/message/error.rs @@ -0,0 +1,47 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::peer_manager::node_id::NodeIdError; +use derive_error::Error; +use tari_crypto::signatures::SchnorrSignatureError; +use tari_utilities::{ciphers::cipher::CipherError, message_format::MessageFormatError}; + +#[derive(Error, Debug)] +pub enum MessageError { + /// Multipart message is malformed + MalformedMultipart, + /// Failed to serialize message + SerializeFailed, + /// Failed to deserialize message + DeserializeFailed, + /// An error occurred serialising an object into binary + BinarySerializeError, + /// An error occurred deserialising binary data into an object + BinaryDeserializeError, + MessageFormatError(MessageFormatError), + SchnorrSignatureError(SchnorrSignatureError), + /// Failed to Encode or Decode the message using the Cipher + CipherError(CipherError), + NodeIdError(NodeIdError), + /// Problem initializing the RNG + RngError, +} diff --git a/comms/src/message/message.rs b/comms/src/message/message.rs new file mode 100644 index 0000000000..6dc2ab0a2a --- /dev/null +++ b/comms/src/message/message.rs @@ -0,0 +1,124 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + message::{Frame, MessageError}, + types::CommsRng, +}; +use rand::RngCore; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::convert::TryFrom; +use tari_utilities::message_format::MessageFormat; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MessageHeader { + pub message_type: MType, + pub nonce: u64, +} + +impl MessageHeader { + /// Create a new MessageHeader with a random nonce + pub fn new(message_type: MType) -> Result { + Ok(Self { + message_type, + nonce: CommsRng::new().map_err(|_| MessageError::RngError)?.next_u64(), + }) + } +} + +/// Represents a Message as described in [RFC-0172](https://rfc.tari.com/RFC-0172_PeerToPeerMessagingProtocol.html#messaging-structure). +/// This message has been decrypted but the contents are still serialized +/// as described in [RFC-0171](https://rfc.tari.com/RFC-0171_MessageSerialisation.html) +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Message { + pub header: Frame, + pub body: Frame, +} + +impl Message { + /// Create a new Message from two MessageFormat types + pub fn from_message_format(header: H, msg: B) -> Result { + let header_frame = header.to_binary()?; + let body_frame = msg.to_binary()?; + Ok(Self { + header: header_frame, + body: body_frame, + }) + } + + /// Deserialize and return the header of the message + pub fn deserialize_header(&self) -> Result, MessageError> + where + MessageHeader: MessageFormat, + MType: DeserializeOwned, + { + MessageHeader::::from_binary(&self.header).map_err(Into::into) + } + + pub fn deserialize_message(&self) -> Result + where T: MessageFormat { + T::from_binary(&self.body).map_err(Into::into) + } +} + +impl TryFrom<(MType, T)> for Message +where + MessageHeader: MessageFormat, + T: MessageFormat, +{ + type Error = MessageError; + + fn try_from((message_type, msg): (MType, T)) -> Result { + Ok(Self::from_message_format(MessageHeader::new(message_type)?, msg)?) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] + struct TestHeader { + a: u32, + b: u64, + } + + #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] + struct TestMsg { + a: u32, + } + + #[test] + fn from_message_format() { + let header = TestHeader { a: 1, b: 2 }; + let body = TestMsg { a: 2 }; + + let msg = Message::from_message_format(header.clone(), body.clone()).unwrap(); + + let header2 = msg.deserialize_header::().unwrap(); + let body2 = msg.deserialize_message::().unwrap(); + + assert_eq!(header.a, header2.message_type); + assert_eq!(header.b, header2.nonce); + assert_eq!(body, body2); + } +} diff --git a/comms/src/message/message_context.rs b/comms/src/message/message_context.rs new file mode 100644 index 0000000000..b1eaeddde6 --- /dev/null +++ b/comms/src/message/message_context.rs @@ -0,0 +1,76 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use crate::{ + dispatcher::DispatchableKey, + inbound_message_service::inbound_message_publisher::InboundMessagePublisher, + message::{InboundMessage, MessageEnvelope}, + outbound_message_service::outbound_message_service::OutboundMessageService, + peer_manager::{peer_manager::PeerManager, NodeIdentity, Peer}, +}; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + fmt::Debug, + sync::{Arc, RwLock}, +}; + +#[derive(Clone)] +pub struct MessageContext +where MType: Send + Sync + Debug +{ + pub forwardable: bool, + pub message_envelope: MessageEnvelope, + pub peer: Peer, + pub outbound_message_service: Arc, + pub peer_manager: Arc, + pub inbound_message_publisher: Arc>>, + pub node_identity: Arc, +} + +impl MessageContext +where + MType: DispatchableKey, + MType: Serialize + DeserializeOwned, + MType: Debug, +{ + /// Construct a new MessageContext that consist of the peer connection information and the received message header + /// and body + pub fn new( + node_identity: Arc, + peer: Peer, + forwardable: bool, + message_envelope: MessageEnvelope, + outbound_message_service: Arc, + peer_manager: Arc, + inbound_message_publisher: Arc>>, + ) -> Self + { + MessageContext { + forwardable, + message_envelope, + peer, + node_identity, + outbound_message_service, + peer_manager, + inbound_message_publisher, + } + } +} diff --git a/comms/src/message/message_data.rs b/comms/src/message/message_data.rs new file mode 100644 index 0000000000..e8ed13adaa --- /dev/null +++ b/comms/src/message/message_data.rs @@ -0,0 +1,102 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + message::{FrameSet, MessageEnvelope, MessageError}, + peer_manager::NodeId, +}; +use serde_derive::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; +use tari_utilities::message_format::MessageFormat; + +/// Messages submitted to the inbound message pool are of type MessageData. This struct contains the received message +/// envelope from a peer, its node identity and the connection id associated with the received message. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct MessageData { + pub source_node_id: NodeId, + pub forwardable: bool, + pub message_envelope: MessageEnvelope, +} + +impl MessageData { + /// Construct a new MessageData that consist of the peer connection information and the received message envelope + /// header and body + pub fn new(source_node_id: NodeId, forwardable: bool, message_envelope: MessageEnvelope) -> MessageData { + MessageData { + source_node_id, + forwardable, + message_envelope, + } + } + + /// Convert the MessageData into a FrameSet + pub fn into_frame_set(self) -> FrameSet { + let mut frame_set = Vec::new(); + frame_set.push(self.source_node_id.as_ref().to_vec()); + frame_set.extend(self.forwardable.to_binary()); + frame_set.extend(self.message_envelope.into_frame_set()); + frame_set + } +} + +impl TryFrom for MessageData { + type Error = MessageError; + + /// Attempt to create a MessageData from a FrameSet + fn try_from(mut frames: FrameSet) -> Result { + if frames.len() < 5 { + return Err(MessageError::MalformedMultipart); + }; + let source_node_id: NodeId = frames.remove(0).try_into().map_err(MessageError::NodeIdError)?; + let forwardable = bool::from_binary(&frames.remove(0))?; + let message_envelope: MessageEnvelope = frames.try_into()?; + Ok(MessageData { + message_envelope, + forwardable, + source_node_id, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::message::Frame; + use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + + #[test] + fn test_try_from_and_into() { + let mut rng = rand::OsRng::new().unwrap(); + let (_, source_node_identity) = RistrettoPublicKey::random_keypair(&mut rng); + let version_frame: Frame = vec![10]; + let header_frame: Frame = vec![0, 1, 2, 3, 4]; + let body_frame: Frame = vec![5, 6, 7, 8, 9]; + let message_envelope = MessageEnvelope::new(version_frame, header_frame, body_frame); + let expected_message_data = + MessageData::new(NodeId::from_key(&source_node_identity).unwrap(), true, message_envelope); + // Convert MessageData to FrameSet + let message_data_buffer = expected_message_data.clone().into_frame_set(); + // Create MessageData from FrameSet + let message_data: MessageData = MessageData::try_from(message_data_buffer).unwrap(); + assert_eq!(expected_message_data, message_data); + } +} diff --git a/comms/src/message/mod.rs b/comms/src/message/mod.rs new file mode 100644 index 0000000000..e60481baad --- /dev/null +++ b/comms/src/message/mod.rs @@ -0,0 +1,106 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! # Message +//! +//! The message module contains the message types which wrap domain-level messages. +//! +//! Described further in [RFC-0172](https://rfc.tari.com/RFC-0172_PeerToPeerMessagingProtocol.html#messaging-structure) +//! +//! - [Frame] and [FrameSet] +//! +//! A [FrameSet] consists of multiple [Frame]s. A [Frame] is the raw byte representation of a message. +//! +//! - [MessageEnvelope] +//! +//! Represents data that is about to go on the wire or has just come off. +//! +//! - [MessageEnvelopeHeader] +//! +//! The header that every message contains. +//! +//! - [Message] +//! +//! This message is deserialized from the body [Frame] of the [MessageEnvelope]. +//! It consists of a [MessageHeader] and a domain-level body [Frame]. +//! This part of the [MessageEnvelope] can optionally be encrypted for a particular peer. +//! +//! - [MessageHeader] +//! +//! Information about the contained message. Currently, this only contains the +//! domain-level message type. +//! +//! - [MessageData] +//! +//! This message is dispatched by the [InboundMessageBroker] to a [DomainConnector]. +//! +//! [Frame]: ./tyoe.Frame.html +//! [FrameSet]: ./tyoe.FrameSet.html +//! [MessageEnvelope]: ./envelope/struct.MessageEnvelope.html +//! [MessageEnvelopeHeader]: ./envelope/struct.MessageEnvelopeHeader.html +//! [Message]: ./message/struct.Message.html +//! [MessageHeader]: ./message/struct.MessageHeader.html +//! [MessageData]: ./message/struct.MessageData.html +//! [InboundMessageBroker]: ../inbound_message_service/inbound_message_broker/struct.InboundMessageBroker.html +//! [DomainConnector]: ../domain_connector/struct.DomainConnector.html +use crate::peer_manager::node_id::NodeId; +use bitflags::*; +use serde::{Deserialize, Serialize}; + +mod domain_message_context; +mod envelope; +mod error; +mod message; +mod message_context; +mod message_data; + +pub use self::{ + domain_message_context::*, + envelope::{MessageEnvelope, MessageEnvelopeHeader}, + error::MessageError, + message::{Message, MessageHeader}, + message_context::MessageContext, + message_data::*, +}; + +/// Represents a single message frame. +pub type Frame = Vec; +/// Represents a collection of frames which make up a multipart message. +pub type FrameSet = Vec; + +bitflags! { + /// Used to indicate characteristics of the incoming or outgoing message, such + /// as whether the message is encrypted. + #[derive(Deserialize, Serialize)] + pub struct MessageFlags: u8 { + const NONE = 0b0000_0000; + const ENCRYPTED = 0b0000_0001; + } +} + +/// Represents the ways a destination node can be represented. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub enum NodeDestination

{ + Unknown, + PublicKey(P), + NodeId(NodeId), +} diff --git a/comms/src/outbound_message_service/broadcast_strategy.rs b/comms/src/outbound_message_service/broadcast_strategy.rs new file mode 100644 index 0000000000..da26f6fccd --- /dev/null +++ b/comms/src/outbound_message_service/broadcast_strategy.rs @@ -0,0 +1,130 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + consts::DHT_FORWARD_NODE_COUNT, + message::NodeDestination, + peer_manager::{node_id::NodeId, peer_manager::PeerManager, PeerManagerError}, + types::CommsPublicKey, +}; +use derive_error::Error; +use std::sync::Arc; + +#[derive(Debug, Error)] +pub enum BroadcastStrategyError { + PeerManagerError(PeerManagerError), +} + +#[derive(Debug)] +pub struct ClosestRequest { + pub n: usize, + pub node_id: NodeId, + pub excluded_peers: Vec, +} + +#[derive(Debug)] +pub enum BroadcastStrategy { + /// Send to a particular peer matching the given node ID + DirectNodeId(NodeId), + /// Send to a particular peer matching the given Public Key + DirectPublicKey(CommsPublicKey), + /// Send to all known Communication Node peers + Flood, + /// Send to all n nearest neighbour Communication Nodes + Closest(ClosestRequest), + /// Send to a random set of peers of size n that are Communication Nodes + Random(usize), +} + +impl BroadcastStrategy { + /// The forward function selects the most appropriate broadcast strategy based on the received messages destination + pub fn forward( + source_node_id: NodeId, + peer_manager: &Arc, + header_dest: NodeDestination, + excluded_peers: Vec, + ) -> Result + { + Ok(match header_dest { + NodeDestination::Unknown => { + // Send to the current nodes nearest neighbours + BroadcastStrategy::Closest(ClosestRequest { + n: DHT_FORWARD_NODE_COUNT, + node_id: source_node_id, + excluded_peers, + }) + }, + NodeDestination::PublicKey(dest_public_key) => { + if peer_manager.exists(&dest_public_key)? { + // Send to destination peer directly if the current node knows that peer + BroadcastStrategy::DirectPublicKey(dest_public_key) + } else { + // Send to the current nodes nearest neighbours + BroadcastStrategy::Closest(ClosestRequest { + n: DHT_FORWARD_NODE_COUNT, + node_id: source_node_id, + excluded_peers, + }) + } + }, + NodeDestination::NodeId(dest_node_id) => { + match peer_manager.find_with_node_id(&dest_node_id) { + Ok(dest_peer) => { + // Send to destination peer directly if the current node knows that peer + BroadcastStrategy::DirectPublicKey(dest_peer.public_key) + }, + Err(_) => { + // Send to peers that are closest to the destination network region + BroadcastStrategy::Closest(ClosestRequest { + n: DHT_FORWARD_NODE_COUNT, + node_id: dest_node_id, + excluded_peers, + }) + }, + } + }, + }) + } + + /// The discover function selects an appropriate broadcast strategy for the discovery of a specific node + pub fn discover( + source_node_id: NodeId, + dest_node_id: Option, + header_dest: NodeDestination, + excluded_peers: Vec, + ) -> Self + { + let network_location_node_id = match dest_node_id { + Some(node_id) => node_id, + None => match header_dest.clone() { + NodeDestination::Unknown => source_node_id, + NodeDestination::PublicKey(_) => source_node_id, + NodeDestination::NodeId(node_id) => node_id, + }, + }; + BroadcastStrategy::Closest(ClosestRequest { + n: DHT_FORWARD_NODE_COUNT, + node_id: network_location_node_id, + excluded_peers, + }) + } +} diff --git a/comms/src/outbound_message_service/error.rs b/comms/src/outbound_message_service/error.rs new file mode 100644 index 0000000000..5a7cfc6d11 --- /dev/null +++ b/comms/src/outbound_message_service/error.rs @@ -0,0 +1,42 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE + +use crate::{message::MessageError, peer_manager::PeerManagerError}; +use derive_error::Error; +use tari_utilities::{message_format::MessageFormatError, thread_join::ThreadError}; + +/// Error type for OutboundMessageService subsystem +#[derive(Debug, Error)] +pub enum OutboundError { + /// The message could not be serialized + MessageSerializationError(MessageError), + /// Error during serialization or deserialization + MessageFormatError(MessageFormatError), + /// Problem encountered with Broadcast Strategy and PeerManager + PeerManagerError(PeerManagerError), + #[error(msg_embedded, non_std, no_from)] + ShutdownSignalSendError(String), + /// Timeout exceeded while waiting for worker threads to complete + ThreadJoinError(ThreadError), + /// Failed to send message on message sink + SyncSenderError, +} diff --git a/comms/src/outbound_message_service/mod.rs b/comms/src/outbound_message_service/mod.rs new file mode 100644 index 0000000000..8726e53ef3 --- /dev/null +++ b/comms/src/outbound_message_service/mod.rs @@ -0,0 +1,68 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! # Outbound Message Service (OMS) +//! +//! Responsible for sending messages on the peer-to-peer network. +//! +//! In order to send a message the OMS: +//! +//! - evaluates and selects [Peer]'s according to the given [BroadcastStrategy], +//! - constructs, signs and optionally encrypts a [MessageEnvelope] for each selected [Peer], and +//! - forwards each constructed message frame to the [OutboundMessagePool] (OMP). +//! +//! # Broadcast Strategy +//! +//! Represents a strategy for selecting known [Peer]s from the [PeerManager]. +//! See [BroadcastStrategy] for more details. +//! +//! # Outbound Message Pool (OMP) +//! +//! Responsible for reliably sending messages to [Peer]s. +//! +//! The OMP reads from an [0MQ inproc] message queue. Each message received on this queue represents +//! a message which should be delivered to a single peer. A message is fair-dealt to a worker for +//! processing. The worker thread attempts to establish a [PeerConnection] to the given [Peer] +//! using the [ConnectionManager]. Once established, it uses the connection to send the +//! message. Once sent, it discards the message. If, for whatever reason, the message fails +//! to send, the message will be requeued and will try again later. If the message fails after +//! a configured number of attempts, the message is discarded. +//! +//! [BroadcastStrategy]: ./broadcast_strategy/enum.BroadcastStrategy.html +//! [MessageEnvelope]: ../message/struct.MessageEnvelope.html +//! [Peer]: ../peer_manager/peer/struct.Peer.html +//! [OutboundMessagePool]: ./outbound_message_pool/struct.OutboundMessagePool.html +//! [PeerConnection]: ../connection/peer_connection/index.html +//! [ConnectionManager]: ../connection_manager/index.html +//! [PeerManager]: ../peer_manager/index.html +//! [0MQ inproc]: http://api.zeromq.org/2-1:zmq-inproc + +pub mod broadcast_strategy; +pub mod error; +pub mod outbound_message_pool; +pub mod outbound_message_service; + +pub use self::{ + broadcast_strategy::{BroadcastStrategy, ClosestRequest}, + error::OutboundError, + outbound_message_pool::{OutboundMessage, OutboundMessagePool}, +}; diff --git a/comms/src/outbound_message_service/outbound_message_pool/error.rs b/comms/src/outbound_message_service/outbound_message_pool/error.rs new file mode 100644 index 0000000000..096fbff489 --- /dev/null +++ b/comms/src/outbound_message_service/outbound_message_pool/error.rs @@ -0,0 +1,46 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::{ConnectionError, DealerProxyError}, + connection_manager::ConnectionManagerError, + peer_manager::PeerManagerError, +}; +use derive_error::Error; +use tari_utilities::message_format::MessageFormatError; + +#[derive(Error, Debug)] +pub enum OutboundMessagePoolError { + ConnectionError(ConnectionError), + /// Worker shut down sender disconnected before sending shutdown signal + WorkerShutdownSignalDisconnected, + #[error(msg_embedded, non_std, no_from)] + InvalidFrameFormat(String), + MessageFormatError(MessageFormatError), + PeerManagerError(PeerManagerError), + ConnectionManagerError(ConnectionManagerError), + DealerProxyError(DealerProxyError), + /// Unable to allocate message pool worker thread + ThreadInitializationError, + /// The message retry service has unexpectedly disconnected from it's channel + MessageRetryServiceDisconnected, +} diff --git a/comms/src/outbound_message_service/outbound_message_pool/mod.rs b/comms/src/outbound_message_service/outbound_message_pool/mod.rs new file mode 100644 index 0000000000..83123e698c --- /dev/null +++ b/comms/src/outbound_message_service/outbound_message_pool/mod.rs @@ -0,0 +1,35 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod outbound_message; +mod pool; +mod retry_queue; +mod worker; + +pub use self::{ + error::OutboundMessagePoolError, + outbound_message::OutboundMessage, + pool::{OutboundMessagePool, OutboundMessagePoolConfig}, + retry_queue::RetryQueue, + worker::MessagePoolWorker, +}; diff --git a/comms/src/outbound_message_service/outbound_message_pool/outbound_message.rs b/comms/src/outbound_message_service/outbound_message_pool/outbound_message.rs new file mode 100644 index 0000000000..c5ca2d9268 --- /dev/null +++ b/comms/src/outbound_message_service/outbound_message_pool/outbound_message.rs @@ -0,0 +1,81 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{message::FrameSet, peer_manager::node_id::NodeId}; +use serde::{Deserialize, Serialize}; + +/// The OutboundMessage has a copy of the MessageEnvelope. OutboundMessageService will create the +/// OutboundMessage and forward it to the OutboundMessagePool. +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub struct OutboundMessage { + destination_node_id: NodeId, + message_frames: FrameSet, +} + +impl OutboundMessage { + /// Create a new OutboundMessage from the destination_node_id and message_frames + pub fn new(destination: NodeId, message_frames: FrameSet) -> OutboundMessage { + OutboundMessage { + destination_node_id: destination, + message_frames, + } + } + + /// Get a reference to the destination NodeID + pub fn destination_node_id(&self) -> &NodeId { + &self.destination_node_id + } + + /// Get a reference to the message frames + pub fn message_frames(&self) -> &FrameSet { + &self.message_frames + } + + /// Consume this wrapper and return ownership of the frames + pub fn into_frames(self) -> FrameSet { + self.message_frames + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn new() { + let node_id = NodeId::new(); + let subject = OutboundMessage::new(node_id.clone(), vec![vec![1]]); + assert_eq!(subject.message_frames[0].len(), 1); + assert_eq!(subject.destination_node_id, node_id); + } + + #[test] + fn getters() { + let node_id = NodeId::new(); + let frames = vec![vec![1]]; + let subject = OutboundMessage::new(node_id.clone(), frames.clone()); + + assert_eq!(subject.destination_node_id(), &node_id); + assert_eq!(subject.message_frames(), &frames); + assert_eq!(subject.into_frames(), frames); + } +} diff --git a/comms/src/outbound_message_service/outbound_message_pool/pool.rs b/comms/src/outbound_message_service/outbound_message_pool/pool.rs new file mode 100644 index 0000000000..d2a39b8d85 --- /dev/null +++ b/comms/src/outbound_message_service/outbound_message_pool/pool.rs @@ -0,0 +1,360 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use super::{MessagePoolWorker, RetryQueue}; +use crate::{ + connection_manager::ConnectionManager, + outbound_message_service::{ + outbound_message_pool::error::OutboundMessagePoolError, + OutboundError, + OutboundMessage, + }, + peer_manager::{NodeId, PeerManager}, +}; +use crossbeam_channel::{self as channel, Receiver, RecvTimeoutError, Sender}; +use crossbeam_deque::{Stealer, Worker}; +use log::*; +use std::{ + sync::Arc, + thread::{self, JoinHandle}, + time::Duration, +}; +use tari_utilities::thread_join::ThreadJoinWithTimeout; + +/// The default number of processing worker threads that will be created by the OutboundMessageService +pub const DEFAULT_NUM_OUTBOUND_MSG_WORKERS: usize = 4; + +const LOG_TARGET: &str = "comms::outbound_message_service::pool"; + +/// Set the maximum waiting time for Retry Service Threads and MessagePoolWorker threads to join +const MSG_POOL_WORKER_THREAD_JOIN_TIMEOUT: Duration = Duration::from_millis(3000); +const WORK_FORWARDER_THREAD_JOIN_TIMEOUT: Duration = Duration::from_millis(1500); + +#[derive(Clone, Copy)] +pub struct OutboundMessagePoolConfig { + /// How many workers to spawn + pub num_workers: usize, + /// How many times the pool will requeue a message to be sent + pub max_retries: u32, +} + +impl Default for OutboundMessagePoolConfig { + fn default() -> Self { + OutboundMessagePoolConfig { + num_workers: DEFAULT_NUM_OUTBOUND_MSG_WORKERS, + max_retries: 10, + } + } +} + +/// # OutboundMessagePool +/// +/// The OutboundMessagePool receives messages and forwards them to a pool of [MsgPoolWorker]s who's job it is to +/// reliably send the message, if possible. +/// +/// The pool starts the configured number of workers (see [OutboundMessagePoolConfig]) and distributes messages +/// between them using a [crossbeam_deque::Worker]. +/// +/// Messages to send are received on a [crossbeam_channel::Receiver]. A copy of the [Sender] side can be obtained +/// by calling the [OutboundMessagePool::sender] method. +/// +/// [crossbeam_channel::Receiver]: https://docs.rs/crossbeam-channel/0.3.9/crossbeam_channel/struct.Receiver.html +/// [Sender]: https://docs.rs/crossbeam-channel/0.3.9/crossbeam_channel/struct.Sender.html +/// [OutboundMessagePool::sender]: #method.sender +/// [crossbeam_deque::Worker]: https://docs.rs/crossbeam/0.7.2/crossbeam/deque/struct.Worker.html +/// [OutboundMessage]: ../outbound_message/struct.OutboundMessage.html +/// [OutboundMessagePoolConfig]: ./struct.OutboundMessagePoolConfig.html +/// [MsgPoolWorker]: ../worker/struct.MsgPoolWorker.html +pub struct OutboundMessagePool { + config: OutboundMessagePoolConfig, + message_tx: Sender, + message_rx: Option>, + peer_manager: Arc, + retry_queue: RetryQueue, + connection_manager: Arc, + worker_thread_handles: Vec>>, + work_forwarder_handle: Option>, + worker_shutdown_signals: Vec>, + work_forwarder_shutdown_tx: Sender<()>, + work_forwarder_shutdown_rx: Option>, +} +impl OutboundMessagePool { + /// Construct a new Outbound Message Pool. + /// + /// # Arguments + /// `config` - The configuration struct to use for the Outbound Message Pool + /// `peer_manager` - Arc to a PeerManager + /// `connection_manager` - Arc to a ConnectionManager + pub fn new( + config: OutboundMessagePoolConfig, + peer_manager: Arc, + connection_manager: Arc, + ) -> OutboundMessagePool + { + let (message_tx, message_rx) = channel::unbounded(); + let (shutdown_tx, shutdown_rx) = channel::bounded(1); + let retry_queue = RetryQueue::new(); + OutboundMessagePool { + config, + message_rx: Some(message_rx), + message_tx, + peer_manager, + connection_manager, + retry_queue, + worker_thread_handles: Vec::new(), + worker_shutdown_signals: Vec::new(), + work_forwarder_handle: None, + work_forwarder_shutdown_tx: shutdown_tx, + work_forwarder_shutdown_rx: Some(shutdown_rx), + } + } + + /// Returns a copy of the Sender which can be used to send messages for processing to the + /// OutboundMessagePool workers + pub fn sender(&self) -> Sender { + self.message_tx.clone() + } + + /// Starts a thread that reads from the message_source and pushes worker on the worker queue + fn start_work_forwarder(&mut self, worker: Worker) -> JoinHandle<()> { + let message_rx = self + .message_rx + .take() + .expect("Invariant: OutboundMessagePool was initialized without a message_rx"); + + let shutdown_rx = self + .work_forwarder_shutdown_rx + .take() + .expect("Invariant: OutboundMessagePool was initialized without a shutdown_rx"); + + thread::spawn(move || loop { + match shutdown_rx.recv_timeout(Duration::from_millis(1)) { + Ok(_) => break, + Err(RecvTimeoutError::Timeout) => {}, + Err(RecvTimeoutError::Disconnected) => { + warn!( + target: LOG_TARGET, + "Work forwarder shutdown signal disconnected unexpectedly" + ); + break; + }, + } + + match message_rx.recv_timeout(Duration::from_millis(1000)) { + Ok(msg) => worker.push(msg), + Err(RecvTimeoutError::Timeout) => {}, + Err(RecvTimeoutError::Disconnected) => { + warn!(target: LOG_TARGET, "Work forwarder sender disconnected unexpectedly"); + break; + }, + } + }) + } + + /// Start the Outbound Message Pool. + /// + /// This starts the configured number of workers and a worker forwarder. The forwarder forwards + /// work to the worker queue and the workers take work from the worker queue. + pub fn start(&mut self) -> Result<(), OutboundMessagePoolError> { + info!(target: LOG_TARGET, "Starting outbound message pool"); + + let worker = Worker::new_fifo(); + + info!(target: LOG_TARGET, "Starting {} OMP workers", self.config.num_workers); + for _ in 0..self.config.num_workers { + self.start_message_worker(worker.stealer(), self.retry_queue.clone())?; + } + + info!(target: LOG_TARGET, "Starting OMP work producer"); + let handle = self.start_work_forwarder(worker); + self.work_forwarder_handle = Some(handle); + + Ok(()) + } + + fn start_message_worker( + &mut self, + stealer: Stealer, + retry_queue: RetryQueue, + ) -> Result<(), OutboundMessagePoolError> + { + let (worker_thread_handle, worker_shutdown_signal) = MessagePoolWorker::start( + self.config, + stealer, + retry_queue, + self.peer_manager.clone(), + self.connection_manager.clone(), + )?; + + self.worker_thread_handles.push(worker_thread_handle); + self.worker_shutdown_signals.push(worker_shutdown_signal); + + Ok(()) + } + + /// Tell the underlying dealer thread, nessage retry service and workers to shut down + pub fn shutdown(self) -> Result<(), OutboundError> { + // Send Shutdown control message + for worker_shutdown_signal in self.worker_shutdown_signals { + worker_shutdown_signal.send(()).map_err(|e| { + OutboundError::ShutdownSignalSendError(format!( + "Failed to send shutdown signal to outbound workers: {:?}", + e + )) + })?; + } + + self.retry_queue.clear(); + // Send shutdown signal to message retry queue if it has been started + self.work_forwarder_shutdown_tx.send(()).map_err(|e| { + OutboundError::ShutdownSignalSendError(format!("Failed to send shutdown signal to work forwarder: {:?}", e)) + })?; + + if let Some(handle) = self.work_forwarder_handle { + handle + .timeout_join(WORK_FORWARDER_THREAD_JOIN_TIMEOUT) + .map_err(OutboundError::ThreadJoinError)?; + } + + // Join worker threads + for worker_thread_handle in self.worker_thread_handles { + worker_thread_handle + .timeout_join(MSG_POOL_WORKER_THREAD_JOIN_TIMEOUT) + .map_err(OutboundError::ThreadJoinError)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::{ + connection::{InprocAddress, NetAddress, ZmqContext}, + connection_manager::{ConnectionManager, PeerConnectionConfig}, + outbound_message_service::{ + outbound_message_pool::{OutboundMessagePoolConfig, RetryQueue}, + OutboundMessagePool, + }, + peer_manager::{peer::PeerFlags, NodeId, NodeIdentity, Peer, PeerManager}, + }; + use crossbeam_deque::Worker; + use std::{sync::Arc, time::Duration}; + use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + use tari_storage::HMapDatabase; + use tari_utilities::thread_join::ThreadJoinWithTimeout; + + fn make_peer_connection_config(consumer_address: InprocAddress) -> PeerConnectionConfig { + PeerConnectionConfig { + peer_connection_establish_timeout: Duration::from_millis(10), + max_message_size: 1024, + host: "127.0.0.1".parse().unwrap(), + max_connect_retries: 1, + max_connections: 10, + message_sink_address: consumer_address, + socks_proxy_address: None, + } + } + + fn outbound_message_pool_setup( + context: &ZmqContext, + ) -> (Arc, Arc, Arc) { + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let peer_manager = Arc::new(PeerManager::new(HMapDatabase::new()).unwrap()); + + // Connection Manager + let connection_manager = Arc::new(ConnectionManager::new( + context.clone(), + node_identity.clone(), + peer_manager.clone(), + make_peer_connection_config(InprocAddress::random()), + )); + + (peer_manager, connection_manager, node_identity) + } + + #[test] + fn new() { + let context = ZmqContext::new(); + let (peer_manager, connection_manager, _) = outbound_message_pool_setup(&context); + let omp_config = OutboundMessagePoolConfig::default(); + let omp = OutboundMessagePool::new(omp_config.clone(), peer_manager.clone(), connection_manager.clone()); + assert_eq!(omp.worker_thread_handles.len(), 0); + assert_eq!(omp.worker_shutdown_signals.len(), 0); + assert!(omp.work_forwarder_shutdown_rx.is_some()); + assert!(omp.work_forwarder_handle.is_none()); + } + + #[test] + fn work_forwarder_shutdown() { + let context = ZmqContext::new(); + let (peer_manager, connection_manager, _) = outbound_message_pool_setup(&context); + let omp_config = OutboundMessagePoolConfig::default(); + let mut omp = OutboundMessagePool::new(omp_config.clone(), peer_manager.clone(), connection_manager.clone()); + + let worker = Worker::new_fifo(); + let handle = omp.start_work_forwarder(worker); + + omp.shutdown().unwrap(); + handle.timeout_join(Duration::from_millis(3000)).unwrap(); + } + + #[test] + fn start_message_worker() { + let context = ZmqContext::new(); + let (peer_manager, connection_manager, _) = outbound_message_pool_setup(&context); + let omp_config = OutboundMessagePoolConfig::default(); + let mut omp = OutboundMessagePool::new(omp_config.clone(), peer_manager.clone(), connection_manager.clone()); + assert_eq!(omp.worker_shutdown_signals.len(), 0); + assert_eq!(omp.worker_thread_handles.len(), 0); + + let worker = Worker::new_fifo(); + let retry_queue = RetryQueue::new(); + + omp.start_message_worker(worker.stealer(), retry_queue).unwrap(); + + assert_eq!(omp.worker_shutdown_signals.len(), 1); + assert_eq!(omp.worker_thread_handles.len(), 1); + + omp.shutdown().unwrap(); + } + + #[test] + fn clean_shutdown() { + let context = ZmqContext::new(); + let (peer_manager, connection_manager, _) = outbound_message_pool_setup(&context); + + // Add random peer + let mut rng = rand::OsRng::new().unwrap(); + let (_dest_sk, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk.clone()).unwrap(); + let net_addresses = "127.0.0.1:45325".parse::().unwrap().into(); + let dest_peer = Peer::new(pk.clone(), node_id, net_addresses, PeerFlags::default()); + peer_manager.add_peer(dest_peer.clone()).unwrap(); + + let omp_config = OutboundMessagePoolConfig::default(); + let mut omp = OutboundMessagePool::new(omp_config.clone(), peer_manager.clone(), connection_manager.clone()); + + omp.start().unwrap(); + + omp.shutdown().unwrap(); + } +} diff --git a/comms/src/outbound_message_service/outbound_message_pool/retry_queue.rs b/comms/src/outbound_message_service/outbound_message_pool/retry_queue.rs new file mode 100644 index 0000000000..4688a73337 --- /dev/null +++ b/comms/src/outbound_message_service/outbound_message_pool/retry_queue.rs @@ -0,0 +1,702 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{ + cmp::{self, Ordering}, + collections::{BinaryHeap, HashMap, VecDeque}, + hash::Hash, + sync::{Arc, RwLock}, + time::{Duration, Instant}, +}; + +#[derive(Eq, PartialEq)] +struct ScheduledItem { + item: T, + scheduled_at: Instant, +} + +impl ScheduledItem { + pub fn new(item: T, scheduled_at: Instant) -> Self { + Self { item, scheduled_at } + } + + pub fn is_scheduled(&self) -> bool { + self.scheduled_at <= Instant::now() + } +} + +impl PartialOrd> for ScheduledItem +where ScheduledItem: Ord +{ + /// Orders from least to most time remaining from being scheduled + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ScheduledItem +where ScheduledItem: Eq +{ + /// Orders from least to most time remaining from being scheduled + fn cmp(&self, other: &Self) -> Ordering { + let now = Instant::now(); + let sub_self = self.scheduled_at.checked_duration_since(now); + let sub_other = other.scheduled_at.checked_duration_since(now); + + match (sub_self, sub_other) { + // Both are "scheduled" + (None, None) => Ordering::Equal, + // self is "scheduled", other is not + (None, Some(_)) => Ordering::Greater, + // other is "scheduled", self is not + (Some(_), None) => Ordering::Less, + // Both aren't "scheduled", whichever one is closer to being scheduled is greater + (Some(a), Some(b)) => b.cmp(&a), + } + } +} + +/// # RetryBucket +/// +/// Represents an ordered grouping of work items, with methods which can be used to track attempts. +#[derive(Default)] +pub struct RetryBucket { + attempts: u32, + contents: VecDeque, +} + +impl RetryBucket { + /// Create a new RetryBucket with the given contents + pub fn new(contents: Vec) -> Self { + Self { + contents: contents.into(), + attempts: 1, + } + } + + /// Increment the number of attempts for this bucket + pub fn incr_attempts(&mut self) { + self.attempts += 1; + } + + /// Return the number of attempts for this bucket + pub fn attempts(&self) -> u32 { + self.attempts + } + + /// Pop an item from the front + pub fn pop_front(&mut self) -> Option { + self.contents.pop_front() + } + + /// Push an item onto the front + pub fn push_front(&mut self, item: T) { + self.contents.push_front(item) + } + + /// Push an item onto the back + pub fn push_back(&mut self, item: T) { + self.contents.push_back(item) + } + + /// Pour the contents of the given bucket into this one + pub fn pour_from(&mut self, bucket: &mut Self) { + bucket.contents.extend(self.contents.drain(..)) + } + + /// Get the future Instant in time that this bucket should be scheduled. Exponentially backing off + /// according to the number of attempts. + pub fn schedule(&self) -> Instant { + Instant::now() + Duration::from_secs(self.exponential_backoff_offset()) + } + + fn exponential_backoff_offset(&self) -> u64 { + if self.attempts == 0 { + return 0; + } + let secs = 0.5 * (f32::powf(2.0, self.attempts as f32) - 1.0); + cmp::max(2, secs.ceil() as u64) + } +} + +/// A lease represents an item which is either available for lease or already taken. +/// A taken lease may have items added. +/// +/// This is used by the RetryQueue to contain the RetryBuckets and keep track of whether +/// the buckets are available for lease or not. +enum Lease { + Available(T), + Taken(Option), +} + +impl Lease { + #[cfg(test)] + fn is_available(&self) -> bool { + match self { + Lease::Available(_) => true, + _ => false, + } + } + + #[cfg(test)] + fn is_taken(&self) -> bool { + match self { + Lease::Taken(_) => true, + _ => false, + } + } + + fn take(self) -> Option { + match self { + Lease::Available(t) => Some(t), + Lease::Taken(t) => t, + } + } + + fn borrow_inner_mut(&mut self) -> Option<&mut T> { + match self { + Lease::Available(t) => Some(t), + Lease::Taken(Some(t)) => Some(t), + Lease::Taken(None) => None, + } + } + + fn borrow_inner(&self) -> Option<&T> { + match self { + Lease::Available(t) => Some(t), + Lease::Taken(Some(t)) => Some(t), + Lease::Taken(None) => None, + } + } +} + +/// # RetryQueue +/// +/// A thread-safe data structure which provides a small API for leasing buckets of items related by their key. +/// +/// A common use-case for this is bundling related items of work together for sequential processing, where processing +/// related work items doesn't make sense. +/// +/// Buckets of related work can be leased by workers and processed. While a bucket is leased, more related work may come +/// in. To handle this case, a "temporary" bucket may be filled while the original loaned bucket is being processed. +/// Once the worker is done processing the bucket, it may return the bucket (with remaining messages, if any) +/// and the bucket lease will be available again for other workers, albeit with the temporary buckets contents added. +/// Alternatively, the worker may want to remove the lease under that key (all messages processed). In this case, +/// a call to the `remove()` method returns the bucket (in this case the temporary one) for processing by the worker. +/// +/// Each bucket is scheduled in time according to it's number of attempts (exponential backoff). +#[derive(Clone)] +pub struct RetryQueue +where K: Hash + Eq +{ + inner: Arc>>, +} + +impl RetryQueue +where + K: Hash + Eq, + K: Clone, +{ + /// Create a new RetryQueue + pub fn new() -> Self { + Self { + inner: Arc::new(RwLock::new(Inner::new())), + } + } + + /// Returns `true` of the queue contains the key, otherwise `false` + pub fn contains(&self, key: &K) -> bool { + let inner = acquire_read_lock!(self.inner); + inner.contains(key) + } + + /// Returns `true` if the queue is empty, otherwise `false` + pub fn is_empty(&self) -> bool { + let inner = acquire_read_lock!(self.inner); + inner.is_empty() + } + + /// Lease the next bucket if one is scheduled and available + pub fn lease_next(&self) -> Option<(K, RetryBucket)> { + let mut inner = acquire_write_lock!(self.inner); + inner.take_next() + } + + /// Return the leased bucket and make it available. Any bucket items added during the lease + /// are added to the returned bucket. Returns true if the lease was returned, otherwise false + pub fn return_lease(&self, key: &K, bucket: RetryBucket) -> bool { + let mut inner = acquire_write_lock!(self.inner); + // It is an error to return a lease that doesn't exist. + inner.return_lease(key, bucket) + } + + /// Add an item to a bucket. If a bucket doesn't exist for the given key one is created, otherwise + /// the message is appended to the available or temporary bucket. + pub fn add_item(&self, key: K, item: T) { + let mut inner = acquire_write_lock!(self.inner); + inner.add_item(key, item); + } + + /// Remove a bucket from the queue. If an available/temporary bucket exists, it is returned. + pub fn remove(&self, key: &K) -> Option> { + let mut inner = acquire_write_lock!(self.inner); + inner.remove(key) + } + + /// Clear the contents of the queue + pub fn clear(&self) { + let mut inner = acquire_write_lock!(self.inner); + inner.clear(); + } +} + +struct Inner +where K: Hash + Eq +{ + buckets: HashMap>>, + roster: BinaryHeap>, +} + +impl Inner +where + K: Hash + Eq, + K: Clone, +{ + fn new() -> Self { + Self { + buckets: HashMap::new(), + roster: BinaryHeap::new(), + } + } + + fn contains(&self, key: &K) -> bool { + self.buckets.contains_key(key) + } + + fn is_empty(&self) -> bool { + self.buckets.is_empty() + } + + fn clear(&mut self) { + self.buckets.clear(); + self.roster.clear(); + } + + fn add_item(&mut self, key: K, item: T) { + match self.buckets.get_mut(&key) { + // Messages have already been added for this key, so we simply add it to + // the existing available bucket + Some(Lease::Available(bucket)) => { + bucket.push_back(item); + }, + // The bucket has been taken, in the meantime other messages for the node have + // arrived. We fill a "taken" bucket with these messages. It is the job of + // the worker that has taken the original bucket to process these messages + // if it is able. It does this by removing the `Taken` Booking which returns + // the rest of the messages. + Some(Lease::Taken(Some(bucket))) => { + bucket.push_back(item); + }, + Some(Lease::Taken(v @ None)) => { + // Set the bucket as the "taken" bucket + *v = Some(RetryBucket::new(vec![item])); + }, + None => { + // Create a new bucket and schedule on the roster + let bucket = RetryBucket::new(vec![item]); + self.add_schedule_for_key(key.clone(), bucket.schedule()); + self.buckets.insert(key, Lease::Available(bucket)); + }, + } + } + + fn return_lease(&mut self, key: &K, mut bucket: RetryBucket) -> bool { + let is_lease_returned = match self.buckets.get_mut(key) { + Some(lease @ Lease::Taken(Some(_))) => { + let b = lease.borrow_inner_mut().expect("RetryBucket in Lease must be Some"); + b.pour_from(&mut bucket); + *lease = Lease::Available(bucket); + true + }, + Some(lease @ Lease::Taken(None)) => { + *lease = Lease::Available(bucket); + true + }, + _ => false, + }; + + // If we were able to put the bucket back, we need to rescheduled + // the bucket + if is_lease_returned { + let schedule = self + .buckets + .get(key) + .expect("Lease must exist for given key") + .borrow_inner() + .expect("RetryBucket must exist in Lease") + .schedule(); + + self.add_schedule_for_key(key.clone(), schedule); + } + + is_lease_returned + } + + fn add_schedule_for_key(&mut self, key: K, schedule: Instant) { + self.roster.push(ScheduledItem::new(key, schedule)); + } + + fn remove(&mut self, key: &K) -> Option> { + self.buckets.remove(key).and_then(|lease| lease.take()) + } + + fn take_next(&mut self) -> Option<(K, RetryBucket)> { + match self.roster.peek().filter(|schedule| schedule.is_scheduled()) { + Some(_) => { + let schedule = self.roster.pop().expect("Could not pop ScheduledItem after peek"); + let lease = self + .buckets + .insert(schedule.item.clone(), Lease::Taken(None)) + .expect("Invariant: Item in schedule does not exist in buckets"); + lease.take().map(|bucket| (schedule.item, bucket)) + }, + None => None, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + //---------------------------------- ScheduledItem --------------------------------------------// + #[test] + fn scheduled_item_new() { + let item = 123; + let instant = Instant::now(); + let scheduled = ScheduledItem::new(item, instant); + assert_eq!(scheduled.item, item); + assert_eq!(scheduled.scheduled_at, instant); + } + + #[test] + fn scheduled_item_is_scheduled() { + let instant = Instant::now(); + let scheduled = ScheduledItem::new(123, instant); + assert!(scheduled.is_scheduled()) + } + + #[test] + fn scheduled_item_ord() { + let instant = Instant::now(); + let scheduled1 = ScheduledItem::new(123, instant); + let scheduled2 = ScheduledItem::new(123, instant + Duration::from_secs(10)); + let scheduled3 = ScheduledItem::new(123, instant + Duration::from_secs(20)); + assert!(scheduled1 > scheduled2); + assert!(scheduled2 > scheduled3); + assert!(scheduled1 > scheduled3); + assert!(scheduled2 < scheduled1); + assert!(scheduled3 < scheduled2); + assert!(scheduled3 < scheduled1); + } + + //---------------------------------- RetryBucket --------------------------------------------// + + #[test] + fn retry_bucket_new() { + let bucket = RetryBucket::new(vec!["A"]); + assert_eq!(bucket.contents, vec!["A"]); + assert_eq!(bucket.attempts, 1); + } + + #[test] + fn retry_bucket_default() { + let bucket = RetryBucket::<()>::default(); + assert_eq!(bucket.contents, vec![]); + assert_eq!(bucket.attempts, 0); + } + + #[test] + fn retry_bucket_incr_attempts() { + let mut bucket = RetryBucket::<()>::default(); + bucket.incr_attempts(); + assert_eq!(bucket.attempts, 1); + bucket.incr_attempts(); + assert_eq!(bucket.attempts, 2); + } + + #[test] + fn retry_bucket_pop_front() { + let mut bucket = RetryBucket::new(vec![123, 456]); + assert_eq!(bucket.pop_front(), Some(123)); + assert_eq!(bucket.pop_front(), Some(456)); + assert_eq!(bucket.pop_front(), None); + assert_eq!(bucket.contents.len(), 0); + } + + #[test] + fn retry_bucket_push_front() { + let mut bucket = RetryBucket::new(vec![]); + bucket.push_front(123); + bucket.push_front(456); + assert_eq!(bucket.contents[0], 456); + assert_eq!(bucket.contents[1], 123); + } + + #[test] + fn retry_bucket_push_back() { + let mut bucket = RetryBucket::new(vec![]); + bucket.push_back(123); + bucket.push_back(456); + assert_eq!(bucket.contents[0], 123); + assert_eq!(bucket.contents[1], 456); + } + + #[test] + fn retry_bucket_pour_into() { + let mut bucket1 = RetryBucket::new(vec![123]); + let mut bucket2 = RetryBucket::new(vec![456]); + bucket2.pour_from(&mut bucket1); + assert_eq!(bucket1.contents[0], 123); + assert_eq!(bucket1.contents[1], 456); + } + + #[test] + fn retry_bucket_exponential_backoff_offset() { + let mut bucket = RetryBucket::<()>::default(); + bucket.attempts = 0; + assert_eq!(bucket.exponential_backoff_offset(), 0); + bucket.attempts = 1; + assert_eq!(bucket.exponential_backoff_offset(), 2); + bucket.attempts = 2; + assert_eq!(bucket.exponential_backoff_offset(), 2); + bucket.attempts = 3; + assert_eq!(bucket.exponential_backoff_offset(), 4); + bucket.attempts = 4; + assert_eq!(bucket.exponential_backoff_offset(), 8); + bucket.attempts = 5; + assert_eq!(bucket.exponential_backoff_offset(), 16); + bucket.attempts = 6; + assert_eq!(bucket.exponential_backoff_offset(), 32); + bucket.attempts = 7; + assert_eq!(bucket.exponential_backoff_offset(), 64); + bucket.attempts = 8; + assert_eq!(bucket.exponential_backoff_offset(), 128); + bucket.attempts = 9; + assert_eq!(bucket.exponential_backoff_offset(), 256); + bucket.attempts = 10; + assert_eq!(bucket.exponential_backoff_offset(), 512); + } + + #[test] + fn retry_bucket_schedule() { + let mut bucket = RetryBucket::<()>::default(); + bucket.attempts = 1; + assert!(bucket.schedule() > Instant::now()); + assert!(bucket.schedule() < Instant::now() + Duration::from_secs(3)); + } + + //---------------------------------- Lease --------------------------------------------// + + #[test] + fn lease_take() { + // Non-copy type + let s = "A".to_string(); + let lease = Lease::Available(s.clone()); + assert_eq!(lease.take(), Some(s)); + + let s = "A".to_string(); + let lease = Lease::Taken(Some(s.clone())); + assert_eq!(lease.take(), Some(s)); + + let lease = Lease::<()>::Taken(None); + assert_eq!(lease.take(), None); + } + + #[test] + fn lease_borrow_inner() { + let lease = Lease::Available(123); + assert_eq!(lease.borrow_inner(), Some(&123)); + + let lease = Lease::Taken(Some(123)); + assert_eq!(lease.borrow_inner(), Some(&123)); + + let lease = Lease::<()>::Taken(None); + assert_eq!(lease.borrow_inner(), None); + } + + #[test] + fn lease_borrow_inner_mut() { + let mut lease = Lease::Available(123); + assert_eq!(lease.borrow_inner_mut(), Some(&mut 123)); + + let mut lease = Lease::Taken(Some(123)); + assert_eq!(lease.borrow_inner_mut(), Some(&mut 123)); + + let mut lease = Lease::<()>::Taken(None); + assert_eq!(lease.borrow_inner_mut(), None); + } + + //---------------------------------- RetryBucket --------------------------------------------// + + #[test] + fn retry_queue_new() { + let retry_queue = RetryQueue::<(), ()>::new(); + let lock = acquire_read_lock!(retry_queue.inner); + assert!(lock.buckets.is_empty()); + assert!(lock.roster.is_empty()); + } + + #[test] + fn retry_queue_clear() { + let retry_queue = RetryQueue::new(); + retry_queue.add_item(1, 1); + retry_queue.add_item(1, 1); + retry_queue.add_item(2, 2); + retry_queue.add_item(2, 2); + + assert!(!retry_queue.is_empty()); + retry_queue.clear(); + assert!(retry_queue.is_empty()); + assert!(!retry_queue.contains(&1)); + assert!(!retry_queue.contains(&2)); + } + + #[test] + fn retry_queue_contains() { + let retry_queue = RetryQueue::new(); + assert!(!retry_queue.contains(&1)); + retry_queue.add_item(1, 1); + assert!(retry_queue.contains(&1)); + } + + #[test] + fn retry_queue_is_empty() { + let retry_queue = RetryQueue::new(); + assert!(retry_queue.is_empty()); + retry_queue.add_item(1, 1); + assert!(!retry_queue.is_empty()); + } + + fn util_modify_schedule(retry_queue: &RetryQueue, scheduled_at: Instant) { + // Ensure that the item is scheduled + let mut lock = acquire_write_lock!(retry_queue.inner); + let mut item = lock.roster.pop().unwrap(); + item.scheduled_at = scheduled_at; + lock.roster.push(item); + } + + #[test] + fn retry_queue_lease_next() { + let retry_queue = RetryQueue::new(); + assert!(retry_queue.lease_next().is_none()); + retry_queue.add_item(1, 2); + + util_modify_schedule(&retry_queue, Instant::now() - Duration::from_secs(1)); + let (k, mut bucket) = retry_queue.lease_next().unwrap(); + // We've leased the bucket, but retry queue still knows about the key + assert!(retry_queue.contains(&k)); + + assert_eq!(k, 1); + assert_eq!(bucket.pop_front(), Some(2)); + assert!(retry_queue.lease_next().is_none()); + + { + let lock = acquire_read_lock!(retry_queue.inner); + assert!(lock.buckets.get(&1).unwrap().is_taken()); + } + + // Required to remove the Taken lease - removing the Taken lease when + // done is part of the contract of using RetryQueue + retry_queue.remove(&1); + + retry_queue.add_item(1, 2); + util_modify_schedule(&retry_queue, Instant::now() + Duration::from_secs(10)); + assert!(retry_queue.lease_next().is_none()); + } + + #[test] + fn retry_queue_add_item() { + let retry_queue = RetryQueue::new(); + retry_queue.add_item(1, 2); + retry_queue.add_item(1, 3); + retry_queue.add_item(2, 3); + + { + let lock = acquire_read_lock!(retry_queue.inner); + assert_eq!(lock.buckets.len(), 2); + assert!(lock.buckets.contains_key(&1)); + assert!(lock.buckets.contains_key(&2)); + assert_eq!(lock.buckets.get(&1).unwrap().borrow_inner().unwrap().contents.len(), 2); + assert_eq!(lock.buckets.get(&2).unwrap().borrow_inner().unwrap().contents.len(), 1); + } + } + + #[test] + fn retry_queue_return_lease() { + let retry_queue = RetryQueue::new(); + retry_queue.add_item(1, 2); + retry_queue.add_item(1, 3); + + util_modify_schedule(&retry_queue, Instant::now() - Duration::from_millis(1)); + + let (k, bucket) = retry_queue.lease_next().unwrap(); + { + let lock = acquire_read_lock!(retry_queue.inner); + assert!(lock.buckets.get(&1).unwrap().is_taken()); + } + + // Items added while lease is taken should be appended to the returning bucket + retry_queue.add_item(1, 4); + retry_queue.add_item(1, 5); + + assert!(retry_queue.return_lease(&k, bucket)); + { + let lock = acquire_read_lock!(retry_queue.inner); + assert!(lock.buckets.get(&1).unwrap().is_available()); + assert_eq!(lock.buckets.get(&1).unwrap().borrow_inner().unwrap().contents.len(), 4); + // Check that the resulting contents of the bucket include the items added during the lease + assert_eq!(lock.buckets.get(&1).unwrap().borrow_inner().unwrap().contents[0], 2); + assert_eq!(lock.buckets.get(&1).unwrap().borrow_inner().unwrap().contents[1], 3); + assert_eq!(lock.buckets.get(&1).unwrap().borrow_inner().unwrap().contents[2], 4); + assert_eq!(lock.buckets.get(&1).unwrap().borrow_inner().unwrap().contents[3], 5); + } + } + + #[test] + fn retry_queue_remove() { + let retry_queue = RetryQueue::new(); + retry_queue.add_item(1, 2); + + retry_queue.remove(&1).unwrap(); + assert!(retry_queue.remove(&1).is_none()); + assert!(!retry_queue.contains(&1)); + } + + #[test] + fn retry_queue_clone() { + let retry_queue = RetryQueue::new(); + retry_queue.add_item(1, 2); + let clone = retry_queue.clone(); + assert!(clone.contains(&1)); + } +} diff --git a/comms/src/outbound_message_service/outbound_message_pool/worker.rs b/comms/src/outbound_message_service/outbound_message_pool/worker.rs new file mode 100644 index 0000000000..bcbf02c3a9 --- /dev/null +++ b/comms/src/outbound_message_service/outbound_message_pool/worker.rs @@ -0,0 +1,354 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{ + error::OutboundMessagePoolError, + pool::OutboundMessagePoolConfig, + retry_queue::{RetryBucket, RetryQueue}, + OutboundMessage, +}; +use crate::{ + connection::PeerConnection, + connection_manager::ConnectionManager, + peer_manager::{NodeId, Peer, PeerManager}, +}; +use crossbeam_channel::{self as channel, Receiver, RecvTimeoutError, Sender}; +use crossbeam_deque::{Steal, Stealer}; +use log::*; +use std::{sync::Arc, thread, time::Duration}; + +const LOG_TARGET: &str = "comms::outbound_message_service::pool::worker"; +/// Set the allocated stack size for each MessagePoolWorker thread +const THREAD_STACK_SIZE: usize = 256 * 1024; // 256kb + +/// This is an instance of a single Worker thread for the Outbound Message Pool +pub struct MessagePoolWorker { + config: OutboundMessagePoolConfig, + stealer: Stealer, + retry_queue: RetryQueue, + peer_manager: Arc, + connection_manager: Arc, + shutdown_receiver: Receiver<()>, +} + +impl MessagePoolWorker { + /// Start the MessagePoolWorker thread + pub fn start( + config: OutboundMessagePoolConfig, + stealer: Stealer, + retry_queue: RetryQueue, + peer_manager: Arc, + connection_manager: Arc, + ) -> Result<(thread::JoinHandle>, Sender<()>), OutboundMessagePoolError> + { + let (shutdown_signal, shutdown_receiver) = channel::bounded(1); + let mut worker = Self { + config, + stealer, + retry_queue, + peer_manager, + connection_manager, + shutdown_receiver, + }; + + let thread_handle = thread::Builder::new() + .name("message-pool-worker-thread".to_string()) + .stack_size(THREAD_STACK_SIZE) + .spawn(move || loop { + match worker.run() { + Ok(_) => break Ok(()), + Err(err @ OutboundMessagePoolError::WorkerShutdownSignalDisconnected) => { + error!(target: LOG_TARGET, "Message Pool worker exited: {:?}", err); + error!( + target: LOG_TARGET, + "The shutdown signal disconnected likely because the outbound message pool went out of \ + scope before shutdown was called. Exiting worker with an error." + ); + break Err(err); + }, + Err(err) => { + error!(target: LOG_TARGET, "Outbound Message Pool worker exited: {:?}", err); + warn!(target: LOG_TARGET, "Restarting outbound message worker after failure."); + // Sleep so that if this service continually restarts, we don't get high CPU usage + thread::sleep(Duration::from_secs(1)); + }, + } + }) + .map_err(|_| OutboundMessagePoolError::ThreadInitializationError)?; + + Ok((thread_handle, shutdown_signal)) + } + + /// Start MessagePoolWorker which will connect to the inbound message dealer, accept messages from the queue, + /// attempt to send them and if it cannot send then requeue the message + fn run(&mut self) -> Result<(), OutboundMessagePoolError> { + loop { + match self.shutdown_receiver.recv_timeout(Duration::from_millis(5)) { + // Shut down signal received + Ok(_) => break, + // Nothing received + Err(RecvTimeoutError::Timeout) => {}, + // Sender disconnected before sending the shutdown signal, this is an error + Err(RecvTimeoutError::Disconnected) => { + return Err(OutboundMessagePoolError::WorkerShutdownSignalDisconnected) + }, + } + + // Check for new messages + match self.stealer.steal() { + Steal::Success(msg) => { + if self.retry_queue.contains(msg.destination_node_id()) { + // The retry queue has scheduled messages for this node_id, + // so rather than trying to send now, we add this to the + // retry queue so that they can all be sent in a batch when scheduled + // to do so + let node_id = msg.destination_node_id().clone(); + debug!( + target: LOG_TARGET, + "Messages for this NodeId ({}) are scheduled for retry, adding this message to the retry \ + queue", + node_id + ); + self.retry_queue.add_item(node_id, msg); + } else { + // Attempt to send a single message + match self.attempt(&msg) { + Ok(peer) => debug!( + target: LOG_TARGET, + "Message successfully sent to NodeId={}", peer.node_id + ), + Err(err) => { + debug!( + target: LOG_TARGET, + "Failed to send message to peer ({}). {:?}. Sending to failed queue.", + msg.destination_node_id(), + err, + ); + // Add to failed message queue + self.retry_queue.add_item(msg.destination_node_id().clone(), msg); + }, + } + } + }, + Steal::Empty => { + // No incoming messages, maybe the retry_queue has some work + if self.retry_queue.is_empty() { + // Nothing in retry queue, sleep for a bit + thread::sleep(Duration::from_millis(100)); + } else { + // Work on a bucket in the retry queue + self.process_retry_queue(); + } + }, + Steal::Retry => {}, + } + } + + Ok(()) + } + + fn process_retry_queue(&self) { + if let Some((node_id, bucket)) = self.retry_queue.lease_next() { + self.try_send_bucket(&node_id, bucket); + } + } + + fn try_send_bucket(&self, node_id: &NodeId, mut bucket: RetryBucket) { + match self.attempt_batch(&node_id, &mut bucket) { + Ok(_) => { + // Bucket has been sent - now we must send any messages that could have been + // scheduled while the lease on the bucket was out + if let Some(left_over) = self.retry_queue.remove(&node_id) { + self.try_send_bucket(node_id, left_over); + } + }, + Err(err) => { + debug!( + target: LOG_TARGET, + "(Attempt {} of {}) Failed to send message to peer ({}). {:?}.", + bucket.attempts(), + self.config.max_retries, + node_id, + err, + ); + // Message bucket failed to send - put the bucket (with remaining messages) back on the + // retry queue. + bucket.incr_attempts(); + if bucket.attempts() > self.config.max_retries { + debug!( + target: LOG_TARGET, + "Unable to send message to NodeId {} after {} attempts. Message bucket discarded.", + node_id, + bucket.attempts(), + ); + self.retry_queue.remove(node_id); + return; + } + + if !self.retry_queue.return_lease(&node_id, bucket) { + // This should never happen + debug_assert!(false, "return_lease called for bucket which doesn't exist"); + warn!( + target: LOG_TARGET, + "Lease on message bucket for NodeId '{}' was not returned", node_id + ); + } + }, + } + } + + fn attempt_batch( + &self, + node_id: &NodeId, + bucket: &mut RetryBucket, + ) -> Result<(), OutboundMessagePoolError> + { + self.attempt_establish_connection(&node_id) + .and_then(|(_, conn)| self.send_batch(&conn, bucket)) + .or_else(|err| { + debug!( + target: LOG_TARGET, + "(Attempt {} of {}) Failed to send message to peer ({}). {:?}. Sending to failed queue.", + bucket.attempts(), + self.config.max_retries, + node_id, + err, + ); + Err(err) + }) + } + + fn attempt(&self, msg: &OutboundMessage) -> Result { + self.attempt_establish_connection(msg.destination_node_id()) + .and_then(|(peer, conn)| self.send_msg(&conn, msg).map(|_| peer)) + } + + /// Attempt to send a message to the NodeId specified in the message. If the the attempt is not successful then mark + /// the failed connection attempt and requeue the message for another attempt + fn attempt_establish_connection( + &self, + node_id: &NodeId, + ) -> Result<(Peer, Arc), OutboundMessagePoolError> + { + let peer = self + .peer_manager + .find_with_node_id(node_id) + .map_err(OutboundMessagePoolError::PeerManagerError)?; + + let peer_connection = self + .connection_manager + .establish_connection_to_peer(&peer) + .map_err(OutboundMessagePoolError::ConnectionManagerError)?; + + Ok((peer, peer_connection)) + } + + fn send_batch( + &self, + connection: &PeerConnection, + bucket: &mut RetryBucket, + ) -> Result<(), OutboundMessagePoolError> + { + while let Some(msg) = bucket.pop_front() { + self.send_msg(connection, &msg).or_else(|err| { + bucket.push_front(msg); + Err(err) + })?; + } + + Ok(()) + } + + fn send_msg( + &self, + peer_connection: &PeerConnection, + msg: &OutboundMessage, + ) -> Result<(), OutboundMessagePoolError> + { + // TODO: Cloning here due to PeerConnection requiring ownership. Investigate if PeerConnection + // could send an Arc to eliminate the need to clone bytes. + let frames = msg.message_frames().clone(); + + peer_connection + .send(frames) + .map_err(OutboundMessagePoolError::ConnectionError) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + connection::{InprocAddress, ZmqContext}, + connection_manager::PeerConnectionConfig, + peer_manager::NodeIdentity, + }; + use crossbeam_deque::Worker; + use tari_storage::HMapDatabase; + use tari_utilities::thread_join::ThreadJoinWithTimeout; + + fn make_peer_connection_config(consumer_address: InprocAddress) -> PeerConnectionConfig { + PeerConnectionConfig { + peer_connection_establish_timeout: Duration::from_millis(10), + max_message_size: 1024, + host: "127.0.0.1".parse().unwrap(), + max_connect_retries: 1, + max_connections: 10, + message_sink_address: consumer_address, + socks_proxy_address: None, + } + } + + fn outbound_message_worker_setup() -> (Arc, Arc, Arc) { + let context = ZmqContext::new(); + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let peer_manager = Arc::new(PeerManager::new(HMapDatabase::new()).unwrap()); + + // Connection Manager + let connection_manager = Arc::new(ConnectionManager::new( + context, + node_identity.clone(), + peer_manager.clone(), + make_peer_connection_config(InprocAddress::random()), + )); + + (peer_manager, connection_manager, node_identity) + } + + #[test] + fn start_shutdown() { + let (peer_manager, connection_manager, _) = outbound_message_worker_setup(); + let worker = Worker::new_fifo(); + let stealer = worker.stealer(); + let (handle, signal) = MessagePoolWorker::start( + OutboundMessagePoolConfig::default(), + stealer, + RetryQueue::new(), + peer_manager, + connection_manager, + ) + .unwrap(); + + signal.send(()).unwrap(); + handle.timeout_join(Duration::from_millis(3000)).unwrap(); + } +} diff --git a/comms/src/outbound_message_service/outbound_message_service.rs b/comms/src/outbound_message_service/outbound_message_service.rs new file mode 100644 index 0000000000..bc61f2480b --- /dev/null +++ b/comms/src/outbound_message_service/outbound_message_service.rs @@ -0,0 +1,293 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{outbound_message_pool::OutboundMessage, BroadcastStrategy, OutboundError}; +use crate::{ + message::{Frame, Message, MessageEnvelope, MessageError, MessageFlags, NodeDestination}, + peer_manager::{peer_manager::PeerManager, NodeIdentity}, +}; +use crossbeam_channel::Sender; +use std::{convert::TryInto, sync::Arc}; +use tari_utilities::message_format::MessageFormat; + +/// Handler functions use the OutboundMessageService to send messages to peers. The OutboundMessage service will receive +/// messages from handlers, apply a broadcasting strategy, encrypted and serialized the messages into OutboundMessages +/// and write them to the outbound message pool. +pub struct OutboundMessageService { + message_sender: Sender, + node_identity: Arc, + peer_manager: Arc, +} + +impl OutboundMessageService { + /// Constructs a new OutboundMessageService + pub fn new( + node_identity: Arc, + message_sender: Sender, + peer_manager: Arc, + ) -> Result + { + Ok(OutboundMessageService { + message_sender, + node_identity, + peer_manager, + }) + } + + /// Sends a domain-level message using the given BroadcastStrategy. + /// + /// *Arguments* + /// + /// - `broadcast_strategy`: [BroadcastStrategy] + /// - `flags`: MessageFlags - See [message module docs]. + /// - `message`: T - The message to send. + /// + /// [BroadcastStrategy]: ../broadcast_strategy/enum.BroadcastStrategy.html + /// [message module docs]: ../../message/index.html + pub fn send_message( + &self, + broadcast_strategy: BroadcastStrategy, + flags: MessageFlags, + message: T, + ) -> Result<(), OutboundError> + where + T: TryInto, + T: MessageFormat, + { + let msg = message.try_into().map_err(OutboundError::MessageSerializationError)?; + + let message_envelope_body = msg.to_binary().map_err(OutboundError::MessageFormatError)?; + self.send_raw(broadcast_strategy, flags, message_envelope_body) + } + + /// Handler functions use the send function to transmit a message to a peer or set of peers based on the + /// BroadcastStrategy + pub fn send_raw( + &self, + broadcast_strategy: BroadcastStrategy, + flags: MessageFlags, + message_envelope_body: Frame, + ) -> Result<(), OutboundError> + { + // Use the BroadcastStrategy to select appropriate peer(s) from PeerManager and then construct and send a + // personalised message to each selected peer + let selected_node_identities = self.peer_manager.get_broadcast_identities(broadcast_strategy)?; + + // Constructing a MessageEnvelope for each recipient + for dest_node_identity in selected_node_identities.into_iter() { + let message_envelope = MessageEnvelope::construct( + &self.node_identity, + dest_node_identity.public_key.clone(), + NodeDestination::NodeId(dest_node_identity.node_id.clone()), + message_envelope_body.clone(), + flags, + ) + .map_err(OutboundError::MessageSerializationError)?; + + let msg = OutboundMessage::new(dest_node_identity.node_id, message_envelope.into_frame_set()); + self.message_sender + .send(msg) + .map_err(|_| OutboundError::SyncSenderError)?; + } + Ok(()) + } + + /// Forwards a received message_envelope to other peers using the given BroadcastStrategy. + /// + /// *Arguments* + /// + /// - `broadcast_strategy`: [BroadcastStrategy] + /// - `message_envelope`: MessageEnvelope - The message to forward. + /// + /// [BroadcastStrategy]: ../broadcast_strategy/enum.BroadcastStrategy.html + pub fn forward_message( + &self, + broadcast_strategy: BroadcastStrategy, + message_envelope: MessageEnvelope, + ) -> Result<(), OutboundError> + { + // Use the BroadcastStrategy to select appropriate peer(s) from PeerManager and then forward the + // received message to each selected peer + let selected_node_identities = self.peer_manager.get_broadcast_identities(broadcast_strategy)?; + + // Modify MessageEnvelope for forwarding + let message_envelope = MessageEnvelope::forward_construct(&self.node_identity, message_envelope) + .map_err(OutboundError::MessageSerializationError)?; + let message_envelope_frames = message_envelope.into_frame_set(); + + // Constructing an OutboundMessage for each recipient + for dest_node_identity in selected_node_identities.into_iter() { + let msg = OutboundMessage::new(dest_node_identity.node_id, message_envelope_frames.clone()); + + self.message_sender + .send(msg) + .map_err(|_| OutboundError::SyncSenderError)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use crate::{ + connection::net_address::NetAddress, + message::Message, + peer_manager::{ + node_id::NodeId, + peer::{Peer, PeerFlags}, + }, + }; + use bitflags::_core::time::Duration; + use crossbeam_channel as channel; + use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + use tari_storage::HMapDatabase; + + fn make_test_message_frame() -> Frame { + let message_header = "Test Message Header".as_bytes().to_vec(); + let message_body = "Test Message Body".as_bytes().to_vec(); + let message_envelope_body = Message::from_message_format(message_header, message_body).unwrap(); + message_envelope_body.to_binary().unwrap() + } + + #[test] + fn test_outbound_send() { + let mut rng = rand::OsRng::new().unwrap(); + let node_identity = Arc::new(NodeIdentity::random_for_test(None)); + + let (dest_sk, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let net_addresses = "127.0.0.1:55445".parse::().unwrap().into(); + let dest_peer = Peer::new(pk, node_id, net_addresses, PeerFlags::default()); + + // Setup OutboundMessageService and transmit a message to the destination + let peer_manager = Arc::new(PeerManager::new(HMapDatabase::new()).unwrap()); + peer_manager.add_peer(dest_peer.clone()).unwrap(); + + let (message_sender, message_receiver) = channel::unbounded(); + let outbound_message_service = + OutboundMessageService::new(node_identity.clone(), message_sender, peer_manager).unwrap(); + + // Construct and send OutboundMessage + let message_header = "Test Message Header".as_bytes().to_vec(); + let message_body = "Test Message Body".as_bytes().to_vec(); + let message = Message::from_message_format(message_header, message_body).unwrap(); + outbound_message_service + .send_raw( + BroadcastStrategy::DirectNodeId(dest_peer.node_id.clone()), + MessageFlags::ENCRYPTED, + message.to_binary().unwrap(), + ) + .unwrap(); + + let outbound_message = message_receiver.recv_timeout(Duration::from_millis(100)).unwrap(); + assert_eq!(outbound_message.destination_node_id(), &dest_peer.node_id); + let message_envelope: MessageEnvelope = outbound_message.message_frames().clone().try_into().unwrap(); + let message_envelope_header = message_envelope.deserialize_header().unwrap(); + assert_eq!(message_envelope_header.origin_source, node_identity.identity.public_key); + assert_eq!(message_envelope_header.peer_source, node_identity.identity.public_key); + assert_eq!( + message_envelope_header.dest, + NodeDestination::NodeId(dest_peer.node_id.clone()) + ); + assert!(message_envelope_header + .verify_signatures(message_envelope.body_frame().clone()) + .unwrap()); + assert_eq!(message_envelope_header.flags, MessageFlags::ENCRYPTED); + let decrypted_message = message_envelope + .deserialize_encrypted_body(&dest_sk, &node_identity.identity.public_key) + .unwrap(); + assert_eq!(message, decrypted_message); + } + + #[test] + fn test_outbound_forward() { + let (message_sender, message_receiver) = channel::unbounded(); + let origin_node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let peer_node_identity = Arc::new(NodeIdentity::random_for_test(None)); + let dest_node_identity = Arc::new(NodeIdentity::random_for_test(None)); + + let net_addresses = "127.0.0.1:55445".parse::().unwrap().into(); + let dest_peer = Peer::new( + dest_node_identity.identity.public_key.clone(), + dest_node_identity.identity.node_id.clone(), + net_addresses, + PeerFlags::default(), + ); + + // Setup OutboundMessageService and transmit a message to the destination + let peer_manager = Arc::new(PeerManager::new(HMapDatabase::new()).unwrap()); + peer_manager.add_peer(dest_peer.clone()).unwrap(); + + let outbound_message_service = + OutboundMessageService::new(peer_node_identity.clone(), message_sender, peer_manager).unwrap(); + + // Origin constructs MessageEnvelope + let desire_message_body = make_test_message_frame(); + let origin_envelope = MessageEnvelope::construct( + &origin_node_identity, + dest_node_identity.identity.public_key.clone(), + NodeDestination::Unknown, + desire_message_body.clone(), + MessageFlags::ENCRYPTED, + ) + .unwrap(); + + // Peer receives MessageEnvelope from Origin, modifies and forwards it + let peer_envelope = MessageEnvelope::forward_construct(&peer_node_identity, origin_envelope).unwrap(); + + outbound_message_service + .forward_message( + BroadcastStrategy::DirectNodeId(dest_node_identity.identity.node_id.clone()), + peer_envelope, + ) + .unwrap(); + + let outbound_message = message_receiver.recv_timeout(Duration::from_millis(100)).unwrap(); + assert_eq!(outbound_message.destination_node_id(), &dest_peer.node_id); + let message_envelope: MessageEnvelope = outbound_message.message_frames().clone().try_into().unwrap(); + let message_envelope_header = message_envelope.deserialize_header().unwrap(); + assert_eq!( + message_envelope_header.origin_source, + origin_node_identity.identity.public_key + ); + assert_eq!( + message_envelope_header.peer_source, + peer_node_identity.identity.public_key + ); + assert_eq!(message_envelope_header.dest, NodeDestination::Unknown); + assert!(message_envelope_header + .verify_signatures(message_envelope.body_frame().clone()) + .unwrap()); + assert_eq!(message_envelope_header.flags, MessageFlags::ENCRYPTED); + assert_eq!( + desire_message_body, + message_envelope + .decrypted_body_frame( + &dest_node_identity.secret_key, + &origin_node_identity.identity.public_key + ) + .unwrap() + ); + } +} diff --git a/comms/src/peer_manager/error.rs b/comms/src/peer_manager/error.rs new file mode 100644 index 0000000000..327401fb1e --- /dev/null +++ b/comms/src/peer_manager/error.rs @@ -0,0 +1,53 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE + +use crate::{connection::NetAddressError, peer_manager::node_id::NodeIdError}; +use derive_error::Error; +use tari_storage::KeyValStoreError; +use tari_utilities::message_format::MessageFormatError; + +#[derive(Debug, Error)] +pub enum PeerManagerError { + /// The requested peer does not exist or could not be located + PeerNotFoundError, + /// The Thread Safety has been breached and the data access has become poisoned + PoisonedAccess, + // A problem occurred during the serialization of the keys or data + SerializationError(MessageFormatError), + /// A problem occurred converting the serialized data into peers + DeserializationError, + /// The index doesn't relate to an existing peer + IndexOutOfBounds, + /// The requested operation can only be performed if the PeerManager is linked to a DataStore + DatastoreUndefined, + /// An empty response was received from the Datastore + EmptyDatastoreQuery, + // A NetAddressError occurred + NetAddressError(NetAddressError), + /// The peer has been banned + BannedPeer, + /// Problem initializing the RNG + RngError, + // An problem has been encountered with the database + DatabaseError(KeyValStoreError), + NodeIdError(NodeIdError), +} diff --git a/comms/src/peer_manager/mod.rs b/comms/src/peer_manager/mod.rs new file mode 100644 index 0000000000..4f75e3dfdd --- /dev/null +++ b/comms/src/peer_manager/mod.rs @@ -0,0 +1,78 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! The peer list maintained by the Peer Manager are used when constructing outbound messages. Peers can be added and +//! removed from the list and can be found via their NodeId, Public key or Net Address. A subset of peers can be +//! requested from the Peer Manager based on a specific Broadcast Strategy. +//! +//! In an application a single Peer Manager should be initialized and passed around the application contained in an Arc. +//! All the functions in peer manager are thread-safe. +//! +//! If the Peer Manager is instantiated with a provided DataStore it will provide persistence via the provided DataStore +//! implementation. +//! +//! ```edition2018 +//! # use tari_comms::peer_manager::{NodeId, Peer, PeerManager, PeerFlags}; +//! # use tari_comms::types::CommsPublicKey; +//! # use tari_comms::connection::{NetAddress, NetAddressesWithStats}; +//! # use tari_crypto::keys::PublicKey; +//! # use tari_storage::lmdb_store::LMDBBuilder; +//! # use lmdb_zero::db; +//! # use std::sync::Arc; +//! # use tari_storage::LMDBWrapper; +//! +//! let mut rng = rand::OsRng::new().unwrap(); +//! let (dest_sk, pk) = CommsPublicKey::random_keypair(&mut rng); +//! let node_id = NodeId::from_key(&pk).unwrap(); +//! let net_addresses = NetAddressesWithStats::from("1.2.3.4:8000".parse::().unwrap()); +//! let peer = Peer::new(pk, node_id.clone(), net_addresses, PeerFlags::default()); +//! let database_name = "pm_peer_database"; +//! let datastore = LMDBBuilder::new() +//! .set_path("/tmp/") +//! .set_environment_size(10) +//! .set_max_number_of_databases(1) +//! .add_database(database_name, lmdb_zero::db::CREATE) +//! .build().unwrap(); +//! let peer_database = datastore.get_handle(database_name).unwrap(); +//! let peer_database = LMDBWrapper::new(Arc::new(peer_database)); +//! let peer_manager = PeerManager::new(peer_database).unwrap(); +//! +//! peer_manager.add_peer(peer.clone()); +//! +//! let returned_peer = peer_manager.find_with_node_id(&node_id).unwrap(); +//! ``` + +pub mod error; +pub mod node_id; +pub mod node_identity; +pub mod peer; +pub mod peer_key; +pub mod peer_manager; +pub mod peer_storage; + +pub use self::{ + error::PeerManagerError, + node_id::NodeId, + node_identity::{NodeIdentity, PeerNodeIdentity}, + peer::{Peer, PeerFlags}, + peer_manager::PeerManager, +}; diff --git a/comms/src/peer_manager/node_id.rs b/comms/src/peer_manager/node_id.rs new file mode 100644 index 0000000000..c36da9453a --- /dev/null +++ b/comms/src/peer_manager/node_id.rs @@ -0,0 +1,482 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{connection::peer_connection::ConnectionId, message::Frame}; +use derive_error::Error; +use serde::{de, Deserialize, Deserializer, Serialize}; +use std::{ + cmp::Ordering, + convert::{TryFrom, TryInto}, + fmt, + hash::{Hash, Hasher}, + marker::PhantomData, +}; +use tari_utilities::{ + hex::{to_hex, Hex}, + ByteArray, + ByteArrayError, + Hashable, +}; + +const NODE_ID_ARRAY_SIZE: usize = 32; +type NodeIdArray = [u8; NODE_ID_ARRAY_SIZE]; + +#[derive(Debug, Error)] +pub enum NodeIdError { + IncorrectByteCount, + OutOfBounds, +} + +/// Hold the XOR distance calculated between two NodeId's. This is used for DHT-style routing. +#[derive(Clone, Debug, Eq, PartialOrd, Ord, Default)] +pub struct NodeDistance(NodeIdArray); + +impl NodeDistance { + /// Construct a new zero distance + pub fn new() -> NodeDistance { + NodeDistance([0; NODE_ID_ARRAY_SIZE]) + } + + /// Calculate the distance between two node ids using the XOR metric + pub fn from_node_ids(x: &NodeId, y: &NodeId) -> NodeDistance { + let mut nd = NodeDistance::new(); + for i in 0..nd.0.len() { + nd.0[i] = x.0[i] ^ y.0[i]; + } + nd + } + + pub fn max_distance() -> NodeDistance { + NodeDistance([255; NODE_ID_ARRAY_SIZE]) + } +} + +impl PartialEq for NodeDistance { + fn eq(&self, nd: &NodeDistance) -> bool { + self.0 == nd.0 + } +} + +impl TryFrom<&[u8]> for NodeDistance { + type Error = NodeIdError; + + /// Construct a node distance from 32 bytes + fn try_from(elements: &[u8]) -> Result { + if elements.len() >= NODE_ID_ARRAY_SIZE { + let mut bytes = [0; NODE_ID_ARRAY_SIZE]; + bytes.copy_from_slice(&elements[0..NODE_ID_ARRAY_SIZE]); + Ok(NodeDistance(bytes)) + } else { + Err(NodeIdError::IncorrectByteCount) + } + } +} + +/// A Node Identity is used as a unique identifier for a node in the Tari communications network. +#[derive(Clone, Debug, Eq, Deserialize, Serialize, Default)] +pub struct NodeId(NodeIdArray); + +impl NodeId { + /// Construct a new node id on the origin + pub fn new() -> Self { + Self([0; NODE_ID_ARRAY_SIZE]) + } + + /// Derive a node id from a public key: node_id=hash(public_key) + pub fn from_key(key: &K) -> Result { + Self::try_from(key.hash().as_slice()) + } + + /// Generate a node id from a base layer registration using the block hash and public key + // pub fn from_baselayer_registration(......) -> NodeId { + // TODO: NodeId=hash(blockhash(with block_height),public key?) + // } + + /// Calculate the distance between the current node id and the provided node id using the XOR metric + pub fn distance(&self, node_id: &NodeId) -> NodeDistance { + NodeDistance::from_node_ids(&self, &node_id) + } + + /// Find and return the indices of the K nearest neighbours from the provided node id list + pub fn closest_indices(&self, node_ids: &Vec, k: usize) -> Result, NodeIdError> { + if k > node_ids.len() { + return Err(NodeIdError::OutOfBounds); + } + let mut indices: Vec = Vec::with_capacity(node_ids.len()); + let mut dists: Vec = Vec::with_capacity(node_ids.len()); + for i in 0..node_ids.len() { + indices.push(i); + dists.push(self.distance(&node_ids[i])); + } + // Perform partial sort of elements only up to K elements + let mut nearest_node_indices: Vec = Vec::with_capacity(k); + for i in 0..k { + for j in i + 1..node_ids.len() { + if dists[i] > dists[j] { + dists.swap(i, j); + indices.swap(i, j); + } + } + nearest_node_indices.push(indices[i]); + } + Ok(nearest_node_indices) + } + + /// Find and return the node ids of the K nearest neighbours from the provided node id list + pub fn closest(&self, node_ids: &Vec, k: usize) -> Result, NodeIdError> { + let nearest_node_indices = self.closest_indices(&node_ids, k)?; + let mut nearest_node_ids: Vec = Vec::with_capacity(nearest_node_indices.len()); + for nearest in nearest_node_indices { + nearest_node_ids.push(node_ids[nearest].clone()); + } + Ok(nearest_node_ids) + } + + pub fn into_inner(self) -> NodeIdArray { + self.0 + } +} + +impl ByteArray for NodeId { + /// Try and convert the given byte array to a NodeId. Any failures (incorrect array length, + /// implementation-specific checks, etc) return a [ByteArrayError](enum.ByteArrayError.html). + fn from_bytes(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|err| ByteArrayError::ConversionError(format!("{:?}", err))) + } + + /// Return the NodeId as a byte array + fn as_bytes(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl PartialEq for NodeId { + fn eq(&self, nid: &NodeId) -> bool { + self.0 == nid.0 + } +} + +impl PartialOrd for NodeId { + fn partial_cmp(&self, other: &NodeId) -> Option { + self.0.partial_cmp(&other.0) + } +} + +impl TryFrom<&[u8]> for NodeId { + type Error = NodeIdError; + + /// Construct a node id from 32 bytes + fn try_from(elements: &[u8]) -> Result { + if elements.len() >= 32 { + let mut bytes = [0; NODE_ID_ARRAY_SIZE]; + bytes.copy_from_slice(&elements[0..NODE_ID_ARRAY_SIZE]); + Ok(NodeId(bytes)) + } else { + Err(NodeIdError::IncorrectByteCount) + } + } +} + +impl TryFrom for NodeId { + type Error = NodeIdError; + + /// Try construct a node id from a frame + fn try_from(frame: Frame) -> Result { + frame.as_slice().try_into() + } +} + +impl Hash for NodeId { + /// Require the implementation of the Hash trait for Hashmaps + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl AsRef<[u8]> for NodeId { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for NodeId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", to_hex(&self.0)) + } +} + +impl From for ConnectionId { + fn from(node_id: NodeId) -> Self { + ConnectionId::new(node_id.into_inner().to_vec()) + } +} + +pub fn deserialize_node_id_from_hex<'de, D>(des: D) -> Result +where D: Deserializer<'de> { + struct KeyStringVisitor { + marker: PhantomData, + }; + + impl<'de> de::Visitor<'de> for KeyStringVisitor { + type Value = NodeId; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a node id in hex format") + } + + fn visit_str(self, v: &str) -> Result + where E: de::Error { + NodeId::from_hex(v).map_err(E::custom) + } + } + des.deserialize_str(KeyStringVisitor { marker: PhantomData }) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::{CommsPublicKey, CommsSecretKey}; + use tari_crypto::keys::{PublicKey, SecretKey}; + use tari_utilities::byte_array::ByteArray; + + #[test] + fn display() { + let node_id = NodeId::try_from( + [ + 144, 28, 106, 112, 220, 197, 216, 119, 9, 217, 42, 77, 159, 211, 53, 207, 0, 157, 5, 55, 235, 247, 160, + 195, 240, 48, 146, 168, 119, 15, 241, 54, + ] + .as_bytes(), + ) + .unwrap(); + + let result = format!("{}", node_id); + assert_eq!( + "901c6a70dcc5d87709d92a4d9fd335cf009d0537ebf7a0c3f03092a8770ff136", + result + ); + } + + #[test] + fn test_from_public_key() { + let mut rng = rand::OsRng::new().unwrap(); + let sk = CommsSecretKey::random(&mut rng); + let pk = CommsPublicKey::from_secret_key(&sk); + let node_id = NodeId::from_key(&pk).unwrap(); + assert_ne!(node_id.0.to_vec(), NodeId::new().0.to_vec()); + // Ensure node id is different to original public key + let mut pk_array: [u8; 32] = [0; 32]; + pk_array.copy_from_slice(&pk.as_bytes()); + assert_ne!(node_id.0.to_vec(), pk_array.to_vec()); + } + + #[test] + fn test_distance_and_ordering() { + let node_id1 = NodeId::try_from( + [ + 144, 28, 106, 112, 220, 197, 216, 119, 9, 217, 42, 77, 159, 211, 53, 207, 0, 157, 5, 55, 235, 247, 160, + 195, 240, 48, 146, 168, 119, 15, 241, 54, + ] + .as_bytes(), + ) + .unwrap(); + let node_id2 = NodeId::try_from( + [ + 186, 43, 62, 14, 60, 214, 9, 180, 145, 122, 55, 160, 83, 83, 45, 185, 219, 206, 226, 128, 5, 26, 20, 0, + 192, 121, 216, 178, 134, 212, 51, 131, + ] + .as_bytes(), + ) + .unwrap(); + let node_id3 = NodeId::try_from( + [ + 60, 32, 246, 39, 108, 201, 214, 91, 30, 230, 3, 126, 31, 46, 66, 203, 27, 51, 240, 177, 230, 22, 118, + 102, 201, 55, 211, 147, 229, 26, 116, 103, + ] + .as_bytes(), + ) + .unwrap(); + assert!(node_id1.0 < node_id2.0); + assert!(node_id1.0 > node_id3.0); + let desired_n1_to_n2_dist = NodeDistance::try_from( + [ + 42, 55, 84, 126, 224, 19, 209, 195, 152, 163, 29, 237, 204, 128, 24, 118, 219, 83, 231, 183, 238, 237, + 180, 195, 48, 73, 74, 26, 241, 219, 194, 181, + ] + .as_bytes(), + ) + .unwrap(); + let desired_n1_to_n3_dist = NodeDistance::try_from( + [ + 172, 60, 156, 87, 176, 12, 14, 44, 23, 63, 41, 51, 128, 253, 119, 4, 27, 174, 245, 134, 13, 225, 214, + 165, 57, 7, 65, 59, 146, 21, 133, 81, + ] + .as_bytes(), + ) + .unwrap(); + let n1_to_n2_dist = node_id1.distance(&node_id2); + let n1_to_n3_dist = node_id1.distance(&node_id3); + assert!(n1_to_n2_dist < n1_to_n3_dist); + assert_eq!(n1_to_n2_dist, desired_n1_to_n2_dist); + assert_eq!(n1_to_n3_dist, desired_n1_to_n3_dist); + } + + #[test] + fn test_closest() { + let mut node_ids: Vec = Vec::new(); + node_ids.push( + NodeId::try_from( + [ + 144, 28, 106, 112, 220, 197, 216, 119, 9, 217, 42, 77, 159, 211, 53, 207, 245, 157, 5, 55, 235, + 247, 160, 195, 240, 48, 146, 168, 119, 15, 241, 54, + ] + .as_bytes(), + ) + .unwrap(), + ); + node_ids.push( + NodeId::try_from( + [ + 75, 249, 102, 1, 2, 166, 155, 37, 22, 54, 84, 98, 56, 62, 242, 115, 238, 149, 12, 239, 231, 217, + 35, 168, 106, 203, 199, 168, 147, 32, 234, 38, + ] + .as_bytes(), + ) + .unwrap(), + ); + node_ids.push( + NodeId::try_from( + [ + 60, 32, 246, 39, 108, 201, 214, 91, 30, 230, 3, 126, 31, 46, 66, 203, 27, 51, 240, 177, 230, 22, + 118, 102, 201, 55, 211, 147, 229, 26, 116, 103, + ] + .as_bytes(), + ) + .unwrap(), + ); + node_ids.push( + NodeId::try_from( + [ + 134, 116, 78, 53, 246, 206, 200, 147, 126, 96, 54, 113, 67, 56, 173, 52, 150, 35, 250, 18, 29, 87, + 231, 228, 125, 49, 95, 53, 103, 250, 54, 214, + ] + .as_bytes(), + ) + .unwrap(), + ); + node_ids.push( + NodeId::try_from( + [ + 75, 146, 162, 130, 22, 63, 247, 182, 156, 103, 174, 32, 134, 97, 41, 240, 180, 116, 2, 142, 53, + 197, 209, 113, 191, 205, 45, 151, 93, 167, 43, 72, + ] + .as_bytes(), + ) + .unwrap(), + ); + node_ids.push( + NodeId::try_from( + [ + 186, 43, 62, 14, 60, 214, 9, 180, 145, 122, 55, 160, 83, 83, 45, 185, 219, 206, 226, 128, 5, 26, + 20, 0, 192, 121, 216, 178, 134, 212, 51, 131, + ] + .as_bytes(), + ) + .unwrap(), + ); + node_ids.push( + NodeId::try_from( + [ + 143, 189, 32, 210, 30, 231, 82, 5, 86, 85, 28, 82, 154, 127, 90, 98, 108, 106, 186, 179, 36, 194, + 246, 209, 17, 244, 126, 108, 104, 187, 204, 213, + ] + .as_bytes(), + ) + .unwrap(), + ); + node_ids.push( + NodeId::try_from( + [ + 155, 210, 214, 160, 153, 70, 172, 234, 177, 178, 62, 82, 166, 202, 71, 205, 139, 247, 170, 91, 234, + 197, 239, 27, 14, 238, 97, 8, 28, 169, 96, 169, + ] + .as_bytes(), + ) + .unwrap(), + ); + node_ids.push( + NodeId::try_from( + [ + 173, 218, 34, 188, 211, 173, 235, 82, 18, 159, 55, 47, 242, 24, 95, 60, 208, 53, 97, 51, 43, 71, + 149, 89, 123, 150, 162, 67, 240, 208, 67, 56, + ] + .as_bytes(), + ) + .unwrap(), + ); + + let node_id = NodeId::try_from( + [ + 169, 125, 200, 137, 210, 73, 241, 238, 25, 108, 8, 48, 66, 29, 2, 117, 1, 252, 36, 214, 252, 38, 207, + 113, 175, 126, 36, 202, 215, 125, 114, 131, + ] + .as_bytes(), + ) + .unwrap(); + let k = 3; + match node_id.closest(&node_ids, k) { + Ok(knn_node_ids) => { + println!(" KNN = {:?}", knn_node_ids); + assert_eq!(knn_node_ids.len(), k); + assert_eq!(knn_node_ids[0].0, [ + 173, 218, 34, 188, 211, 173, 235, 82, 18, 159, 55, 47, 242, 24, 95, 60, 208, 53, 97, 51, 43, 71, + 149, 89, 123, 150, 162, 67, 240, 208, 67, 56 + ]); + assert_eq!(knn_node_ids[1].0, [ + 186, 43, 62, 14, 60, 214, 9, 180, 145, 122, 55, 160, 83, 83, 45, 185, 219, 206, 226, 128, 5, 26, + 20, 0, 192, 121, 216, 178, 134, 212, 51, 131 + ]); + assert_eq!(knn_node_ids[2].0, [ + 143, 189, 32, 210, 30, 231, 82, 5, 86, 85, 28, 82, 154, 127, 90, 98, 108, 106, 186, 179, 36, 194, + 246, 209, 17, 244, 126, 108, 104, 187, 204, 213 + ]); + }, + Err(_e) => assert!(false), + }; + assert!(node_id.closest(&node_ids, node_ids.len() + 1).is_err()); + } + + #[test] + fn partial_eq() { + let bytes = [ + 173, 218, 34, 188, 211, 173, 235, 82, 18, 159, 55, 47, 242, 24, 95, 60, 208, 53, 97, 51, 43, 71, 149, 89, + 123, 150, 162, 67, 240, 208, 67, 56, + ] + .as_bytes(); + let nid1 = NodeId::try_from(bytes.clone()).unwrap(); + let nid2 = NodeId::try_from(bytes.clone()).unwrap(); + + assert_eq!(nid1, nid2); + } +} diff --git a/comms/src/peer_manager/node_identity.rs b/comms/src/peer_manager/node_identity.rs new file mode 100644 index 0000000000..ac7ee284c4 --- /dev/null +++ b/comms/src/peer_manager/node_identity.rs @@ -0,0 +1,169 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::node_id::deserialize_node_id_from_hex; +use crate::{ + connection::NetAddress, + peer_manager::{ + node_id::{NodeId, NodeIdError}, + Peer, + PeerFlags, + }, + types::{CommsPublicKey, CommsSecretKey}, +}; +use derive_error::Error; +use rand::{CryptoRng, Rng}; +use serde::{Deserialize, Serialize}; +use std::sync::RwLock; +use tari_crypto::keys::{PublicKey, SecretKey}; +use tari_utilities::hex::serialize_to_hex; + +#[derive(Debug, Error)] +pub enum NodeIdentityError { + NodeIdError(NodeIdError), + /// The Thread Safety has been breached and the data access has become poisoned + PoisonedAccess, +} + +/// Identity of this node +/// # Fields +/// `identity`: The public identity fields for this node +/// +/// `secret_key`: The secret key corresponding to the public key of this node +/// +/// `control_service_address`: The NetAddress of the local node's Control port +#[derive(Serialize, Deserialize)] +pub struct NodeIdentity { + pub identity: PeerNodeIdentity, + pub secret_key: CommsSecretKey, + control_service_address: RwLock, +} + +impl NodeIdentity { + /// Create a new NodeIdentity from the provided key pair and control service address + pub fn new( + secret_key: CommsSecretKey, + public_key: CommsPublicKey, + control_service_address: NetAddress, + ) -> Result + { + let node_id = NodeId::from_key(&public_key).map_err(NodeIdentityError::NodeIdError)?; + + Ok(NodeIdentity { + identity: PeerNodeIdentity::new(node_id, public_key), + secret_key, + control_service_address: RwLock::new(control_service_address), + }) + } + + /// Generates a new random NodeIdentity for CommsPublicKey + pub fn random(rng: &mut R, control_service_address: NetAddress) -> Result + where R: CryptoRng + Rng { + let secret_key = CommsSecretKey::random(rng); + let public_key = CommsPublicKey::from_secret_key(&secret_key); + let node_id = NodeId::from_key(&public_key).map_err(NodeIdentityError::NodeIdError)?; + + Ok(NodeIdentity { + identity: PeerNodeIdentity::new(node_id, public_key), + secret_key, + control_service_address: RwLock::new(control_service_address), + }) + } + + /// Retrieve the control_service_address + pub fn control_service_address(&self) -> Result { + Ok(self + .control_service_address + .read() + .map_err(|_| NodeIdentityError::PoisonedAccess)? + .clone()) + } + + /// Modify the control_service_address + pub fn set_control_service_address(&self, control_service_address: NetAddress) -> Result<(), NodeIdentityError> { + *self + .control_service_address + .write() + .map_err(|_| NodeIdentityError::PoisonedAccess)? = control_service_address; + Ok(()) + } + + /// This returns a random NodeIdentity for testing purposes. This function can panic. If a control_service_address + /// is None, 127.0.0.1:9000 will be used (i.e. the caller doesn't care what the control_service_address is). + #[cfg(test)] + pub fn random_for_test(control_service_address: Option) -> Self { + use rand::OsRng; + Self::random( + &mut OsRng::new().unwrap(), + control_service_address.or("127.0.0.1:9000".parse().ok()).unwrap(), + ) + .unwrap() + } +} + +impl From for Peer { + fn from(node_identity: NodeIdentity) -> Peer { + Peer::new( + node_identity.identity.public_key, + node_identity.identity.node_id, + node_identity.control_service_address.read().unwrap().clone().into(), + PeerFlags::empty(), + ) + } +} + +impl Clone for NodeIdentity { + fn clone(&self) -> Self { + Self { + identity: self.identity.clone(), + secret_key: self.secret_key.clone(), + control_service_address: RwLock::new(self.control_service_address().unwrap()), + } + } +} + +/// The PeerNodeIdentity is a container that stores the public identity (NodeId, Identification Public Key pair) of a +/// single node +#[derive(Eq, PartialEq, Debug, Serialize, Deserialize, Clone)] +pub struct PeerNodeIdentity { + #[serde(serialize_with = "serialize_to_hex")] + #[serde(deserialize_with = "deserialize_node_id_from_hex")] + pub node_id: NodeId, + pub public_key: CommsPublicKey, +} + +impl PeerNodeIdentity { + /// Construct a new identity for a node that contains its NodeId and identification key pair + pub fn new(node_id: NodeId, public_key: CommsPublicKey) -> PeerNodeIdentity { + PeerNodeIdentity { node_id, public_key } + } +} + +/// Construct a PeerNodeIdentity from a Peer +impl From for PeerNodeIdentity { + fn from(peer: Peer) -> Self { + Self { + public_key: peer.public_key, + node_id: peer.node_id, + } + } +} diff --git a/comms/src/peer_manager/peer.rs b/comms/src/peer_manager/peer.rs new file mode 100644 index 0000000000..ce2e59b9f4 --- /dev/null +++ b/comms/src/peer_manager/peer.rs @@ -0,0 +1,216 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::node_id::deserialize_node_id_from_hex; +use bitflags::*; +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use tari_utilities::hex::serialize_to_hex; + +use crate::{ + connection::{ + net_address::{net_addresses::NetAddressesWithStats, NetAddressWithStats}, + NetAddress, + }, + peer_manager::{node_id::NodeId, PeerManagerError}, + types::CommsPublicKey, +}; +// TODO reputation metric? + +bitflags! { + #[derive(Default, Deserialize, Serialize)] + pub struct PeerFlags: u8 { + const BANNED = 0b0000_0001; + } +} + +/// A Peer represents a communication peer that is identified by a Public Key and NodeId. The Peer struct maintains a +/// collection of the NetAddressesWithStats that this Peer can be reached by. The struct also maintains a set of flags +/// describing the status of the Peer. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct Peer { + pub public_key: CommsPublicKey, + #[serde(serialize_with = "serialize_to_hex")] + #[serde(deserialize_with = "deserialize_node_id_from_hex")] + pub node_id: NodeId, + pub addresses: NetAddressesWithStats, + pub flags: PeerFlags, +} + +impl Peer { + /// Constructs a new peer + pub fn new( + public_key: CommsPublicKey, + node_id: NodeId, + addresses: NetAddressesWithStats, + flags: PeerFlags, + ) -> Peer + { + Peer { + public_key, + node_id, + addresses, + flags, + } + } + + /// Constructs a new peer + pub fn from_public_key_and_address( + public_key: CommsPublicKey, + net_address: NetAddress, + ) -> Result + { + let node_id = NodeId::from_key(&public_key)?; + let addresses = NetAddressesWithStats::new(vec![NetAddressWithStats::new(net_address.clone())]); + + Ok(Peer { + public_key, + node_id, + addresses, + flags: PeerFlags::empty(), + }) + } + + pub fn update( + &mut self, + node_id: Option, + net_addresses: Option>, + flags: Option, + ) + { + if let Some(new_node_id) = node_id { + self.node_id = new_node_id + }; + if let Some(new_net_addresses) = net_addresses { + self.addresses.update_net_addresses(new_net_addresses) + }; + if let Some(new_flags) = flags { + self.flags = new_flags + }; + } + + /// Provides that date time of the last successful interaction with the peer + pub fn last_seen(&self) -> Option> { + self.addresses.last_seen() + } + + /// Returns the ban status of the peer + pub fn is_banned(&self) -> bool { + self.flags.contains(PeerFlags::BANNED) + } + + /// Changes the ban flag bit of the peer + pub fn set_banned(&mut self, ban_flag: bool) { + self.flags.set(PeerFlags::BANNED, ban_flag); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + connection::{net_address::net_addresses::NetAddressesWithStats, NetAddress}, + peer_manager::node_id::NodeId, + types::CommsPublicKey, + }; + use serde_json::Value; + use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + use tari_utilities::{hex::Hex, message_format::MessageFormat}; + + #[test] + fn test_is_and_set_banned() { + let mut rng = rand::OsRng::new().unwrap(); + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let addresses = NetAddressesWithStats::from("123.0.0.123:8000".parse::().unwrap()); + let mut peer: Peer = Peer::new(pk, node_id, addresses, PeerFlags::default()); + assert_eq!(peer.is_banned(), false); + peer.set_banned(true); + assert_eq!(peer.is_banned(), true); + peer.set_banned(false); + assert_eq!(peer.is_banned(), false); + } + + #[test] + fn test_update() { + let mut rng = rand::OsRng::new().unwrap(); + let (_sk, public_key1) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&public_key1).unwrap(); + let net_address1 = "124.0.0.124:7000".parse::().unwrap(); + let mut peer: Peer = Peer::new( + public_key1.clone(), + node_id, + NetAddressesWithStats::from(net_address1.clone()), + PeerFlags::default(), + ); + + let (_sk, public_key2) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id2 = NodeId::from_key(&public_key2).unwrap(); + let net_address2 = "125.0.0.125:8000".parse::().unwrap(); + let net_address3 = "126.0.0.126:9000".parse::().unwrap(); + + peer.update( + Some(node_id2.clone()), + Some(vec![net_address2.clone(), net_address3.clone()]), + Some(PeerFlags::BANNED), + ); + + assert_eq!(peer.public_key, public_key1); + assert_eq!(peer.node_id, node_id2); + assert!(!peer + .addresses + .addresses + .iter() + .any(|net_address_with_stats| net_address_with_stats.net_address == net_address1)); + assert!(peer + .addresses + .addresses + .iter() + .any(|net_address_with_stats| net_address_with_stats.net_address == net_address2)); + assert!(peer + .addresses + .addresses + .iter() + .any(|net_address_with_stats| net_address_with_stats.net_address == net_address3)); + assert_eq!(peer.flags, PeerFlags::BANNED); + } + + #[test] + fn json_ser_der() { + let expected_pk_hex = "02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031"; + let expected_nodeid_hex = "5f517508fdaeef0aeae7b577336731dfb6fe60bbbde363a5712100109b5d0f69"; + let pk = CommsPublicKey::from_hex(expected_pk_hex).unwrap(); + let node_id = NodeId::from_key(&pk).unwrap(); + let peer = Peer::new( + pk, + node_id, + "127.0.0.1:9000".parse::().unwrap().into(), + PeerFlags::empty(), + ); + + let json = peer.to_json().unwrap(); + let json: Value = serde_json::from_str(&json).unwrap(); + + assert_eq!(json["public_key"], expected_pk_hex); + assert_eq!(json["node_id"], expected_nodeid_hex); + } +} diff --git a/infrastructure/comms/src/connection/onion/connection.rs b/comms/src/peer_manager/peer_key.rs similarity index 91% rename from infrastructure/comms/src/connection/onion/connection.rs rename to comms/src/peer_manager/peer_key.rs index cab63eeb04..f645f79f8d 100644 --- a/infrastructure/comms/src/connection/onion/connection.rs +++ b/comms/src/peer_manager/peer_key.rs @@ -20,8 +20,11 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::connection::Connection; +use rand::RngCore; -pub struct OnionConnection {} +pub type PeerKey = u64; -impl Connection for OnionConnection {} +pub fn generate_peer_key(rng: &mut R) -> PeerKey +where R: RngCore { + rng.next_u64() +} diff --git a/comms/src/peer_manager/peer_manager.rs b/comms/src/peer_manager/peer_manager.rs new file mode 100644 index 0000000000..59778695ac --- /dev/null +++ b/comms/src/peer_manager/peer_manager.rs @@ -0,0 +1,489 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::net_address::NetAddress, + outbound_message_service::broadcast_strategy::BroadcastStrategy, + peer_manager::{node_id::NodeId, node_identity::PeerNodeIdentity, peer::Peer, peer_storage::PeerStorage}, + types::{CommsDatabase, CommsPublicKey}, +}; + +use crate::peer_manager::{peer::PeerFlags, PeerManagerError}; +use std::{sync::RwLock, time::Duration}; + +/// The PeerManager consist of a routing table of previously discovered peers. +/// It also provides functionality to add, find and delete peers. A subset of peers can also be requested from the +/// routing table based on the selected Broadcast strategy. +pub struct PeerManager { + peer_storage: RwLock>, +} + +impl PeerManager { + /// Constructs a new empty PeerManager + pub fn new(database: CommsDatabase) -> Result { + Ok(Self { + peer_storage: RwLock::new(PeerStorage::new(database)?), + }) + } + + /// Adds a peer to the routing table of the PeerManager if the peer does not already exist. When a peer already + /// exist, the stored version will be replaced with the newly provided peer. + pub fn add_peer(&self, peer: Peer) -> Result<(), PeerManagerError> { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .add_peer(peer) + } + + pub fn update_peer( + &self, + public_key: &CommsPublicKey, + node_id: Option, + net_addresses: Option>, + flags: Option, + ) -> Result<(), PeerManagerError> + { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .update_peer(public_key, node_id, net_addresses, flags) + } + + /// The peer with the specified public_key will be removed from the PeerManager + pub fn delete_peer(&self, node_id: &NodeId) -> Result<(), PeerManagerError> { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .delete_peer(node_id) + } + + /// Find the peer with the provided NodeID + pub fn find_with_node_id(&self, node_id: &NodeId) -> Result { + self.peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .find_with_node_id(node_id) + } + + /// Find the peer with the provided PublicKey + pub fn find_with_public_key(&self, public_key: &CommsPublicKey) -> Result { + self.peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .find_with_public_key(public_key) + } + + /// Find the peer with the provided NetAddress + pub fn find_with_net_address(&self, net_address: &NetAddress) -> Result { + self.peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .find_with_net_address(net_address) + } + + /// Check if a peer exist using the specified public_key + pub fn exists(&self, public_key: &CommsPublicKey) -> Result { + Ok(self + .peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .exists(public_key)) + } + + /// Check if a peer exist using the specified node_id + pub fn exists_node_id(&self, node_id: &NodeId) -> Result { + Ok(self + .peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .exists_node_id(node_id)) + } + + /// Request a sub-set of peers based on the provided BroadcastStrategy + pub fn get_broadcast_identities( + &self, + broadcast_strategy: BroadcastStrategy, + ) -> Result, PeerManagerError> + { + match broadcast_strategy { + BroadcastStrategy::DirectNodeId(node_id) => { + // Send to a particular peer matching the given node ID + self.peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .direct_identity_node_id(&node_id) + }, + BroadcastStrategy::DirectPublicKey(public_key) => { + // Send to a particular peer matching the given node ID + self.peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .direct_identity_public_key(&public_key) + }, + BroadcastStrategy::Flood => { + // Send to all known Communication Node peers + self.peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .flood_identities() + }, + BroadcastStrategy::Closest(closest_request) => { + // Send to all n nearest neighbour Communication Nodes + self.peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .closest_identities( + &closest_request.node_id, + closest_request.n, + &closest_request.excluded_peers, + ) + }, + BroadcastStrategy::Random(n) => { + // Send to a random set of peers of size n that are Communication Nodes + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .random_identities(n) + }, + } + } + + /// Check if a specific node_id is in the network region of the N nearest neighbours of the region specified by + /// region_node_id + pub fn in_network_region( + &self, + node_id: &NodeId, + region_node_id: &NodeId, + n: usize, + ) -> Result + { + self.peer_storage + .read() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .in_network_region(node_id, region_node_id, n) + } + + /// Thread safe access to peer - Changes the ban flag bit of the peer + pub fn set_banned(&self, node_id: &NodeId, ban_flag: bool) -> Result<(), PeerManagerError> { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .set_banned(node_id, ban_flag) + } + + /// Thread safe access to peer - Adds a new net address to the peer if it doesn't yet exist + pub fn add_net_address(&self, node_id: &NodeId, net_address: &NetAddress) -> Result<(), PeerManagerError> { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .add_net_address(node_id, net_address) + } + + /// Thread safe access to peer - Finds and returns the highest priority net address until all connection attempts + /// for each net address have been reached + pub fn get_best_net_address(&self, node_id: &NodeId) -> Result { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .get_best_net_address(node_id) + } + + /// Thread safe access to peer - The average connection latency of the provided net address will be updated to + /// include the current measured latency sample + pub fn update_latency( + &self, + net_address: &NetAddress, + latency_measurement: Duration, + ) -> Result<(), PeerManagerError> + { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .update_latency(net_address, latency_measurement) + } + + /// Thread safe access to peer - Mark that a message was received from the specified net address + pub fn mark_message_received(&self, net_address: &NetAddress) -> Result<(), PeerManagerError> { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .mark_message_received(net_address) + } + + /// Thread safe access to peer - Mark that a rejected message was received from the specified net address + pub fn mark_message_rejected(&self, net_address: &NetAddress) -> Result<(), PeerManagerError> { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .mark_message_rejected(net_address) + } + + /// Thread safe access to peer - Mark that a successful connection was established with the specified net address + pub fn mark_successful_connection_attempt(&self, net_address: &NetAddress) -> Result<(), PeerManagerError> { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .mark_successful_connection_attempt(net_address) + } + + /// Thread safe access to peer - Mark that a connection could not be established with the specified net address + pub fn mark_failed_connection_attempt(&self, net_address: &NetAddress) -> Result<(), PeerManagerError> { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .mark_failed_connection_attempt(net_address) + } + + /// Thread safe access to peer - Reset all connection attempts on all net addresses for peer + pub fn reset_connection_attempts(&self, node_id: &NodeId) -> Result<(), PeerManagerError> { + self.peer_storage + .write() + .map_err(|_| PeerManagerError::PoisonedAccess)? + .reset_connection_attempts(node_id) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + connection::net_address::{net_addresses::NetAddressesWithStats, NetAddress}, + outbound_message_service::broadcast_strategy::ClosestRequest, + peer_manager::{ + node_id::NodeId, + peer::{Peer, PeerFlags}, + }, + }; + use rand::OsRng; + use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + use tari_storage::HMapDatabase; + + fn create_test_peer(rng: &mut OsRng, ban_flag: bool) -> Peer { + let (_sk, pk) = RistrettoPublicKey::random_keypair(rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let net_addresses = NetAddressesWithStats::from("1.2.3.4:8000".parse::().unwrap()); + let mut peer = Peer::new(pk, node_id, net_addresses, PeerFlags::default()); + peer.set_banned(ban_flag); + peer + } + + #[test] + fn test_get_broadcast_identities() { + // Create peer manager with random peers + let peer_manager = PeerManager::new(HMapDatabase::new()).unwrap(); + let mut test_peers: Vec = Vec::new(); + // Create 20 peers were the 1st and last one is bad + let mut rng = rand::OsRng::new().unwrap(); + test_peers.push(create_test_peer(&mut rng, true)); + assert!(peer_manager.add_peer(test_peers[test_peers.len() - 1].clone()).is_ok()); + for _i in 0..18 { + test_peers.push(create_test_peer(&mut rng, false)); + assert!(peer_manager.add_peer(test_peers[test_peers.len() - 1].clone()).is_ok()); + } + test_peers.push(create_test_peer(&mut rng, true)); + assert!(peer_manager.add_peer(test_peers[test_peers.len() - 1].clone()).is_ok()); + + // Test Valid Direct + let identities = peer_manager + .get_broadcast_identities(BroadcastStrategy::DirectNodeId(test_peers[2].node_id.clone())) + .unwrap(); + assert_eq!(identities.len(), 1); + assert_eq!(identities[0].node_id, test_peers[2].node_id); + assert_eq!(identities[0].public_key, test_peers[2].public_key); + // Test Invalid Direct + let unmanaged_peer = create_test_peer(&mut rng, false); + assert!(peer_manager + .get_broadcast_identities(BroadcastStrategy::DirectNodeId(unmanaged_peer.node_id.clone())) + .is_err()); + + // Test Flood + let identities = peer_manager.get_broadcast_identities(BroadcastStrategy::Flood).unwrap(); + assert_eq!(identities.len(), 18); + for peer_identity in &identities { + assert_eq!( + peer_manager + .find_with_node_id(&peer_identity.node_id) + .unwrap() + .is_banned(), + false + ); + } + + // Test Closest - No exclusions + let identities = peer_manager + .get_broadcast_identities(BroadcastStrategy::Closest(ClosestRequest { + n: 3, + node_id: unmanaged_peer.node_id.clone(), + excluded_peers: Vec::new(), + })) + .unwrap(); + assert_eq!(identities.len(), 3); + // Remove current identity nodes from test peers + let mut unused_peers: Vec = Vec::new(); + for peer in &test_peers { + if !identities + .iter() + .any(|peer_identity| peer.node_id == peer_identity.node_id || peer.is_banned()) + { + unused_peers.push(peer.clone()); + } + } + // Check that none of the remaining unused peers have smaller distances compared to the selected peers + for peer_identity in &identities { + let selected_dist = unmanaged_peer.node_id.distance(&peer_identity.node_id); + for unused_peer in &unused_peers { + let unused_dist = unmanaged_peer.node_id.distance(&unused_peer.node_id); + assert!(unused_dist > selected_dist); + } + } + + // Test Closest - With an exclusion + let excluded_peers = vec![ + identities[0].public_key.clone(), // ,identities[1].public_key.clone() + ]; + let identities = peer_manager + .get_broadcast_identities(BroadcastStrategy::Closest(ClosestRequest { + n: 3, + node_id: unmanaged_peer.node_id.clone(), + excluded_peers: excluded_peers.clone(), + })) + .unwrap(); + println!("identities.len()={:?}", identities.len()); + assert_eq!(identities.len(), 3); + // Remove current identity nodes from test peers + let mut unused_peers: Vec = Vec::new(); + for peer in &test_peers { + if !identities.iter().any(|peer_identity| { + peer.node_id == peer_identity.node_id || peer.is_banned() || excluded_peers.contains(&peer.public_key) + }) { + unused_peers.push(peer.clone()); + } + } + // Check that none of the remaining unused peers have smaller distances compared to the selected peers + for peer_identity in &identities { + let selected_dist = unmanaged_peer.node_id.distance(&peer_identity.node_id); + for unused_peer in &unused_peers { + let unused_dist = unmanaged_peer.node_id.distance(&unused_peer.node_id); + assert!(unused_dist > selected_dist); + } + assert!(!excluded_peers.contains(&peer_identity.public_key)); + } + + // Test Random + let identities1 = peer_manager + .get_broadcast_identities(BroadcastStrategy::Random(10)) + .unwrap(); + let identities2 = peer_manager + .get_broadcast_identities(BroadcastStrategy::Random(10)) + .unwrap(); + assert_ne!(identities1, identities2); + } + + #[test] + fn test_in_network_region() { + let mut rng = rand::OsRng::new().unwrap(); + // Create peer manager with random peers + let peer_manager = PeerManager::new(HMapDatabase::new()).unwrap(); + let network_region_node_id = create_test_peer(&mut rng, false).node_id; + // Create peers + let mut test_peers: Vec = Vec::new(); + for _ in 0..10 { + test_peers.push(create_test_peer(&mut rng, false)); + assert!(peer_manager.add_peer(test_peers[test_peers.len() - 1].clone()).is_ok()); + } + test_peers[0].set_banned(true); + test_peers[1].set_banned(true); + + // Get nearest neighbours + let n = 5; + let nearest_identities = peer_manager + .get_broadcast_identities(BroadcastStrategy::Closest(ClosestRequest { + n, + node_id: network_region_node_id.clone(), + excluded_peers: Vec::new(), + })) + .unwrap(); + + for peer in &test_peers { + if nearest_identities + .iter() + .any(|peer_identity| peer.node_id == peer_identity.node_id) + { + assert!(peer_manager + .in_network_region(&peer.node_id, &network_region_node_id, n) + .unwrap()); + } else { + assert!(!peer_manager + .in_network_region(&peer.node_id, &network_region_node_id, n) + .unwrap()); + } + } + } + + #[test] + fn test_peer_reset_connection_attempts() { + // Create peer manager with random peers + let peer_manager = PeerManager::new(HMapDatabase::new()).unwrap(); + let mut rng = rand::OsRng::new().unwrap(); + let peer = create_test_peer(&mut rng, false); + peer_manager.add_peer(peer.clone()).unwrap(); + + peer_manager + .mark_failed_connection_attempt(&peer.addresses.addresses[0].clone().as_net_address()) + .unwrap(); + peer_manager + .mark_failed_connection_attempt(&peer.addresses.addresses[0].clone().as_net_address()) + .unwrap(); + assert_eq!( + peer_manager + .find_with_node_id(&peer.node_id.clone()) + .unwrap() + .addresses + .addresses[0] + .connection_attempts, + 2 + ); + peer_manager.reset_connection_attempts(&peer.node_id.clone()).unwrap(); + assert_eq!( + peer_manager + .find_with_node_id(&peer.node_id.clone()) + .unwrap() + .addresses + .addresses[0] + .connection_attempts, + 0 + ); + } + + #[test] + fn test_adding_and_searching_by_net_address() { + // Create peer manager with random peers + let peer_manager = PeerManager::new(HMapDatabase::new()).unwrap(); + let mut rng = rand::OsRng::new().unwrap(); + let peer = create_test_peer(&mut rng, false); + peer_manager.add_peer(peer.clone()).unwrap(); + // Test NetAddress adding and searching + let net_address = NetAddress::from("1.2.3.4:7000".parse::().unwrap()); + assert!(peer_manager.add_net_address(&peer.node_id, &net_address).is_ok()); + assert!(peer_manager.find_with_net_address(&net_address).is_ok()); + } +} diff --git a/comms/src/peer_manager/peer_storage.rs b/comms/src/peer_manager/peer_storage.rs new file mode 100644 index 0000000000..c7885e47e9 --- /dev/null +++ b/comms/src/peer_manager/peer_storage.rs @@ -0,0 +1,855 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + connection::net_address::NetAddress, + peer_manager::{ + node_id::{NodeDistance, NodeId}, + node_identity::PeerNodeIdentity, + peer::{Peer, PeerFlags}, + peer_key::{generate_peer_key, PeerKey}, + PeerManagerError, + }, + types::{CommsPublicKey, CommsRng}, +}; +use rand::Rng; +use std::{cmp::min, collections::HashMap, time::Duration}; +use tari_storage::KeyValueStore; + +/// PeerStorage provides a mechanism to keep a datastore and a local copy of all peers in sync and allow fast searches +/// using the node_id, public key or net_address of a peer. +pub struct PeerStorage { + pub(crate) peers: DS, + node_id_hm: HashMap, + public_key_hm: HashMap, + net_address_hm: HashMap, + rng: CommsRng, +} + +impl PeerStorage +where DS: KeyValueStore +{ + /// Constructs a new empty PeerStorage system + pub fn new(database: DS) -> Result, PeerManagerError> { + // Restore peers and hashmap links from database + let mut node_id_hm: HashMap = HashMap::new(); + let mut public_key_hm: HashMap = HashMap::new(); + let mut net_address_hm: HashMap = HashMap::new(); + database + .for_each(|pair| { + let (peer_key, peer) = pair.unwrap(); + node_id_hm.insert(peer.node_id.clone(), peer_key); + public_key_hm.insert(peer.public_key.clone(), peer_key); + for net_address_with_stats in &peer.addresses.addresses { + net_address_hm.insert(net_address_with_stats.net_address.clone(), peer_key); + } + }) + .map_err(PeerManagerError::DatabaseError)?; + + Ok(PeerStorage { + peers: database, + node_id_hm, + public_key_hm, + net_address_hm, + rng: CommsRng::new().map_err(|_| PeerManagerError::RngError)?, + }) + } + + /// Adds a peer to the routing table of the PeerManager if the peer does not already exist. When a peer already + /// exists, the stored version will be replaced with the newly provided peer. + pub fn add_peer(&mut self, peer: Peer) -> Result<(), PeerManagerError> { + match self.public_key_hm.get(&peer.public_key) { + Some(&peer_key) => { + // Replace existing entry + self.remove_hashmap_links(peer_key)?; + self.add_hashmap_links(peer_key, &peer); + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError)?; + Ok(()) + }, + None => { + // Add new entry + let peer_key = generate_peer_key(&mut self.rng); // Generate new random peer key + self.add_hashmap_links(peer_key, &peer); + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError)?; + Ok(()) + }, + } + } + + /// Adds a peer to the routing table of the PeerManager if the peer does not already exist. When a peer already + /// exist, the stored version will be replaced with the newly provided peer. + pub fn update_peer( + &mut self, + public_key: &CommsPublicKey, + node_id: Option, + net_addresses: Option>, + flags: Option, + ) -> Result<(), PeerManagerError> + { + match self.public_key_hm.get(public_key) { + Some(peer_key) => { + let peer_key = *peer_key; + self.remove_hashmap_links(peer_key)?; + + let mut stored_peer: Peer = self + .peers + .get(&peer_key) + .map_err(|e| PeerManagerError::DatabaseError(e))? + .ok_or(PeerManagerError::PeerNotFoundError)?; + stored_peer.update(node_id, net_addresses, flags); + + self.add_hashmap_links(peer_key, &stored_peer); + self.peers + .insert(peer_key, stored_peer) + .map_err(|e| PeerManagerError::DatabaseError(e))?; + Ok(()) + }, + None => Err(PeerManagerError::PeerNotFoundError), + } + } + + /// The peer with the specified public_key will be removed from the PeerManager + pub fn delete_peer(&mut self, node_id: &NodeId) -> Result<(), PeerManagerError> { + let peer_key = *self + .node_id_hm + .get(&node_id) + .ok_or(PeerManagerError::PeerNotFoundError)?; + self.remove_hashmap_links(peer_key)?; + self.peers.delete(&peer_key).map_err(PeerManagerError::DatabaseError)?; + Ok(()) + } + + /// Add key pairs to the search hashmaps for a newly added or moved peer + fn add_hashmap_links(&mut self, peer_key: PeerKey, peer: &Peer) { + self.node_id_hm.insert(peer.node_id.clone(), peer_key); + self.public_key_hm.insert(peer.public_key.clone(), peer_key); + for net_address_with_stats in &peer.addresses.addresses { + self.net_address_hm + .insert(net_address_with_stats.net_address.clone(), peer_key); + } + } + + /// Remove the peer specified by a given index from the database and remove hashmap keys + fn remove_hashmap_links(&mut self, peer_key: PeerKey) -> Result<(), PeerManagerError> { + let peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + self.node_id_hm.remove(&peer.node_id); + self.public_key_hm.remove(&peer.public_key); + for net_address_with_stats in &peer.addresses.addresses { + self.net_address_hm.remove(&net_address_with_stats.net_address); + } + Ok(()) + } + + /// Find the peer with the provided NodeID + pub fn find_with_node_id(&self, node_id: &NodeId) -> Result { + let peer_key = *self + .node_id_hm + .get(&node_id) + .ok_or(PeerManagerError::PeerNotFoundError)?; + self.peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError) + } + + /// Find the peer with the provided PublicKey + pub fn find_with_public_key(&self, public_key: &CommsPublicKey) -> Result { + let peer_key = *self + .public_key_hm + .get(&public_key) + .ok_or(PeerManagerError::PeerNotFoundError)?; + self.peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError) + } + + /// Find the peer with the provided NetAddress + pub fn find_with_net_address(&self, net_address: &NetAddress) -> Result { + let peer_key = *self + .net_address_hm + .get(&net_address) + .ok_or(PeerManagerError::PeerNotFoundError)?; + self.peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError) + } + + /// Check if a peer exist using the specified public_key + pub fn exists(&self, public_key: &CommsPublicKey) -> bool { + self.public_key_hm.get(&public_key).is_some() + } + + /// Check if a peer exist using the specified node_id + pub fn exists_node_id(&self, node_id: &NodeId) -> bool { + self.node_id_hm.get(&node_id).is_some() + } + + /// Constructs a single NodeIdentity for the peer corresponding to the provided NodeId + pub fn direct_identity_node_id(&self, node_id: &NodeId) -> Result, PeerManagerError> { + let peer_key = *self + .node_id_hm + .get(&node_id) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + if peer.is_banned() { + Err(PeerManagerError::BannedPeer) + } else { + Ok(vec![PeerNodeIdentity::new(node_id.clone(), peer.public_key.clone())]) + } + } + + /// Constructs a single NodeIdentity for the peer corresponding to the provided NodeId + pub fn direct_identity_public_key( + &self, + public_key: &CommsPublicKey, + ) -> Result, PeerManagerError> + { + let peer_key = *self + .public_key_hm + .get(&public_key) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + if peer.is_banned() { + Err(PeerManagerError::BannedPeer) + } else { + Ok(vec![PeerNodeIdentity::new(peer.node_id.clone(), public_key.clone())]) + } + } + + /// Compile a list of all known node identities that can be used for the flood BroadcastStrategy + pub fn flood_identities(&self) -> Result, PeerManagerError> { + // TODO: this list should only contain Communication Nodes + let mut identities: Vec = Vec::new(); + self.peers + .for_each(|pair| { + let (_, peer) = pair.unwrap(); + if !peer.is_banned() { + identities.push(PeerNodeIdentity::new(peer.node_id.clone(), peer.public_key.clone())); + } + }) + .map_err(PeerManagerError::DatabaseError)?; + Ok(identities) + } + + /// Compile a list of node identities that can be used for the closest BroadcastStrategy + pub fn closest_identities( + &self, + node_id: &NodeId, + n: usize, + excluded_peers: &Vec, + ) -> Result, PeerManagerError> + { + let mut peer_keys: Vec = Vec::new(); + let mut dists: Vec = Vec::new(); + self.peers + .for_each(|pair| { + let (peer_key, peer) = pair.unwrap(); + if !peer.is_banned() && !excluded_peers.contains(&peer.public_key) { + peer_keys.push(peer_key); + dists.push(node_id.distance(&peer.node_id)); + } + }) + .map_err(PeerManagerError::DatabaseError)?; + // Use all available peers up to a maximum of N + let max_available = min(peer_keys.len(), n); + if max_available > 0 { + // Perform partial sort of elements only up to N elements + let mut nearest_identities: Vec = Vec::with_capacity(max_available); + for i in 0..max_available { + for j in (i + 1)..peer_keys.len() { + if dists[i] > dists[j] { + dists.swap(i, j); + peer_keys.swap(i, j); + } + } + let peer: Peer = self + .peers + .get(&peer_keys[i]) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + nearest_identities.push(PeerNodeIdentity::new(peer.node_id.clone(), peer.public_key.clone())); + } + Ok(nearest_identities) + } else { + Ok(Vec::new()) + } + } + + /// Compile a list of node identities that can be used for the random BroadcastStrategy + pub fn random_identities(&mut self, n: usize) -> Result, PeerManagerError> { + // TODO: Send to a random set of Communication Nodes + let mut peer_keys: Vec = Vec::new(); + self.peers + .for_each(|pair| { + let (peer_key, peer) = pair.unwrap(); + if !peer.is_banned() { + peer_keys.push(peer_key); + } + }) + .map_err(PeerManagerError::DatabaseError)?; + + // Use all available peers up to a maximum of N + let max_available = min(peer_keys.len(), n); + if max_available > 0 { + // Shuffle first n elements + for i in 0..max_available { + let j = self.rng.gen_range(0, peer_keys.len()); + peer_keys.swap(i, j); + } + // Compile list of first n shuffled elements + let mut random_identities: Vec = Vec::with_capacity(max_available); + for i in 0..max_available { + let peer: Peer = self + .peers + .get(&peer_keys[i]) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + random_identities.push(PeerNodeIdentity::new(peer.node_id.clone(), peer.public_key.clone())); + } + Ok(random_identities) + } else { + Ok(Vec::new()) + } + } + + /// Check if a specific node_id is in the network region of the N nearest neighbours of the region specified by + /// region_node_id + pub fn in_network_region( + &self, + node_id: &NodeId, + region_node_id: &NodeId, + n: usize, + ) -> Result + { + let region2node_dist = region_node_id.distance(node_id); + let mut dists: Vec = vec![NodeDistance::max_distance(); n]; + let last_index = dists.len() - 1; + self.peers + .for_each(|pair| { + let (_, peer) = pair.unwrap(); + if !peer.is_banned() { + let curr_dist = region_node_id.distance(&peer.node_id); + for i in 0..dists.len() { + if dists[i] > curr_dist { + dists.insert(i, curr_dist.clone()); + dists.pop(); + break; + } + } + if region2node_dist > dists[last_index] { + return; + } + } + }) + .map_err(PeerManagerError::DatabaseError)?; + Ok(region2node_dist <= dists[last_index]) + } + + /// Enables Thread safe access - Changes the ban flag bit of the peer + pub fn set_banned(&mut self, node_id: &NodeId, ban_flag: bool) -> Result<(), PeerManagerError> { + let peer_key = *self + .node_id_hm + .get(&node_id) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let mut peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + peer.set_banned(ban_flag); + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError) + } + + /// Enables Thread safe access - Adds a new net address to the peer if it doesn't yet exist + pub fn add_net_address(&mut self, node_id: &NodeId, net_address: &NetAddress) -> Result<(), PeerManagerError> { + let peer_key = *self + .node_id_hm + .get(&node_id) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let mut peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + peer.addresses.add_net_address(net_address); + self.net_address_hm.insert(net_address.clone(), peer_key); + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError) + } + + /// Enables Thread safe access - Finds and returns the highest priority net address until all connection attempts + /// for each net address have been reached + pub fn get_best_net_address(&mut self, node_id: &NodeId) -> Result { + let peer_key = *self + .node_id_hm + .get(&node_id) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let mut peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + let best_net_address = peer + .addresses + .get_best_net_address() + .map_err(PeerManagerError::NetAddressError)?; + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError)?; + Ok(best_net_address) + } + + /// Enables Thread safe access - The average connection latency of the provided net address will be updated to + /// include the current measured latency sample + pub fn update_latency( + &mut self, + net_address: &NetAddress, + latency_measurement: Duration, + ) -> Result<(), PeerManagerError> + { + let peer_key = *self + .net_address_hm + .get(&net_address) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let mut peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + peer.addresses + .update_latency(net_address, latency_measurement) + .map_err(PeerManagerError::NetAddressError)?; + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError) + } + + /// Enables Thread safe access - Mark that a message was received from the specified net address + pub fn mark_message_received(&mut self, net_address: &NetAddress) -> Result<(), PeerManagerError> { + let peer_key = *self + .net_address_hm + .get(&net_address) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let mut peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + peer.addresses + .mark_message_received(net_address) + .map_err(PeerManagerError::NetAddressError)?; + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError) + } + + /// Enables Thread safe access - Mark that a rejected message was received from the specified net address + pub fn mark_message_rejected(&mut self, net_address: &NetAddress) -> Result<(), PeerManagerError> { + let peer_key = *self + .net_address_hm + .get(&net_address) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let mut peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + peer.addresses + .mark_message_rejected(net_address) + .map_err(PeerManagerError::NetAddressError)?; + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError) + } + + /// Enables Thread safe access - Mark that a successful connection was established with the specified net address + pub fn mark_successful_connection_attempt(&mut self, net_address: &NetAddress) -> Result<(), PeerManagerError> { + let peer_key = *self + .net_address_hm + .get(&net_address) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let mut peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + peer.addresses + .mark_successful_connection_attempt(net_address) + .map_err(PeerManagerError::NetAddressError)?; + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError) + } + + /// Enables Thread safe access - Mark that a connection could not be established with the specified net address + pub fn mark_failed_connection_attempt(&mut self, net_address: &NetAddress) -> Result<(), PeerManagerError> { + let peer_key = *self + .net_address_hm + .get(&net_address) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let mut peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + peer.addresses + .mark_failed_connection_attempt(net_address) + .map_err(PeerManagerError::NetAddressError)?; + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError) + } + + /// Enables Thread safe access - Finds a peer and if it exists resets all connection attempts on all net address + /// belonging to that peer + pub fn reset_connection_attempts(&mut self, node_id: &NodeId) -> Result<(), PeerManagerError> { + let peer_key = *self + .node_id_hm + .get(&node_id) + .ok_or(PeerManagerError::PeerNotFoundError)?; + let mut peer: Peer = self + .peers + .get(&peer_key) + .map_err(PeerManagerError::DatabaseError)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + peer.addresses.reset_connection_attempts(); + self.peers + .insert(peer_key, peer) + .map_err(PeerManagerError::DatabaseError) + } + + /// Returns the DataStore underlying PeerStorage if one exists + pub fn into_datastore(self) -> DS { + self.peers + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + connection::net_address::{net_addresses::NetAddressesWithStats, NetAddress}, + peer_manager::peer::PeerFlags, + }; + use std::{path::PathBuf, sync::Arc}; + use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + use tari_storage::{ + lmdb_store::{LMDBBuilder, LMDBError, LMDBStore}, + HMapDatabase, + LMDBWrapper, + }; + + fn get_path(name: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name); + path.to_str().unwrap().to_string() + } + + fn init_datastore(name: &str) -> Result { + let path = get_path(name); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + LMDBBuilder::new() + .set_path(&path) + .set_environment_size(10) + .set_max_number_of_databases(2) + .add_database(name, lmdb_zero::db::CREATE) + .build() + } + + fn clean_up_datastore(name: &str) { + std::fs::remove_dir_all(get_path(name)).unwrap(); + } + + #[test] + fn test_restore() { + // Create Peers + let mut rng = rand::OsRng::new().unwrap(); + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let net_address1 = NetAddress::from("1.2.3.4:8000".parse::().unwrap()); + let net_address2 = NetAddress::from("5.6.7.8:8000".parse::().unwrap()); + let net_address3 = NetAddress::from("5.6.7.8:7000".parse::().unwrap()); + let mut net_addresses = NetAddressesWithStats::from(net_address1.clone()); + net_addresses.add_net_address(&net_address2); + net_addresses.add_net_address(&net_address3); + let peer1 = Peer::new(pk, node_id, net_addresses, PeerFlags::default()); + + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let net_address4 = NetAddress::from("9.10.11.12:7000".parse::().unwrap()); + let net_addresses = NetAddressesWithStats::from(net_address4.clone()); + let peer2: Peer = Peer::new(pk, node_id, net_addresses, PeerFlags::default()); + + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let net_address5 = NetAddress::from("13.14.15.16:6000".parse::().unwrap()); + let net_address6 = NetAddress::from("17.18.19.20:8000".parse::().unwrap()); + let mut net_addresses = NetAddressesWithStats::from(net_address5.clone()); + net_addresses.add_net_address(&net_address6); + let peer3 = Peer::new(pk, node_id, net_addresses, PeerFlags::default()); + + // Create new datastore with a peer database + let database_name = "pm_test_restore"; // Note: every test should have unique database + { + let datastore = init_datastore(database_name).unwrap(); + let peer_database = datastore.get_handle(database_name).unwrap(); + let db = LMDBWrapper::new(Arc::new(peer_database)); + let mut peer_storage = PeerStorage::new(db).unwrap(); + + // Test adding and searching for peers + assert!(peer_storage.add_peer(peer1.clone()).is_ok()); + assert!(peer_storage.add_peer(peer2.clone()).is_ok()); + assert!(peer_storage.add_peer(peer3.clone()).is_ok()); + + assert_eq!(peer_storage.peers.size().unwrap(), 3); + assert!(peer_storage.find_with_public_key(&peer1.public_key).is_ok()); + assert!(peer_storage.find_with_public_key(&peer2.public_key).is_ok()); + assert!(peer_storage.find_with_public_key(&peer3.public_key).is_ok()); + } + // Restore from existing database + let datastore = init_datastore(database_name).unwrap(); + let peer_database = datastore.get_handle(database_name).unwrap(); + let db = LMDBWrapper::new(Arc::new(peer_database)); + let peer_storage = PeerStorage::new(db).unwrap(); + + assert_eq!(peer_storage.peers.size().unwrap(), 3); + assert!(peer_storage.find_with_public_key(&peer1.public_key).is_ok()); + assert!(peer_storage.find_with_public_key(&peer2.public_key).is_ok()); + assert!(peer_storage.find_with_public_key(&peer3.public_key).is_ok()); + + clean_up_datastore(database_name); + } + + #[test] + fn test_add_delete_find_peer() { + let mut peer_storage = PeerStorage::new(HMapDatabase::new()).unwrap(); + + // Create Peers + let mut rng = rand::OsRng::new().unwrap(); + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let net_address1 = NetAddress::from("1.2.3.4:8000".parse::().unwrap()); + let net_address2 = NetAddress::from("5.6.7.8:8000".parse::().unwrap()); + let net_address3 = NetAddress::from("5.6.7.8:7000".parse::().unwrap()); + let mut net_addresses = NetAddressesWithStats::from(net_address1.clone()); + net_addresses.add_net_address(&net_address2); + net_addresses.add_net_address(&net_address3); + let peer1 = Peer::new(pk, node_id, net_addresses, PeerFlags::default()); + + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let net_address4 = NetAddress::from("9.10.11.12:7000".parse::().unwrap()); + let net_addresses = NetAddressesWithStats::from(net_address4.clone()); + let peer2: Peer = Peer::new(pk, node_id, net_addresses, PeerFlags::default()); + + let (_sk, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let node_id = NodeId::from_key(&pk).unwrap(); + let net_address5 = NetAddress::from("13.14.15.16:6000".parse::().unwrap()); + let net_address6 = NetAddress::from("17.18.19.20:8000".parse::().unwrap()); + let mut net_addresses = NetAddressesWithStats::from(net_address5.clone()); + net_addresses.add_net_address(&net_address6); + let peer3 = Peer::new(pk, node_id, net_addresses, PeerFlags::default()); + // Test adding and searching for peers + assert!(peer_storage.add_peer(peer1.clone()).is_ok()); + assert!(peer_storage.add_peer(peer2.clone()).is_ok()); + assert!(peer_storage.add_peer(peer3.clone()).is_ok()); + + assert_eq!(peer_storage.peers.len().unwrap(), 3); + + assert_eq!( + peer_storage.find_with_public_key(&peer1.public_key).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_public_key(&peer2.public_key).unwrap().public_key, + peer2.public_key + ); + assert_eq!( + peer_storage.find_with_public_key(&peer3.public_key).unwrap().public_key, + peer3.public_key + ); + + assert_eq!( + peer_storage.find_with_node_id(&peer1.node_id).unwrap().node_id, + peer1.node_id + ); + assert_eq!( + peer_storage.find_with_node_id(&peer2.node_id).unwrap().node_id, + peer2.node_id + ); + assert_eq!( + peer_storage.find_with_node_id(&peer3.node_id).unwrap().node_id, + peer3.node_id + ); + + assert_eq!( + peer_storage.find_with_net_address(&net_address1).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address2).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address3).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address4).unwrap().public_key, + peer2.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address5).unwrap().public_key, + peer3.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address6).unwrap().public_key, + peer3.public_key + ); + + assert!(peer_storage.find_with_public_key(&peer1.public_key).is_ok()); + assert!(peer_storage.find_with_public_key(&peer2.public_key).is_ok()); + assert!(peer_storage.find_with_public_key(&peer3.public_key).is_ok()); + + // Test delete of border case peer + assert!(peer_storage.delete_peer(&peer3.node_id).is_ok()); + + assert_eq!(peer_storage.peers.len().unwrap(), 2); + + assert_eq!( + peer_storage.find_with_public_key(&peer1.public_key).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_public_key(&peer2.public_key).unwrap().public_key, + peer2.public_key + ); + assert!(peer_storage.find_with_public_key(&peer3.public_key).is_err()); + + assert_eq!( + peer_storage.find_with_node_id(&peer1.node_id).unwrap().node_id, + peer1.node_id + ); + assert_eq!( + peer_storage.find_with_node_id(&peer2.node_id).unwrap().node_id, + peer2.node_id + ); + assert!(peer_storage.find_with_node_id(&peer3.node_id).is_err()); + + assert_eq!( + peer_storage.find_with_net_address(&net_address1).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address2).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address3).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address4).unwrap().public_key, + peer2.public_key + ); + assert!(peer_storage.find_with_net_address(&net_address5).is_err()); + assert!(peer_storage.find_with_net_address(&net_address6).is_err()); + + assert!(peer_storage.find_with_public_key(&peer1.public_key).is_ok()); + assert!(peer_storage.find_with_public_key(&peer2.public_key).is_ok()); + assert!(peer_storage.find_with_public_key(&peer3.public_key).is_err()); + + // Test of delete with moving behaviour + assert!(peer_storage.add_peer(peer3.clone()).is_ok()); + assert!(peer_storage.delete_peer(&peer2.node_id).is_ok()); + + assert_eq!(peer_storage.peers.len().unwrap(), 2); + + assert_eq!( + peer_storage.find_with_public_key(&peer1.public_key).unwrap().public_key, + peer1.public_key + ); + assert!(peer_storage.find_with_public_key(&peer2.public_key).is_err()); + assert_eq!( + peer_storage.find_with_public_key(&peer3.public_key).unwrap().public_key, + peer3.public_key + ); + + assert_eq!( + peer_storage.find_with_node_id(&peer1.node_id).unwrap().node_id, + peer1.node_id + ); + assert!(peer_storage.find_with_node_id(&peer2.node_id).is_err()); + assert_eq!( + peer_storage.find_with_node_id(&peer3.node_id).unwrap().node_id, + peer3.node_id + ); + + assert_eq!( + peer_storage.find_with_net_address(&net_address1).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address2).unwrap().public_key, + peer1.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address3).unwrap().public_key, + peer1.public_key + ); + assert!(peer_storage.find_with_net_address(&net_address4).is_err()); + assert_eq!( + peer_storage.find_with_net_address(&net_address5).unwrap().public_key, + peer3.public_key + ); + assert_eq!( + peer_storage.find_with_net_address(&net_address6).unwrap().public_key, + peer3.public_key + ); + + assert!(peer_storage.find_with_public_key(&peer1.public_key).is_ok()); + assert!(peer_storage.find_with_public_key(&peer2.public_key).is_err()); + assert!(peer_storage.find_with_public_key(&peer3.public_key).is_ok()); + } +} diff --git a/comms/src/pub_sub_channel.rs b/comms/src/pub_sub_channel.rs new file mode 100644 index 0000000000..8c9294831a --- /dev/null +++ b/comms/src/pub_sub_channel.rs @@ -0,0 +1,200 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// use super::async_::channel as async_channel; +// use super::async_::Publisher; +// use super::async_::Subscriber; +use bus_queue::{bounded, Publisher, Subscriber}; +use futures::{compat::Compat, future, prelude::*, stream::Fuse}; +use std::fmt::Debug; + +/// The container for a message that is passed along the pub-sub channel that contains a Topic to define the type of +/// message and the message itself. +#[derive(Debug)] +pub struct TopicPayload { + topic: T, + message: M, +} + +impl TopicPayload { + pub fn new(topic: T, message: M) -> Self { + Self { topic, message } + } +} + +pub type TopicPublisher = Publisher>; +pub type TopicSubscriber = Subscriber>; + +pub struct TopicSubscriptionFactory { + subscriber: TopicSubscriber, +} + +impl TopicSubscriptionFactory +where + T: Eq + Send, + M: Clone + Send, +{ + pub fn new(subscriber: TopicSubscriber) -> Self { + TopicSubscriptionFactory { subscriber } + } + + /// Provide a subscriber (which will be consumed) and a topic to filter it by and this function will return a stream + /// that yields only the desired messages + pub fn get_subscription(&self, topic: T) -> impl Stream { + self.subscriber.clone().filter_map(move |item| { + let result = if item.topic == topic { + Some(item.message.clone()) + } else { + None + }; + future::ready(result) + }) + } + + /// Provide a Compat wrapped version of the subscription stream for things that want to consume old-style streams + pub fn get_subscription_compat(&self, topic: T) -> Compat>> { + self.get_subscription(topic).map(|i| Ok(i)).compat() + } + + /// Provide a fused version of the subscription stream so that domain modules don't need to know about fuse() + pub fn get_subscription_fused(&self, topic: T) -> Fuse> { + self.get_subscription(topic).fuse() + } +} + +/// Create Topic based Pub-Sub channel which returns the Publisher side of the channel and TopicSubscriptionFactory +/// which can produce multiple subscribers for provided topics. +pub fn pubsub_channel( + size: usize, +) -> (TopicPublisher, TopicSubscriptionFactory) { + let (publisher, subscriber): (TopicPublisher, TopicSubscriber) = bounded(size); + + (publisher, TopicSubscriptionFactory::new(subscriber)) +} + +#[cfg(test)] +mod test { + use super::*; + use futures::{executor::block_on, future::select}; + + #[test] + fn topic_pub_sub() { + let (mut publisher, subscriber_factory) = pubsub_channel(10); + + #[derive(Debug, Clone)] + struct Dummy { + a: u32, + b: String, + } + + let messages = vec![ + TopicPayload::new("Topic1", Dummy { + a: 1u32, + b: "one".to_string(), + }), + TopicPayload::new("Topic2", Dummy { + a: 2u32, + b: "two".to_string(), + }), + TopicPayload::new("Topic1", Dummy { + a: 3u32, + b: "three".to_string(), + }), + TopicPayload::new("Topic2", Dummy { + a: 4u32, + b: "four".to_string(), + }), + TopicPayload::new("Topic1", Dummy { + a: 5u32, + b: "five".to_string(), + }), + TopicPayload::new("Topic2", Dummy { + a: 6u32, + b: "size".to_string(), + }), + TopicPayload::new("Topic1", Dummy { + a: 7u32, + b: "seven".to_string(), + }), + ]; + + block_on(async { + for m in messages { + publisher.send(m).await.unwrap(); + } + }); + + let mut sub1 = subscriber_factory.get_subscription("Topic1").fuse(); + + let topic1a = block_on(async { + let mut result = Vec::new(); + + loop { + select!( + item = sub1.next() => {if let Some(i) = item {result.push(i)}}, + default => break, + ); + } + result + }); + + assert_eq!(topic1a.len(), 4); + assert_eq!(topic1a[0].a, 1); + assert_eq!(topic1a[1].a, 3); + assert_eq!(topic1a[2].a, 5); + assert_eq!(topic1a[3].a, 7); + + let messages2 = vec![ + TopicPayload::new("Topic1", Dummy { + a: 11u32, + b: "one one".to_string(), + }), + TopicPayload::new("Topic2", Dummy { + a: 22u32, + b: "two two".to_string(), + }), + TopicPayload::new("Topic1", Dummy { + a: 33u32, + b: "three three".to_string(), + }), + ]; + + block_on(async move { + stream::iter(messages2).map(|i| Ok(i)).forward(publisher).await.unwrap(); + }); + + let topic1b = block_on(async { sub1.collect::>().await }); + + assert_eq!(topic1b.len(), 2); + assert_eq!(topic1b[0].a, 11); + assert_eq!(topic1b[1].a, 33); + + let sub2 = subscriber_factory.get_subscription("Topic2"); + + let topic2 = block_on(async { sub2.collect::>().await }); + + assert_eq!(topic2.len(), 4); + assert_eq!(topic2[0].a, 2); + assert_eq!(topic2[1].a, 4); + assert_eq!(topic2[2].a, 6); + assert_eq!(topic2[3].a, 22); + } +} diff --git a/comms/src/types.rs b/comms/src/types.rs new file mode 100644 index 0000000000..ceaea66169 --- /dev/null +++ b/comms/src/types.rs @@ -0,0 +1,67 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + dispatcher::{DispatchError, Dispatcher}, + inbound_message_service::comms_msg_handlers::{CommsDispatchType, InboundMessageServiceResolver}, + peer_manager::{peer_key::PeerKey, Peer}, +}; +use tari_crypto::{common::Blake256, keys::PublicKey, ristretto::RistrettoPublicKey}; +use tari_storage::lmdb_store::LMDBStore; +#[cfg(test)] +use tari_storage::HMapDatabase; +#[cfg(not(test))] +use tari_storage::LMDBWrapper; +use tari_utilities::ciphers::chacha20::ChaCha20; + +/// The message protocol version for the MessageEnvelopeHeader +pub const MESSAGE_PROTOCOL_VERSION: u8 = 0; + +/// The wire protocol version for the MessageEnvelope wire format +pub const WIRE_PROTOCOL_VERSION: u8 = 0; + +/// The default port that control services listen on +pub const DEFAULT_LISTENER_ADDRESS: &str = "0.0.0.0:7899"; + +/// Specify the digest type for the signature challenges +pub type Challenge = Blake256; + +/// Public key type +pub type CommsPublicKey = RistrettoPublicKey; +pub type CommsSecretKey = ::K; + +/// Specify the RNG that should be used for random selection +pub type CommsRng = rand::OsRng; + +/// Specify what cipher to use for encryption/decryption +pub type CommsCipher = ChaCha20; + +/// Datastore and Database used for persistence storage +pub type CommsDataStore = LMDBStore; + +#[cfg(not(test))] +pub type CommsDatabase = LMDBWrapper; +#[cfg(test)] +pub type CommsDatabase = HMapDatabase; + +/// Dispatcher format for comms level dispatching to handlers +pub type MessageDispatcher = Dispatcher; diff --git a/comms/src/utils.rs b/comms/src/utils.rs new file mode 100644 index 0000000000..98f3596cae --- /dev/null +++ b/comms/src/utils.rs @@ -0,0 +1,58 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod crypto { + use crate::{ + message::MessageError, + types::{Challenge, CommsPublicKey}, + }; + use digest::Digest; + use rand::{CryptoRng, Rng}; + use tari_crypto::{ + keys::{PublicKey, SecretKey}, + signatures::{SchnorrSignature, SchnorrSignatureError}, + }; + use tari_utilities::message_format::MessageFormat; + + pub fn sign( + rng: &mut R, + secret_key: ::K, + body: B, + ) -> Result::K>, SchnorrSignatureError> + where + R: CryptoRng + Rng, + B: AsRef<[u8]>, + { + let challenge = Challenge::new().chain(body).result().to_vec(); + let nonce = ::K::random(rng); + SchnorrSignature::sign(secret_key, nonce, &challenge) + } + + /// Verify that the signature is valid for the message body + pub fn verify(public_key: &CommsPublicKey, signature: &[u8], body: B) -> Result + where B: AsRef<[u8]> { + let signature = SchnorrSignature::::K>::from_binary(signature) + .map_err(MessageError::MessageFormatError)?; + let challenge = Challenge::new().chain(body).result().to_vec(); + Ok(signature.verify_challenge(public_key, &challenge)) + } +} diff --git a/comms/tests/connection/connection.rs b/comms/tests/connection/connection.rs new file mode 100644 index 0000000000..a38fd2c2d9 --- /dev/null +++ b/comms/tests/connection/connection.rs @@ -0,0 +1,194 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support; +use std::time::Duration; +use tari_comms::connection::{ + connection::Connection, + error::ConnectionError, + types::{Direction, Linger}, + CurveEncryption, + InprocAddress, + NetAddress, + ZmqContext, +}; + +use crate::support::factories::{self, TestFactory}; + +#[test] +fn inbound_receive_timeout() { + let ctx = ZmqContext::new(); + + let addr = InprocAddress::random(); + + let conn = Connection::new(&ctx, Direction::Inbound) + .set_linger(Linger::Indefinitely) + .establish(&addr) + .unwrap(); + + let result = conn.receive(1); + assert!(result.is_err()); + let err = result.unwrap_err(); + match err { + ConnectionError::Timeout => {}, + _ => panic!("Unexpected error type: {:?}", err), + } +} + +#[test] +fn inbound_recv_send_inproc() { + let ctx = ZmqContext::new(); + + let addr = InprocAddress::random(); + + let req_rep_pattern = support::comms_patterns::async_request_reply(Direction::Outbound); + + let conn = Connection::new(&ctx, Direction::Inbound) + .set_linger(Linger::Indefinitely) + .establish(&addr) + .unwrap(); + + let signal = req_rep_pattern + .set_endpoint(addr.clone()) + .set_identity("boba") + .set_send_data(vec![ + "Just".as_bytes().to_vec(), + "Three".as_bytes().to_vec(), + "Messages".as_bytes().to_vec(), + ]) + .run(ctx.clone()); + + let frames = conn.receive(2000).unwrap(); + assert_eq!(frames.len(), 4); + assert_eq!("boba".as_bytes(), frames[0].as_slice()); + assert_eq!("Just".as_bytes(), frames[1].as_slice()); + assert_eq!("Three".as_bytes(), frames[2].as_slice()); + assert_eq!("Messages".as_bytes(), frames[3].as_slice()); + + conn.send(&["boba", "OK"]).unwrap(); + + // Wait for pattern to exit + signal.recv_timeout(Duration::from_millis(10)).unwrap(); +} + +#[test] +fn inbound_recv_send_encrypted_tcp() { + let ctx = ZmqContext::new(); + + let addr = factories::net_address::create().use_os_port().build().unwrap(); + + let req_rep_pattern = support::comms_patterns::async_request_reply(Direction::Outbound); + + let (sk, pk) = CurveEncryption::generate_keypair().unwrap(); + + let conn = Connection::new(&ctx, Direction::Inbound) + .set_linger(Linger::Indefinitely) + .set_curve_encryption(CurveEncryption::Server { secret_key: sk }) + .establish(&addr) + .unwrap(); + + let addr = NetAddress::from(conn.get_connected_address().clone().unwrap()); + + let signal = req_rep_pattern + .set_endpoint(addr.clone()) + .set_identity("the dude") + .set_server_public_key(pk) + .set_send_data(vec![(0..255).map(|i| i as u8).collect::>()]) + .run(ctx.clone()); + + let frames = conn.receive(2000).unwrap(); + assert_eq!(frames.len(), 2); + + conn.send(&["the dude", "OK"]).unwrap(); + + // Wait for pattern to exit + signal.recv_timeout(Duration::from_millis(10)).unwrap(); +} + +#[test] +fn outbound_send_recv_inproc() { + let ctx = ZmqContext::new(); + + let addr = InprocAddress::random(); + + let req_rep_pattern = support::comms_patterns::async_request_reply(Direction::Inbound); + + let signal = req_rep_pattern + .set_endpoint(addr.clone()) + .set_send_data(vec!["OK".as_bytes().to_vec()]) + .run(ctx.clone()); + + let conn = Connection::new(&ctx, Direction::Outbound) + .set_linger(Linger::Indefinitely) + .set_identity("identity") + .establish(&addr) + .unwrap(); + + conn.send(&["identity"]).unwrap(); + + let frames = conn.receive(2000).unwrap(); + + assert_eq!(1, frames.len()); + assert_eq!("OK", String::from_utf8_lossy(frames[0].as_slice())); + + // Wait for pattern to exit + signal.recv_timeout(Duration::from_millis(10)).unwrap(); +} + +#[test] +fn outbound_send_recv_encrypted_tcp() { + let ctx = ZmqContext::new(); + + let addr = factories::net_address::create().build().unwrap(); + + let req_rep_pattern = support::comms_patterns::async_request_reply(Direction::Inbound); + + let (sk, spk) = CurveEncryption::generate_keypair().unwrap(); + let (csk, cpk) = CurveEncryption::generate_keypair().unwrap(); + + let signal = req_rep_pattern + .set_endpoint(addr.clone()) + .set_secret_key(sk) + .set_send_data(vec!["OK".as_bytes().to_vec()]) + .run(ctx.clone()); + + let conn = Connection::new(&ctx, Direction::Outbound) + .set_linger(Linger::Indefinitely) + .set_curve_encryption(CurveEncryption::Client { + secret_key: csk, + public_key: cpk, + server_public_key: spk, + }) + .set_identity("identity") + .establish(&addr) + .unwrap(); + + conn.send(&["identity"]).unwrap(); + + let frames = conn.receive(2000).unwrap(); + + assert_eq!(1, frames.len()); + assert_eq!("OK", String::from_utf8_lossy(frames[0].as_slice())); + + // Wait for pattern to exit + signal.recv_timeout(Duration::from_millis(10)).unwrap(); +} diff --git a/comms/tests/connection/mod.rs b/comms/tests/connection/mod.rs new file mode 100644 index 0000000000..d8c7109e70 --- /dev/null +++ b/comms/tests/connection/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod connection; +mod monitor; +mod peer_connection; diff --git a/comms/tests/connection/monitor.rs b/comms/tests/connection/monitor.rs new file mode 100644 index 0000000000..727d6f02e0 --- /dev/null +++ b/comms/tests/connection/monitor.rs @@ -0,0 +1,80 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{thread, time::Duration}; +use tari_comms::connection::{ + connection::Connection, + monitor::{ConnectionMonitor, SocketEventType}, + types::Direction, + zmq::{ZmqContext, ZmqEndpoint}, + InprocAddress, + NetAddress, +}; + +use crate::support::factories::{self, TestFactory}; + +#[test] +fn recv_socket_events() { + let ctx = ZmqContext::new(); + let monitor_addr = InprocAddress::random(); + let address = factories::net_address::create().use_os_port().build().unwrap(); + + let monitor = ConnectionMonitor::connect(&ctx, &monitor_addr).unwrap(); + + let conn_in = Connection::new(&ctx, Direction::Inbound) + .set_monitor_addr(monitor_addr.clone()) + .establish(&address) + .unwrap(); + let connected_address = NetAddress::from(conn_in.get_connected_address().clone().unwrap()); + + { + // Connect and disconnect + let conn_out = Connection::new(&ctx, Direction::Outbound) + .establish(&connected_address) + .unwrap(); + conn_out.send(&["test".as_bytes()]).unwrap(); + + let _ = conn_in.receive(1000).unwrap(); + } + + thread::sleep(Duration::from_millis(10)); + // Collect events + let mut events = vec![]; + while let Ok(event) = monitor.read(10) { + events.push(event); + } + + let event = events.iter().find(|e| e.event_type == SocketEventType::Listening); + assert!(event.is_some(), "Expected to find event Listening"); + let event = event.unwrap(); + assert_eq!(event.address, connected_address.to_zmq_endpoint()); + + let event = events.iter().find(|e| e.event_type == SocketEventType::Accepted); + assert!(event.is_some(), "Expected to find event Accepted"); + let event = event.unwrap(); + assert_eq!(event.address, connected_address.to_zmq_endpoint()); + + let event = events.iter().find(|e| e.event_type == SocketEventType::Disconnected); + assert!(event.is_some(), "Expected to find event Disconnected"); + let event = event.unwrap(); + assert_eq!(event.address, connected_address.to_zmq_endpoint()); +} diff --git a/comms/tests/connection/peer_connection.rs b/comms/tests/connection/peer_connection.rs new file mode 100644 index 0000000000..ad195f3039 --- /dev/null +++ b/comms/tests/connection/peer_connection.rs @@ -0,0 +1,463 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support::{ + factories::{self, TestFactory}, + helpers::asserts::assert_change, +}; +use std::time::Duration; +use tari_comms::connection::{ + peer_connection::PeerConnectionProtoMessage, + types::{Direction, Linger}, + Connection, + ConnectionError, + CurveEncryption, + InprocAddress, + NetAddress, + PeerConnection, + PeerConnectionContextBuilder, + PeerConnectionError, + ZmqContext, +}; + +#[test] +fn connection_in() { + let addr = factories::net_address::create().build().unwrap(); + let ctx = ZmqContext::new(); + + let (server_sk, server_pk) = CurveEncryption::generate_keypair().unwrap(); + let (client_sk, client_pk) = CurveEncryption::generate_keypair().unwrap(); + + let consumer_addr = InprocAddress::random(); + + // Initialize and start peer connection + let context = PeerConnectionContextBuilder::new() + .set_id("123") + .set_direction(Direction::Inbound) + .set_context(&ctx) + .set_message_sink_address(consumer_addr.clone()) + .set_curve_encryption(CurveEncryption::Server { secret_key: server_sk }) + .set_address(addr.clone()) + .build() + .unwrap(); + + let mut conn = PeerConnection::new(); + conn.start(context).unwrap(); + conn.wait_listening_or_failure(&Duration::from_millis(1000)).unwrap(); + + // Connect the message consumer + let consumer = Connection::new(&ctx, Direction::Inbound) + .establish(&consumer_addr) + .unwrap(); + + // Connect to the inbound connection and send a message + let sender = Connection::new(&ctx, Direction::Outbound) + .set_curve_encryption(CurveEncryption::Client { + server_public_key: server_pk, + secret_key: client_sk, + public_key: client_pk, + }) + .establish(&addr) + .unwrap(); + sender.send(&[&[PeerConnectionProtoMessage::Identify as u8]]).unwrap(); + sender + .send(&[&[PeerConnectionProtoMessage::Message as u8], &[1u8]]) + .unwrap(); + + // Receive the message from the consumer socket + let frames = consumer.receive(2000).unwrap(); + assert_eq!("123".as_bytes().to_vec(), frames[1]); + assert_eq!(vec![1u8], frames[2]); + + conn.send(vec![vec![111u8]]).unwrap(); + + let reply = sender.receive(100).unwrap(); + assert_eq!( + vec![vec![PeerConnectionProtoMessage::Message as u8], vec![111u8]], + reply + ); +} + +#[test] +fn connection_out() { + let addr = factories::net_address::create().build().unwrap(); + let ctx = ZmqContext::new(); + + let (server_sk, server_pk) = CurveEncryption::generate_keypair().unwrap(); + let (client_sk, client_pk) = CurveEncryption::generate_keypair().unwrap(); + + let consumer_addr = InprocAddress::random(); + + // Connect to the sender (peer) + let sender = Connection::new(&ctx, Direction::Inbound) + .set_name("Test sender") + .set_curve_encryption(CurveEncryption::Server { secret_key: server_sk }) + .establish(&addr) + .unwrap(); + + let conn_id = "123".as_bytes(); + + // Initialize and start peer connection + let context = PeerConnectionContextBuilder::new() + .set_id(conn_id.clone()) + .set_direction(Direction::Outbound) + .set_context(&ctx) + .set_message_sink_address(consumer_addr.clone()) + .set_curve_encryption(CurveEncryption::Client { + server_public_key: server_pk, + secret_key: client_sk, + public_key: client_pk, + }) + .set_address(addr.clone()) + .build() + .unwrap(); + + let mut conn = PeerConnection::new(); + + assert!(!conn.is_connected()); + conn.start(context).unwrap(); + conn.wait_connected_or_failure(&Duration::from_millis(2000)).unwrap(); + + // Connect the message consumer + let consumer = Connection::new(&ctx, Direction::Inbound) + .set_name("Test message sink") + .establish(&consumer_addr) + .unwrap(); + + conn.send(vec![vec![123u8]]).unwrap(); + + let ident = sender.receive(2000).unwrap(); + assert_eq!(vec![PeerConnectionProtoMessage::Identify as u8], ident[1]); + let data = sender.receive(2000).unwrap(); + assert_eq!(vec![123u8], data[2]); + + sender + .send(&[data[0].as_slice(), &[PeerConnectionProtoMessage::Message as u8], &[ + 123u8, + ]]) + .unwrap(); + let frames = consumer.receive(2000).unwrap(); + assert_eq!(conn_id.to_vec(), frames[1]); + assert_eq!(vec![1u8], frames[2]); + assert_eq!(vec![123u8], frames[3]); +} + +#[test] +fn connection_wait_connect_shutdown() { + let addr = factories::net_address::create().build().unwrap(); + let ctx = ZmqContext::new(); + + let receiver = Connection::new(&ctx, Direction::Inbound).establish(&addr).unwrap(); + + let consumer_addr = InprocAddress::random(); + + let context = PeerConnectionContextBuilder::new() + .set_id("123") + .set_direction(Direction::Outbound) + .set_context(&ctx) + .set_message_sink_address(consumer_addr.clone()) + .set_address(addr) + .build() + .unwrap(); + + let mut conn = PeerConnection::new(); + + assert!(!conn.is_connected()); + conn.start(context).unwrap(); + + conn.wait_connected_or_failure(&Duration::from_millis(2000)).unwrap(); + + conn.shutdown().unwrap(); + + assert!( + conn.wait_disconnected(&Duration::from_millis(2000)).is_ok(), + "Failed to shut down in 100ms" + ); + + drop(receiver); +} + +#[test] +fn connection_wait_connect_failed() { + let addr = factories::net_address::create().use_os_port().build().unwrap(); + let ctx = ZmqContext::new(); + + let consumer_addr = InprocAddress::random(); + + // This has nothing to connect to + let context = PeerConnectionContextBuilder::new() + .set_id("123") + .set_direction(Direction::Outbound) + .set_max_retry_attempts(1) + .set_context(&ctx) + .set_message_sink_address(consumer_addr.clone()) + .set_address(addr.clone()) + .build() + .unwrap(); + + let mut conn = PeerConnection::new(); + + assert!(!conn.is_connected()); + conn.start(context).unwrap(); + + let err = conn + .wait_connected_or_failure(&Duration::from_millis(2000)) + .unwrap_err(); + + assert!(conn.is_failed()); + match err { + ConnectionError::PeerError(err) => match err { + PeerConnectionError::ExceededMaxConnectRetryCount => {}, + _ => panic!("Unexpected connection error '{}'", err), + }, + _ => panic!("Unexpected connection error '{}'", err), + } +} + +#[test] +fn connection_pause_resume() { + let addr = factories::net_address::create().build().unwrap(); + let ctx = ZmqContext::new(); + + let consumer_addr = InprocAddress::random(); + + // Connect to the sender (peer) + let sender = Connection::new(&ctx, Direction::Outbound) + .set_linger(Linger::Indefinitely) + .establish(&addr) + .unwrap(); + let conn_id = "123".as_bytes(); + + // Initialize and start peer connection + let context = PeerConnectionContextBuilder::new() + .set_id(conn_id.clone()) + .set_direction(Direction::Inbound) + .set_context(&ctx) + .set_message_sink_address(consumer_addr.clone()) + .set_address(addr) + .build() + .unwrap(); + + let mut conn = PeerConnection::new(); + + assert!(!conn.is_connected()); + conn.start(context).unwrap(); + + conn.wait_listening_or_failure(&Duration::from_millis(2000)).unwrap(); + + // Connect the message consumer + let consumer = Connection::new(&ctx, Direction::Inbound) + .establish(&consumer_addr) + .unwrap(); + + let msg_type_frame = &[PeerConnectionProtoMessage::Message as u8]; + sender.send(&[&[PeerConnectionProtoMessage::Identify as u8]]).unwrap(); + sender.send(&[msg_type_frame, &[1u8]]).unwrap(); + + let frames = consumer.receive(2000).unwrap(); + assert_eq!(conn_id.to_vec(), frames[1]); + assert_eq!(vec![1u8], frames[2]); + + // Pause the connection + conn.pause().unwrap(); + + sender.send(&[msg_type_frame, &[2u8]]).unwrap(); + sender.send(&[msg_type_frame, &[3u8]]).unwrap(); + sender.send(&[msg_type_frame, &[4u8]]).unwrap(); + + let err = consumer.receive(3000).unwrap_err(); + assert!(err.is_timeout()); + + // Resume connection + conn.resume().unwrap(); + + // Should receive all the pending messages + let frames = consumer.receive(3000).unwrap(); + assert_eq!(vec![2u8], frames[3]); + let frames = consumer.receive(3000).unwrap(); + assert_eq!(vec![3u8], frames[3]); + let frames = consumer.receive(3000).unwrap(); + assert_eq!(vec![4u8], frames[3]); +} + +#[test] +fn connection_disconnect() { + let addr = factories::net_address::create().use_os_port().build().unwrap(); + let ctx = ZmqContext::new(); + + let consumer_addr = InprocAddress::random(); + + // Initialize and start peer connection + let context = PeerConnectionContextBuilder::new() + .set_id("123") + .set_direction(Direction::Inbound) + .set_context(&ctx) + .set_message_sink_address(consumer_addr.clone()) + .set_address(addr) + .build() + .unwrap(); + + let mut conn = PeerConnection::new(); + conn.start(context).unwrap(); + conn.wait_listening_or_failure(&Duration::from_millis(1000)).unwrap(); + let addr = NetAddress::from(conn.get_connected_address().unwrap()); + + { + // Connect to the inbound connection and send a message + let sender = Connection::new(&ctx, Direction::Outbound) + .set_linger(Linger::Indefinitely) + .establish(&addr) + .unwrap(); + sender.send(&[&[PeerConnectionProtoMessage::Identify as u8]]).unwrap(); + sender + .send(&[&[PeerConnectionProtoMessage::Message as u8], &[123u8]]) + .unwrap(); + } + + conn.wait_disconnected(&Duration::from_millis(2000)).unwrap(); +} + +#[test] +fn connection_stats() { + let addr = factories::net_address::create().build().unwrap(); + let ctx = ZmqContext::new(); + + let consumer_addr = InprocAddress::random(); + + // Connect to the sender (peer) + let sender = Connection::new(&ctx, Direction::Outbound) + .set_linger(Linger::Indefinitely) + .establish(&addr) + .unwrap(); + + // Initialize and start peer connection + let context = PeerConnectionContextBuilder::new() + .set_id("123".as_bytes()) + .set_direction(Direction::Inbound) + .set_context(&ctx) + .set_message_sink_address(consumer_addr.clone()) + .set_address(addr) + .build() + .unwrap(); + + let mut conn = PeerConnection::new(); + + assert!(!conn.is_connected()); + conn.start(context).unwrap(); + + let initial_stats = conn.connection_stats(); + let msg_type_frame = &[PeerConnectionProtoMessage::Message as u8]; + + sender.send(&[&[PeerConnectionProtoMessage::Identify as u8]]).unwrap(); + sender.send(&[msg_type_frame, &[1u8]]).unwrap(); + sender.send(&[msg_type_frame, &[2u8]]).unwrap(); + sender.send(&[msg_type_frame, &[3u8]]).unwrap(); + sender.send(&[msg_type_frame, &[4u8]]).unwrap(); + + conn.wait_connected_or_failure(&Duration::from_millis(2000)).unwrap(); + + conn.send(vec![vec![10u8]]).unwrap(); + conn.send(vec![vec![11u8]]).unwrap(); + conn.send(vec![vec![12u8]]).unwrap(); + + // Assert that receive stats update + assert_change( + || { + let stats = conn.connection_stats(); + stats.messages_recv() + }, + 4, + 40, + ); + + assert_change( + || { + let stats = conn.connection_stats(); + stats.messages_sent() + }, + 3, + 20, + ); + + let stats = conn.connection_stats(); + assert!(stats.last_activity() > initial_stats.last_activity()); +} + +#[test] +fn ignore_invalid_message_types() { + let addr = factories::net_address::create().build().unwrap(); + let ctx = ZmqContext::new(); + + let (server_sk, server_pk) = CurveEncryption::generate_keypair().unwrap(); + let (client_sk, client_pk) = CurveEncryption::generate_keypair().unwrap(); + + let consumer_addr = InprocAddress::random(); + + // Initialize and start peer connection + let context = PeerConnectionContextBuilder::new() + .set_id("123") + .set_direction(Direction::Inbound) + .set_context(&ctx) + .set_message_sink_address(consumer_addr.clone()) + .set_curve_encryption(CurveEncryption::Server { secret_key: server_sk }) + .set_address(addr.clone()) + .build() + .unwrap(); + + let mut conn = PeerConnection::new(); + conn.start(context).unwrap(); + conn.wait_listening_or_failure(&Duration::from_millis(1000)).unwrap(); + + // Connect the message consumer + let consumer = Connection::new(&ctx, Direction::Inbound) + .establish(&consumer_addr) + .unwrap(); + + // Connect to the inbound connection and send a message + let sender = Connection::new(&ctx, Direction::Outbound) + .set_curve_encryption(CurveEncryption::Client { + server_public_key: server_pk, + secret_key: client_sk, + public_key: client_pk, + }) + .establish(&addr) + .unwrap(); + + assert!(!conn.is_connected()); + sender.send(&[&[PeerConnectionProtoMessage::Identify as u8]]).unwrap(); + assert_change(|| conn.is_connected(), true, 10); + // Send invalid peer connection message type + sender.send(&[&[255], &[1u8]]).unwrap(); + sender + .send(&[&[PeerConnectionProtoMessage::Message as u8], &[1u8]]) + .unwrap(); + + // Receive the message from the consumer socket + let frames = consumer.receive(2000).unwrap(); + assert_eq!("123".as_bytes().to_vec(), frames[1]); + assert_eq!(vec![1u8], frames[2]); + assert_eq!(vec![1u8], frames[3]); + + // Test no more messages to receive. Since we have received above, the invalid message + // should be already ready to receive (10ms) if it was forwarded by the peer connection. + assert!(consumer.receive(10).is_err()); +} diff --git a/comms/tests/connection_manager/establisher.rs b/comms/tests/connection_manager/establisher.rs new file mode 100644 index 0000000000..2ff027a7b8 --- /dev/null +++ b/comms/tests/connection_manager/establisher.rs @@ -0,0 +1,329 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support::{ + factories::{self, TestFactory}, + helpers::ConnectionMessageCounter, +}; +use std::{path::PathBuf, sync::Arc, time::Duration}; +use tari_comms::{ + connection::{CurveEncryption, Direction, InprocAddress, NetAddress, ZmqContext}, + connection_manager::{establisher::ConnectionEstablisher, ConnectionManagerError, PeerConnectionConfig}, + control_service::messages::{ControlServiceResponseType, Pong}, + message::{Message, MessageEnvelope, MessageFlags, MessageHeader, NodeDestination}, +}; +use tari_storage::{ + lmdb_store::{LMDBBuilder, LMDBError, LMDBStore}, + LMDBWrapper, +}; +use tari_utilities::{message_format::MessageFormat, thread_join::ThreadJoinWithTimeout}; + +fn make_peer_connection_config(message_sink_address: InprocAddress) -> PeerConnectionConfig { + PeerConnectionConfig { + peer_connection_establish_timeout: Duration::from_secs(5), + max_message_size: 1024, + max_connections: 10, + host: "127.0.0.1".parse().unwrap(), + max_connect_retries: 3, + message_sink_address, + socks_proxy_address: None, + } +} + +fn get_path(name: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name); + path.to_str().unwrap().to_string() +} + +fn init_datastore(name: &str) -> Result { + let path = get_path(name); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + LMDBBuilder::new() + .set_path(&path) + .set_environment_size(10) + .set_max_number_of_databases(2) + .add_database(name, lmdb_zero::db::CREATE) + .build() +} + +fn clean_up_datastore(name: &str) { + std::fs::remove_dir_all(get_path(name)).unwrap(); +} + +// This tries to break the establisher by sending malformed messages. The establisher should +// disregard the malformed message and continue to try other addresses. Once all +// addresses fail, the correct error should be returned. +#[test] +fn establish_control_service_connection_fail() { + let context = ZmqContext::new(); + + let node_identity = factories::node_identity::create().build().map(Arc::new).unwrap(); + + let peers = factories::peer::create_many(2) + .with_factory(factories::peer::create().with_net_addresses_factory(factories::net_address::create_many(2))) + .build() + .unwrap(); + + // Setup a connection counter to act as a 'junk' endpoint for a peers control service. + let mut msg_counter1 = ConnectionMessageCounter::new(&context); + msg_counter1.set_response(vec!["JUNK".as_bytes().to_vec()]); + msg_counter1.start(peers[0].addresses[0].net_address.clone()); + + let mut msg_counter2 = ConnectionMessageCounter::new(&context); + msg_counter2.set_response(vec!["JUNK".as_bytes().to_vec()]); + msg_counter2.start(peers[0].addresses[1].net_address.clone()); + + // Note: every test should have unique database + let database_name = "establisher_establish_control_service_connection_fail"; + let datastore = init_datastore(database_name).unwrap(); + let database = datastore.get_handle(database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let peer_manager = Arc::new( + factories::peer_manager::create() + .with_database(database) + .with_peers(peers.clone()) + .build() + .unwrap(), + ); + let config = make_peer_connection_config(InprocAddress::random()); + + let example_peer = &peers[0]; + + let establisher = ConnectionEstablisher::new(context.clone(), node_identity, config, peer_manager); + match establisher.connect_control_service_client(example_peer) { + Ok(_) => panic!("Unexpected success result"), + Err(ConnectionManagerError::MaxConnnectionAttemptsExceeded) => {}, + Err(err) => panic!("Unexpected error type: {:?}", err), + } + + msg_counter1.assert_count(1, 20); + msg_counter2.assert_count(1, 20); + + clean_up_datastore(database_name); +} + +#[test] +fn establish_control_service_connection_succeed() { + let context = ZmqContext::new(); + let address = factories::net_address::create().build().unwrap(); + // The node attempting to connect + let node_identity1 = factories::node_identity::create().build().map(Arc::new).unwrap(); + // The node being connected to + let node_identity2 = factories::node_identity::create().build().map(Arc::new).unwrap(); + + let example_peer = factories::peer::create() + .with_public_key(node_identity2.identity.public_key.clone()) + .with_net_addresses(vec![address]) + .build() + .unwrap(); + + // Setup a connection counter to act as a control service sending back a pong + let pong_response = { + let envelope = MessageEnvelope::construct( + &node_identity2, + node_identity1.identity.public_key.clone(), + NodeDestination::PublicKey(node_identity1.identity.public_key.clone()), + Message::from_message_format( + MessageHeader::new(ControlServiceResponseType::Pong).unwrap(), + Pong {}.to_binary().unwrap(), + ) + .unwrap() + .to_binary() + .unwrap(), + MessageFlags::ENCRYPTED, + ) + .unwrap(); + envelope.into_frame_set() + }; + + let address: NetAddress = example_peer.addresses[0].net_address.clone(); + + let mut msg_counter1 = ConnectionMessageCounter::new(&context); + msg_counter1.set_response(pong_response); + msg_counter1.start(address); + + // Setup peer manager + let database_name = "establisher_establish_control_service_connection_succeed"; // Note: every test should have unique database + let datastore = init_datastore(database_name).unwrap(); + let database = datastore.get_handle(database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let peer_manager = Arc::new( + factories::peer_manager::create() + .with_database(database) + .with_peers(vec![example_peer.clone()]) + .build() + .unwrap(), + ); + + let config = make_peer_connection_config(InprocAddress::random()); + let establisher = ConnectionEstablisher::new(context.clone(), node_identity1, config, peer_manager); + let client = establisher.connect_control_service_client(&example_peer).unwrap(); + client.ping_pong(Duration::from_millis(3000)).unwrap(); + + msg_counter1.assert_count(2, 20); + + clean_up_datastore(database_name); +} + +#[test] +fn establish_peer_connection_outbound() { + let context = ZmqContext::new(); + let msg_sink_address = InprocAddress::random(); + let node_identity = factories::node_identity::create().build().map(Arc::new).unwrap(); + + // Setup a message counter to count the number of messages sent to the consumer address + let msg_counter = ConnectionMessageCounter::new(&context); + msg_counter.start(msg_sink_address.clone()); + + // Setup a peer connection + let (peer_curve_sk, peer_curve_pk) = CurveEncryption::generate_keypair().unwrap(); + let (other_peer_conn, other_peer_conn_handle) = factories::peer_connection::create() + .with_peer_connection_context_factory( + factories::peer_connection_context::create() + .with_message_sink_address(msg_sink_address.clone()) + .with_curve_keypair((peer_curve_sk, peer_curve_pk.clone())) + .with_context(&context) + .with_direction(Direction::Inbound), + ) + .build() + .unwrap(); + + other_peer_conn + .wait_listening_or_failure(&Duration::from_millis(2000)) + .unwrap(); + + let address = other_peer_conn.get_connected_address().unwrap().to_string(); + assert_ne!(address, "127.0.0.1:0"); + let address: NetAddress = other_peer_conn.get_connected_address().unwrap().into(); + + let example_peer = factories::peer::create() + .with_net_addresses(vec![address.clone()]) + .build() + .unwrap(); + + let database_name = "establisher_establish_peer_connection_outbound"; // Note: every test should have unique database + let datastore = init_datastore(database_name).unwrap(); + let database = datastore.get_handle(database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let peer_manager = Arc::new( + factories::peer_manager::create() + .with_database(database) + .with_peers(vec![example_peer.clone()]) + .build() + .unwrap(), + ); + + let config = make_peer_connection_config(InprocAddress::random()); + let establisher = ConnectionEstablisher::new(context.clone(), node_identity, config, peer_manager); + let (connection, peer_conn_handle) = establisher + .establish_outbound_peer_connection(example_peer.node_id.clone().into(), address, peer_curve_pk) + .unwrap(); + + connection.send(vec!["HELLO".as_bytes().to_vec()]).unwrap(); + connection.send(vec!["TARI".as_bytes().to_vec()]).unwrap(); + + connection.shutdown().unwrap(); + connection.wait_disconnected(&Duration::from_millis(3000)).unwrap(); + + other_peer_conn.shutdown().unwrap(); + other_peer_conn.wait_disconnected(&Duration::from_millis(3000)).unwrap(); + other_peer_conn_handle + .timeout_join(Duration::from_millis(3000)) + .unwrap(); + + assert_eq!(msg_counter.count(), 2); + + peer_conn_handle.timeout_join(Duration::from_millis(3000)).unwrap(); + + clean_up_datastore(database_name); +} + +#[test] +fn establish_peer_connection_inbound() { + let context = ZmqContext::new(); + let msg_sink_address = InprocAddress::random(); + let node_identity = factories::node_identity::create().build().map(Arc::new).unwrap(); + + let (secret_key, public_key) = CurveEncryption::generate_keypair().unwrap(); + + let example_peer = factories::peer::create().build().unwrap(); + + let database_name = "establish_peer_connection_inbound"; // Note: every test should have unique database + let datastore = init_datastore(database_name).unwrap(); + let database = datastore.get_handle(database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let peer_manager = Arc::new( + factories::peer_manager::create() + .with_database(database) + .with_peers(vec![example_peer.clone()]) + .build() + .unwrap(), + ); + + // Setup a message counter to count the number of messages sent to the consumer address + let msg_counter = ConnectionMessageCounter::new(&context); + msg_counter.start(msg_sink_address.clone()); + + // Create a connection establisher + let config = make_peer_connection_config(msg_sink_address.clone()); + let establisher = ConnectionEstablisher::new(context.clone(), node_identity, config, peer_manager); + let (connection, peer_conn_handle) = establisher + .establish_inbound_peer_connection(example_peer.node_id.clone().into(), secret_key) + .unwrap(); + + connection + .wait_listening_or_failure(&Duration::from_millis(3000)) + .unwrap(); + let address: NetAddress = connection.get_connected_address().unwrap().into(); + + // Setup a peer connection which will connect to our established inbound peer connection + let (other_peer_conn, other_peer_conn_handle) = factories::peer_connection::create() + .with_peer_connection_context_factory( + factories::peer_connection_context::create() + .with_context(&context) + .with_address(address) + .with_server_public_key(public_key.clone()) + .with_direction(Direction::Outbound), + ) + .build() + .unwrap(); + + other_peer_conn + .wait_connected_or_failure(&Duration::from_millis(3000)) + .unwrap(); + // Start sending messages + other_peer_conn.send(vec!["HELLO".as_bytes().to_vec()]).unwrap(); + other_peer_conn.send(vec!["TARI".as_bytes().to_vec()]).unwrap(); + let _ = other_peer_conn.shutdown(); + other_peer_conn.wait_disconnected(&Duration::from_millis(3000)).unwrap(); + + assert_eq!(msg_counter.count(), 2); + + peer_conn_handle.timeout_join(Duration::from_millis(3000)).unwrap(); + other_peer_conn_handle + .timeout_join(Duration::from_millis(3000)) + .unwrap(); + + clean_up_datastore(database_name); +} diff --git a/comms/tests/connection_manager/manager.rs b/comms/tests/connection_manager/manager.rs new file mode 100644 index 0000000000..5bba10b3a1 --- /dev/null +++ b/comms/tests/connection_manager/manager.rs @@ -0,0 +1,212 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support::{ + factories::{self, TestFactory}, + helpers::ConnectionMessageCounter, +}; +use std::{path::PathBuf, sync::Arc, thread, time::Duration}; +use tari_comms::{ + connection::{types::Linger, InprocAddress, ZmqContext}, + connection_manager::PeerConnectionConfig, + control_service::{ControlService, ControlServiceConfig}, + peer_manager::{Peer, PeerManager}, + types::CommsDatabase, +}; +use tari_storage::{ + lmdb_store::{LMDBBuilder, LMDBError, LMDBStore}, + LMDBWrapper, +}; +use tari_utilities::thread_join::ThreadJoinWithTimeout; + +fn make_peer_connection_config(consumer_address: InprocAddress) -> PeerConnectionConfig { + PeerConnectionConfig { + peer_connection_establish_timeout: Duration::from_secs(5), + max_message_size: 1024, + max_connections: 10, + host: "127.0.0.1".parse().unwrap(), + max_connect_retries: 5, + message_sink_address: consumer_address, + socks_proxy_address: None, + } +} + +fn make_peer_manager(peers: Vec, database: CommsDatabase) -> Arc { + Arc::new( + factories::peer_manager::create() + .with_peers(peers) + .with_database(database) + .build() + .unwrap(), + ) +} + +fn get_path(name: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name); + path.to_str().unwrap().to_string() +} + +fn init_datastore(name: &str) -> Result { + let path = get_path(name); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + LMDBBuilder::new() + .set_path(&path) + .set_environment_size(10) + .set_max_number_of_databases(2) + .add_database(name, lmdb_zero::db::CREATE) + .build() +} + +fn clean_up_datastore(name: &str) { + std::fs::remove_dir_all(get_path(name)).unwrap(); +} + +fn pause() { + thread::sleep(Duration::from_millis(200)); +} + +#[test] +#[allow(non_snake_case)] +fn establish_peer_connection() { + let context = ZmqContext::new(); + + let node_A_identity = Arc::new(factories::node_identity::create().build().unwrap()); + + let node_B_consumer_address = InprocAddress::random(); + let node_B_msg_counter = ConnectionMessageCounter::new(&context); + node_B_msg_counter.start(node_B_consumer_address.clone()); + + //---------------------------------- Node B Setup --------------------------------------------// + + let node_B_control_port_address = factories::net_address::create().build().unwrap(); + let node_B_identity = Arc::new( + factories::node_identity::create() + .with_control_service_address(node_B_control_port_address.clone()) + .build() + .unwrap(), + ); + + let node_B_peer = factories::peer::create() + .with_net_addresses(vec![node_B_control_port_address.clone()]) + .with_public_key(node_B_identity.identity.public_key.clone()) + .build() + .unwrap(); + + // Node B knows no peers + let node_B_database_name = "connection_manager_node_B_peer_manager"; + let datastore = init_datastore(node_B_database_name).unwrap(); + let database = datastore.get_handle(node_B_database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let node_B_peer_manager = make_peer_manager(vec![], database); + let node_B_connection_manager = Arc::new( + factories::connection_manager::create() + .with_context(context.clone()) + .with_node_identity(node_B_identity.clone()) + .with_peer_manager(node_B_peer_manager) + .with_peer_connection_config(make_peer_connection_config(node_B_consumer_address.clone())) + .build() + .unwrap(), + ); + + // Start node B's control service + let node_B_control_service = ControlService::new(context.clone(), node_B_identity.clone(), ControlServiceConfig { + socks_proxy_address: None, + listener_address: node_B_control_port_address, + requested_connection_timeout: Duration::from_millis(5000), + }) + .serve(node_B_connection_manager) + .unwrap(); + + // Give the control service a moment to start up + pause(); + + //---------------------------------- Node A setup --------------------------------------------// + + let node_A_consumer_address = InprocAddress::random(); + + // Add node B to node A's peer manager + let node_A_database_name = "connection_manager_node_A_peer_manager"; // Note: every test should have unique database + let datastore = init_datastore(node_A_database_name).unwrap(); + let database = datastore.get_handle(node_A_database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let node_A_peer_manager = make_peer_manager(vec![node_B_peer.clone()], database); + let node_A_connection_manager = Arc::new( + factories::connection_manager::create() + .with_context(context.clone()) + .with_node_identity(node_A_identity.clone()) + .with_peer_manager(node_A_peer_manager) + .with_peer_connection_config(make_peer_connection_config(node_A_consumer_address)) + .build() + .unwrap(), + ); + + //------------------------------ Negotiate connection to node B -----------------------------------// + + let node_B_peer_copy = node_B_peer.clone(); + let node_A_connection_manager_cloned = node_A_connection_manager.clone(); + let handle1 = thread::spawn(move || -> Result<(), String> { + let to_node_B_conn = node_A_connection_manager_cloned + .establish_connection_to_peer(&node_B_peer) + .map_err(|err| format!("{:?}", err))?; + to_node_B_conn.set_linger(Linger::Indefinitely).unwrap(); + to_node_B_conn + .send(vec!["THREAD1".as_bytes().to_vec()]) + .map_err(|err| format!("{:?}", err))?; + Ok(()) + }); + + let node_A_connection_manager_cloned = node_A_connection_manager.clone(); + let handle2 = thread::spawn(move || -> Result<(), String> { + let to_node_B_conn = node_A_connection_manager_cloned + .establish_connection_to_peer(&node_B_peer_copy) + .map_err(|err| format!("{:?}", err))?; + to_node_B_conn.set_linger(Linger::Indefinitely).unwrap(); + to_node_B_conn + .send(vec!["THREAD2".as_bytes().to_vec()]) + .map_err(|err| format!("{:?}", err))?; + Ok(()) + }); + + handle1.timeout_join(Duration::from_millis(2000)).unwrap(); + handle2.timeout_join(Duration::from_millis(2000)).unwrap(); + + // Give the peer connections a moment to receive and the message sink connections to send + pause(); + + node_B_control_service.shutdown().unwrap(); + node_B_control_service + .timeout_join(Duration::from_millis(1000)) + .unwrap(); + + assert_eq!(node_A_connection_manager.get_active_connection_count(), 1); + node_B_msg_counter.assert_count(2, 20); + + match Arc::try_unwrap(node_A_connection_manager) { + Ok(manager) => manager.shutdown().into_iter().map(|r| r.unwrap()).collect::>(), + Err(_) => panic!("Unable to unwrap connection manager from Arc"), + }; + + clean_up_datastore(node_A_database_name); + clean_up_datastore(node_B_database_name); +} diff --git a/infrastructure/comms/src/connection/onion/mod.rs b/comms/tests/connection_manager/mod.rs similarity index 98% rename from infrastructure/comms/src/connection/onion/mod.rs rename to comms/tests/connection_manager/mod.rs index 8418dd7dbf..7d2f0ed95a 100644 --- a/infrastructure/comms/src/connection/onion/mod.rs +++ b/comms/tests/connection_manager/mod.rs @@ -20,4 +20,5 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub mod connection; +mod establisher; +mod manager; diff --git a/comms/tests/control_service/client.rs b/comms/tests/control_service/client.rs new file mode 100644 index 0000000000..b09a243c92 --- /dev/null +++ b/comms/tests/control_service/client.rs @@ -0,0 +1,59 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support::factories::{self, TestFactory}; +use std::{sync::Arc, time::Duration}; +use tari_comms::{ + connection::{Connection, Direction, InprocAddress, ZmqContext}, + control_service::{messages::Ping, ControlServiceClient}, +}; + +#[test] +fn send_ping_recv_pong() { + let context = ZmqContext::new(); + let address = InprocAddress::random(); + + let outbound_conn = Connection::new(&context, Direction::Outbound) + .establish(&address) + .unwrap(); + let inbound_conn = Connection::new(&context, Direction::Inbound) + .establish(&address) + .unwrap(); + + let node_identity_1 = factories::node_identity::create().build().map(Arc::new).unwrap(); + let node_identity_2 = factories::node_identity::create().build().map(Arc::new).unwrap(); + + let out_client = ControlServiceClient::new( + node_identity_1.clone(), + node_identity_2.identity.public_key.clone(), + outbound_conn, + ); + out_client.send_ping().unwrap(); + + let in_client = ControlServiceClient::new( + node_identity_2.clone(), + node_identity_1.identity.public_key.clone(), + inbound_conn, + ); + + let _msg: Ping = in_client.receive_message(Duration::from_millis(2000)).unwrap().unwrap(); +} diff --git a/infrastructure/comms/src/connection/p2p/mod.rs b/comms/tests/control_service/mod.rs similarity index 98% rename from infrastructure/comms/src/connection/p2p/mod.rs rename to comms/tests/control_service/mod.rs index 8418dd7dbf..a54693b553 100644 --- a/infrastructure/comms/src/connection/p2p/mod.rs +++ b/comms/tests/control_service/mod.rs @@ -20,4 +20,5 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub mod connection; +mod client; +mod service; diff --git a/comms/tests/control_service/service.rs b/comms/tests/control_service/service.rs new file mode 100644 index 0000000000..6b106c8125 --- /dev/null +++ b/comms/tests/control_service/service.rs @@ -0,0 +1,215 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::support::{ + factories::{self, TestFactory}, + helpers::ConnectionMessageCounter, +}; +use std::{path::PathBuf, sync::Arc, time::Duration}; +use tari_comms::{ + connection::{types::Direction, Connection, InprocAddress, ZmqContext}, + connection_manager::{ConnectionManager, PeerConnectionConfig}, + control_service::{messages::ConnectRequestOutcome, ControlService, ControlServiceClient, ControlServiceConfig}, + peer_manager::{NodeId, NodeIdentity, Peer, PeerFlags, PeerManager}, +}; +use tari_storage::{ + lmdb_store::{LMDBBuilder, LMDBDatabase, LMDBError, LMDBStore}, + LMDBWrapper, +}; +use tari_utilities::thread_join::ThreadJoinWithTimeout; + +fn make_peer_manager(peers: Vec, database: LMDBDatabase) -> Arc { + Arc::new( + factories::peer_manager::create() + .with_peers(peers) + .with_database(LMDBWrapper::new(Arc::new(database))) + .build() + .unwrap(), + ) +} +fn get_path(name: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name); + path.to_str().unwrap().to_string() +} + +// Initialize the datastore. Note: every test should have unique database name +fn init_datastore(name: &str) -> Result { + let path = get_path(name); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + LMDBBuilder::new() + .set_path(&path) + .set_environment_size(10) + .set_max_number_of_databases(2) + .add_database(name, lmdb_zero::db::CREATE) + .build() +} + +fn clean_up_datastore(name: &str) { + std::fs::remove_dir_all(get_path(name)).unwrap(); +} + +fn setup( + database_name: &str, + peer_conn_config: PeerConnectionConfig, +) -> (ZmqContext, Arc, Arc, Arc) +{ + let node_identity = factories::node_identity::create().build().map(Arc::new).unwrap(); + let context = ZmqContext::new(); + let datastore = init_datastore(database_name).unwrap(); + let database = datastore.get_handle(database_name).unwrap(); + let peer_manager = make_peer_manager(vec![], database); + let connection_manager = factories::connection_manager::create() + .with_context(context.clone()) + .with_peer_connection_config(peer_conn_config) + .with_peer_manager(Arc::clone(&peer_manager)) + .build() + .map(Arc::new) + .unwrap(); + + (context, node_identity, peer_manager, connection_manager) +} + +#[test] +fn request_connection() { + let database_name = "control_service_request_connection"; + + let message_sink_address = InprocAddress::random(); + let peer_conn_config = PeerConnectionConfig { + message_sink_address, + ..Default::default() + }; + + let (context, node_identity_a, peer_manager, connection_manager) = setup(database_name, peer_conn_config.clone()); + + let msg_counter = ConnectionMessageCounter::new(&context); + msg_counter.start(peer_conn_config.message_sink_address.clone()); + + // Setup the destination peer's control service + let listener_address = factories::net_address::create().build().unwrap(); + let service_handle = ControlService::new(context.clone(), Arc::clone(&node_identity_a), ControlServiceConfig { + listener_address: listener_address.clone(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(2000), + }) + .serve(connection_manager) + .unwrap(); + + // Setup the requesting peer + let node_identity_b = factories::node_identity::create().build().map(Arc::new).unwrap(); + // --- Client connection for the destination peer's control service + let client_conn = Connection::new(&context, Direction::Outbound) + .establish(&listener_address) + .unwrap(); + let client = ControlServiceClient::new( + Arc::clone(&node_identity_b), + node_identity_a.identity.public_key.clone(), + client_conn, + ); + + // --- Request a connection to the peer connection + client + .send_request_connection( + node_identity_b.control_service_address().unwrap(), + NodeId::from_key(&node_identity_b.identity.public_key).unwrap(), + ) + .unwrap(); + let outcome = client + .receive_message::(Duration::from_millis(3000)) + .unwrap() + .unwrap(); + + let peer = peer_manager + .find_with_public_key(&node_identity_b.identity.public_key) + .unwrap(); + assert_eq!(peer.public_key, node_identity_b.identity.public_key); + assert_eq!(peer.node_id, node_identity_b.identity.node_id); + assert_eq!( + peer.addresses[0], + node_identity_b.control_service_address().unwrap().into() + ); + assert_eq!(peer.flags, PeerFlags::empty()); + + match outcome { + ConnectRequestOutcome::Accepted { + address, + curve_public_key, + } => { + // --- Setup outbound peer connection to the requested address + let (peer_conn, peer_conn_handle) = factories::peer_connection::create() + .with_peer_connection_context_factory( + factories::peer_connection_context::create() + .with_context(&context) + .with_direction(Direction::Outbound) + .with_address(address.clone()) + .with_message_sink_address(peer_conn_config.message_sink_address.clone()) + .with_server_public_key(curve_public_key), + ) + .build() + .unwrap(); + + peer_conn + .wait_connected_or_failure(&Duration::from_millis(3000)) + .unwrap(); + + peer_conn.shutdown().unwrap(); + peer_conn_handle.timeout_join(Duration::from_millis(3000)).unwrap(); + }, + ConnectRequestOutcome::Rejected(reason) => panic!("Connection was rejected unexpectedly: {}", reason), + } + service_handle.shutdown().unwrap(); + service_handle.timeout_join(Duration::from_millis(3000)).unwrap(); + + clean_up_datastore(database_name); +} + +#[test] +fn ping_pong() { + let database_name = "control_service_ping_pong"; + let (context, node_identity, _, connection_manager) = setup(database_name, PeerConnectionConfig::default()); + + let listener_address = factories::net_address::create().build().unwrap(); + let service = ControlService::new(context.clone(), Arc::clone(&node_identity), ControlServiceConfig { + listener_address: listener_address.clone(), + socks_proxy_address: None, + requested_connection_timeout: Duration::from_millis(2000), + }) + .serve(connection_manager) + .unwrap(); + + let client_conn = Connection::new(&context, Direction::Outbound) + .establish(&listener_address) + .unwrap(); + let client = ControlServiceClient::new( + Arc::clone(&node_identity), + node_identity.identity.public_key.clone(), + client_conn, + ); + + client.ping_pong(Duration::from_millis(2000)).unwrap().unwrap(); + + service.shutdown().unwrap(); + service.timeout_join(Duration::from_millis(3000)).unwrap(); + + clean_up_datastore(database_name); +} diff --git a/comms/tests/data/.gitkeep b/comms/tests/data/.gitkeep new file mode 100644 index 0000000000..79e790c1e5 --- /dev/null +++ b/comms/tests/data/.gitkeep @@ -0,0 +1 @@ +Temp folder for LMDB database files \ No newline at end of file diff --git a/comms/tests/mod.rs b/comms/tests/mod.rs new file mode 100644 index 0000000000..ec6cac98f4 --- /dev/null +++ b/comms/tests/mod.rs @@ -0,0 +1,30 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[macro_use] +extern crate lazy_static; + +mod connection; +mod connection_manager; +mod control_service; +mod outbound_message_service; +mod support; diff --git a/comms/tests/outbound_message_service/mod.rs b/comms/tests/outbound_message_service/mod.rs new file mode 100644 index 0000000000..6461418670 --- /dev/null +++ b/comms/tests/outbound_message_service/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE + +mod outbound_message_pool; diff --git a/comms/tests/outbound_message_service/outbound_message_pool.rs b/comms/tests/outbound_message_service/outbound_message_pool.rs new file mode 100644 index 0000000000..a8bc771203 --- /dev/null +++ b/comms/tests/outbound_message_service/outbound_message_pool.rs @@ -0,0 +1,313 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE + +use crate::support::{ + factories::{self, TestFactory}, + helpers::ConnectionMessageCounter, +}; +use std::{fs, path::PathBuf, sync::Arc, thread, time::Duration}; +use tari_comms::{ + connection::{InprocAddress, ZmqContext}, + connection_manager::{ConnectionManager, PeerConnectionConfig}, + control_service::{ControlService, ControlServiceConfig}, + message::MessageFlags, + outbound_message_service::{ + outbound_message_pool::OutboundMessagePoolConfig, + outbound_message_service::OutboundMessageService, + BroadcastStrategy, + OutboundMessagePool, + }, + peer_manager::{Peer, PeerManager}, + types::CommsDatabase, +}; +use tari_storage::{ + lmdb_store::{LMDBBuilder, LMDBError, LMDBStore}, + LMDBWrapper, +}; + +fn make_peer_connection_config(message_sink_address: InprocAddress) -> PeerConnectionConfig { + PeerConnectionConfig { + peer_connection_establish_timeout: Duration::from_secs(5), + max_message_size: 1024, + max_connections: 10, + host: "127.0.0.1".parse().unwrap(), + max_connect_retries: 3, + message_sink_address, + socks_proxy_address: None, + } +} + +fn make_peer_manager(peers: Vec, database: CommsDatabase) -> Arc { + Arc::new( + factories::peer_manager::create() + .with_peers(peers) + .with_database(database) + .build() + .unwrap(), + ) +} + +fn get_path(name: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name); + path.to_str().unwrap().to_string() +} + +fn init_datastore(name: &str) -> Result { + let path = get_path(name); + let _ = fs::create_dir(&path).unwrap_or_default(); + LMDBBuilder::new() + .set_path(&path) + .set_environment_size(10) + .set_max_number_of_databases(2) + .add_database(name, lmdb_zero::db::CREATE) + .build() +} + +fn clean_up_datastore(name: &str) { + fs::remove_dir_all(get_path(name)).unwrap(); +} + +/// This tests a message being sent through to the OMP where a peer (Node B) is awaiting alive and accepting +/// connections. +#[test] +#[allow(non_snake_case)] +fn outbound_message_pool_no_retry() { + let context = ZmqContext::new(); + let node_identity = Arc::new(factories::node_identity::create().build().unwrap()); + + //---------------------------------- Node B Setup --------------------------------------------// + + let node_B_msg_sink_address = InprocAddress::random(); + let node_B_control_port_address = factories::net_address::create().build().unwrap(); + + let node_B_msg_counter = ConnectionMessageCounter::new(&context); + node_B_msg_counter.start(node_B_msg_sink_address.clone()); + + let node_B_peer = factories::peer::create() + .with_net_addresses(vec![node_B_control_port_address.clone()]) + // Set node B's secret key to be the same as node A's so that we can generate the same shared secret + .with_public_key(node_identity.identity.public_key.clone()) + .build() + .unwrap(); + + // Node B knows no peers + let node_B_database_name = "omp_node_B_peer_manager"; // Note: every test should have unique database + let datastore = init_datastore(node_B_database_name).unwrap(); + let database = datastore.get_handle(node_B_database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let node_B_peer_manager = make_peer_manager(vec![], database); + let node_B_connection_manager = Arc::new(ConnectionManager::new( + context.clone(), + node_identity.clone(), + node_B_peer_manager, + make_peer_connection_config(node_B_msg_sink_address.clone()), + )); + + // Start node B's control service + let node_B_control_service = ControlService::new(context.clone(), node_identity.clone(), ControlServiceConfig { + socks_proxy_address: None, + listener_address: node_B_control_port_address, + requested_connection_timeout: Duration::from_millis(2000), + }) + .serve(node_B_connection_manager) + .unwrap(); + + //---------------------------------- Node A setup --------------------------------------------// + + let node_A_msg_sink_address = InprocAddress::random(); + + // Add node B to node A's peer manager + let node_A_database_name = "omp_node_A_peer_manager"; // Note: every test should have unique database + let datastore = init_datastore(node_A_database_name).unwrap(); + let database = datastore.get_handle(node_A_database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let node_A_peer_manager = make_peer_manager(vec![node_B_peer.clone()], database); + let node_A_connection_manager = Arc::new( + factories::connection_manager::create() + .with_peer_manager(node_A_peer_manager.clone()) + .with_peer_connection_config(make_peer_connection_config(node_A_msg_sink_address)) + .build() + .unwrap(), + ); + + // Setup Node A OMP and OMS + let omp_config = OutboundMessagePoolConfig::default(); + let mut omp = OutboundMessagePool::new( + omp_config.clone(), + node_A_peer_manager.clone(), + node_A_connection_manager.clone(), + ); + + let oms = OutboundMessageService::new(node_identity.clone(), omp.sender(), node_A_peer_manager.clone()).unwrap(); + + let oms2 = OutboundMessageService::new(node_identity.clone(), omp.sender(), node_A_peer_manager.clone()).unwrap(); + + omp.start().unwrap(); + let message_envelope_body = vec![0, 1, 2, 3]; + + // Send 8 message alternating two different OMS's + for _ in 0..4 { + oms.send_raw( + BroadcastStrategy::DirectNodeId(node_B_peer.node_id.clone()), + MessageFlags::ENCRYPTED, + message_envelope_body.clone(), + ) + .unwrap(); + oms2.send_raw( + BroadcastStrategy::DirectNodeId(node_B_peer.node_id.clone()), + MessageFlags::ENCRYPTED, + message_envelope_body.clone(), + ) + .unwrap(); + } + + node_B_msg_counter.assert_count(8, 30); + node_B_control_service.shutdown().unwrap(); + node_B_control_service + .timeout_join(Duration::from_millis(3000)) + .unwrap(); + + omp.shutdown().unwrap(); + clean_up_datastore(node_A_database_name); + clean_up_datastore(node_B_database_name); +} + +/// This tests the reliability of the OMP. +/// +/// This test is quite slow as it has to allow time for messages to send after a backoff period. +/// +/// 1. A message is sent through to node A's OMP, +/// 2. Node B is offline so the message is sent to the message retry service +/// 3. Node B comes online (control service is started up) +/// 4. The message retry service eventually sends the messages +/// 5. Assert that all messages have been received +#[test] +#[allow(non_snake_case)] +fn test_outbound_message_pool_fail_and_retry() { + let context = ZmqContext::new(); + + let node_A_identity = factories::node_identity::create().build().map(Arc::new).unwrap(); + //---------------------------------- Node B Setup --------------------------------------------// + + let node_B_msg_sink_address = InprocAddress::random(); + let node_B_msg_counter = ConnectionMessageCounter::new(&context); + node_B_msg_counter.start(node_B_msg_sink_address.clone()); + + let node_B_control_port_address = factories::net_address::create().build().unwrap(); + + let node_B_identity = factories::node_identity::create() + .with_control_service_address(node_B_control_port_address.clone()) + .build() + .map(Arc::new) + .unwrap(); + + let node_B_peer = factories::peer::create() + .with_net_addresses(vec![node_B_control_port_address.clone()]) + // Set node B's secret key to be the same as node A's so that we can generate the same shared secret + .with_public_key(node_B_identity.identity.public_key.clone()) + .build() + .unwrap(); + + //---------------------------------- Node A setup --------------------------------------------// + + let node_A_msg_sink_address = InprocAddress::random(); + + // Add node B to node A's peer manager + let database_name = "omp_test_outbound_message_pool_fail_and_retry1"; // Note: every test should have unique database + let datastore = init_datastore(database_name).unwrap(); + let database = datastore.get_handle(database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let node_A_peer_manager = factories::peer_manager::create() + .with_peers(vec![node_B_peer.clone()]) + .with_database(database) + .build() + .map(Arc::new) + .unwrap(); + let node_A_connection_manager = factories::connection_manager::create() + .with_context(context.clone()) + .with_node_identity(node_A_identity.clone()) + .with_peer_manager(node_A_peer_manager.clone()) + .with_peer_connection_config(make_peer_connection_config(node_A_msg_sink_address)) + .build() + .map(Arc::new) + .unwrap(); + + // Setup Node A OMP and OMS + let omp_config = OutboundMessagePoolConfig::default(); + let mut omp = OutboundMessagePool::new( + omp_config.clone(), + node_A_peer_manager.clone(), + node_A_connection_manager.clone(), + ); + + let oms = OutboundMessageService::new(node_A_identity.clone(), omp.sender(), node_A_peer_manager.clone()).unwrap(); + + omp.start().unwrap(); + let message_envelope_body = vec![0, 1, 2, 3]; + + for _ in 0..5 { + oms.send_raw( + BroadcastStrategy::DirectNodeId(node_B_peer.node_id.clone()), + MessageFlags::ENCRYPTED, + message_envelope_body.clone(), + ) + .unwrap(); + } + + thread::sleep(Duration::from_millis(1000)); + + // Later, start node B's control service and test if we receive messages + let node_B_database_name = "omp_node_B_peer_manager"; // Note: every test should have unique database + let datastore = init_datastore(node_B_database_name).unwrap(); + let database = datastore.get_handle(node_B_database_name).unwrap(); + let database = LMDBWrapper::new(Arc::new(database)); + let node_B_peer_manager = make_peer_manager(vec![], database); + let node_B_connection_manager = factories::connection_manager::create() + .with_context(context.clone()) + .with_node_identity(node_B_identity.clone()) + .with_peer_manager(node_B_peer_manager.clone()) + .with_peer_connection_config(make_peer_connection_config(node_B_msg_sink_address)) + .build() + .map(Arc::new) + .unwrap(); + + // Start node B's control service + let node_B_control_service = ControlService::new(context.clone(), node_B_identity.clone(), ControlServiceConfig { + socks_proxy_address: None, + listener_address: node_B_control_port_address, + requested_connection_timeout: Duration::from_millis(2000), + }) + .serve(node_B_connection_manager) + .unwrap(); + + // We wait for the message to retry sending + node_B_msg_counter.assert_count(5, 150); + node_B_control_service.shutdown().unwrap(); + node_B_control_service + .timeout_join(Duration::from_millis(3000)) + .unwrap(); + omp.shutdown().unwrap(); + + clean_up_datastore(database_name); +} diff --git a/comms/tests/support/comms_patterns.rs b/comms/tests/support/comms_patterns.rs new file mode 100644 index 0000000000..9ceb2b7ea6 --- /dev/null +++ b/comms/tests/support/comms_patterns.rs @@ -0,0 +1,168 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{ + sync::mpsc::{channel, Receiver}, + thread, +}; +use tari_comms::{ + connection::{ + types::{Direction, SocketType}, + zmq::{CurvePublicKey, CurveSecretKey, ZmqEndpoint}, + CurveEncryption, + ZmqContext, + }, + message::FrameSet, +}; + +/// Set the allocated stack size for each AsyncRequestReplyPattern thread +const THREAD_STACK_SIZE: usize = 32 * 1024; // 32kb + +/// Creates an [AsyncRequestReplyPattern]. +/// +/// [AsyncRequestReplyPattern]: struct.AsyncRequestReplyPattern.html +pub fn async_request_reply(direction: Direction) -> AsyncRequestReplyPattern +where T: ZmqEndpoint + Clone + Send + Sync + 'static { + AsyncRequestReplyPattern::new(direction) +} + +/// This pattern either sends a message and waits for a response or waits for +/// a response and sends a reply. +/// Once a response is received, the thread exits. This can be used to write functional +/// tests for request/reply flows. +pub struct AsyncRequestReplyPattern { + direction: Direction, + endpoint: Option, + identity: Option, + secret_key: Option, + server_public_key: Option, + frames: Option, +} + +impl AsyncRequestReplyPattern +where T: ZmqEndpoint + Clone + Send + Sync + 'static +{ + /// Create a new AsyncRequestReplyPattern + pub fn new(direction: Direction) -> Self { + AsyncRequestReplyPattern { + direction, + endpoint: None, + identity: None, + secret_key: None, + server_public_key: None, + frames: None, + } + } + + /// Set the endpoint to/from which data is sent and received + pub fn set_endpoint(mut self, v: T) -> Self { + self.endpoint = Some(v); + self + } + + /// Set the identity to use when sending data + pub fn set_identity(mut self, v: &str) -> Self { + self.identity = Some(v.to_string()); + self + } + + /// Set the secret key to use for encrypted connections. + pub fn set_secret_key(mut self, sk: CurveSecretKey) -> Self { + self.secret_key = Some(sk); + self + } + + /// Set the server public key to connect to a corresponding curve server. + pub fn set_server_public_key(mut self, spk: CurvePublicKey) -> Self { + self.server_public_key = Some(spk); + self + } + + /// Sets the data to send. + pub fn set_send_data(mut self, frames: FrameSet) -> Self { + self.frames = Some(frames); + self + } + + /// Start the thread and run the pattern! + pub fn run(self, ctx: ZmqContext) -> Receiver<()> { + let (tx, rx) = channel(); + let identity = self.identity.clone(); + let secret_key = self.secret_key.clone(); + let server_public_key = self.server_public_key.clone(); + let endpoint = self.endpoint.clone().unwrap(); + let msgs = self.frames.clone().unwrap(); + let direction = self.direction; + + thread::Builder::new() + .name("async-request-reply-pattern-thread".to_string()) + .stack_size(THREAD_STACK_SIZE) + .spawn(move || { + let socket = ctx.socket(SocketType::Dealer).unwrap(); + if let Some(i) = identity { + socket.set_identity(i.as_bytes()).unwrap(); + } + + socket.set_linger(100).unwrap(); + + match direction { + Direction::Inbound => { + if let Some(sk) = secret_key { + socket.set_curve_server(true).unwrap(); + socket.set_curve_secretkey(&sk.into_inner()).unwrap(); + } + socket.bind(endpoint.to_zmq_endpoint().as_str()).unwrap(); + + socket.recv_multipart(0).unwrap(); + + socket + .send_multipart(msgs.iter().map(|s| s.as_slice()).collect::>().as_slice(), 0) + .unwrap(); + + // Send thread done signal + tx.send(()).unwrap(); + }, + + Direction::Outbound => { + if let Some(spk) = server_public_key { + socket.set_curve_serverkey(&spk.into_inner()).unwrap(); + let keypair = CurveEncryption::generate_keypair().unwrap(); + socket.set_curve_publickey(&keypair.1.into_inner()).unwrap(); + socket.set_curve_secretkey(&keypair.0.into_inner()).unwrap(); + } + + socket.connect(endpoint.to_zmq_endpoint().as_str()).unwrap(); + socket + .send_multipart(msgs.iter().map(|s| s.as_slice()).collect::>().as_slice(), 0) + .unwrap(); + + socket.recv_multipart(0).unwrap(); + + // Send thread done signal + tx.send(()).unwrap(); + }, + } + }) + .unwrap(); + rx + } +} diff --git a/comms/tests/support/factories/connection_manager.rs b/comms/tests/support/factories/connection_manager.rs new file mode 100644 index 0000000000..2f58a7f903 --- /dev/null +++ b/comms/tests/support/factories/connection_manager.rs @@ -0,0 +1,90 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::sync::Arc; + +use super::{TestFactory, TestFactoryError}; + +use crate::support::factories::peer_manager::PeerManagerFactory; + +use crate::support::factories::node_identity::NodeIdentityFactory; +use tari_comms::{ + connection::ZmqContext, + connection_manager::{ConnectionManager, PeerConnectionConfig}, + peer_manager::{NodeIdentity, PeerManager}, +}; + +pub fn create() -> ConnectionManagerFactory { + ConnectionManagerFactory::default() +} + +#[derive(Default)] +pub struct ConnectionManagerFactory { + zmq_context: Option, + peer_connection_config: PeerConnectionConfig, + peer_manager: Option>, + peer_manager_factory: PeerManagerFactory, + node_identity_factory: NodeIdentityFactory, + node_identity: Option>, +} + +impl ConnectionManagerFactory { + factory_setter!( + with_peer_connection_config, + peer_connection_config, + PeerConnectionConfig + ); + + factory_setter!(with_peer_manager, peer_manager, Option>); + + factory_setter!(with_peer_manager_factory, peer_manager_factory, PeerManagerFactory); + + factory_setter!(with_context, zmq_context, Option); + + factory_setter!(with_node_identity, node_identity, Option>); + + factory_setter!(with_node_identity_factory, node_identity_factory, NodeIdentityFactory); +} + +impl TestFactory for ConnectionManagerFactory { + type Object = ConnectionManager; + + fn build(self) -> Result { + let zmq_context = self.zmq_context.unwrap_or(ZmqContext::new()); + + let peer_manager = match self.peer_manager { + Some(p) => p, + None => self.peer_manager_factory.build().map(Arc::new)?, + }; + + let node_identity = match self.node_identity { + Some(n) => n, + None => self.node_identity_factory.build().map(Arc::new)?, + }; + + let config = self.peer_connection_config; + + let conn_manager = ConnectionManager::new(zmq_context, node_identity, peer_manager, config); + + Ok(conn_manager) + } +} diff --git a/comms/tests/support/factories/macros.rs b/comms/tests/support/factories/macros.rs new file mode 100644 index 0000000000..fcca035a6a --- /dev/null +++ b/comms/tests/support/factories/macros.rs @@ -0,0 +1,39 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +macro_rules! factory_setter { + ($func:ident, $name: ident, Option<$type: ty>) => { + #[allow(dead_code)] + pub fn $func(mut self, val: $type) -> Self { + self.$name = Some(val); + self + } + }; + ($func:ident, $name: ident, $type: ty) => { + #[allow(dead_code)] + pub fn $func(mut self, val: $type) -> Self { + self.$name = val; + self + } + }; + +} diff --git a/infrastructure/comms/src/peer_manager/mod.rs b/comms/tests/support/factories/mod.rs similarity index 68% rename from infrastructure/comms/src/peer_manager/mod.rs rename to comms/tests/support/factories/mod.rs index 0922ee6cf7..cffa39f20c 100644 --- a/infrastructure/comms/src/peer_manager/mod.rs +++ b/comms/tests/support/factories/mod.rs @@ -20,10 +20,42 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub mod ban_list; -pub mod liveness; -pub mod manager; -pub mod message; +use derive_error::Error; +use serde::export::fmt::Debug; + +#[macro_use] +mod macros; + +pub mod connection_manager; + +pub mod net_address; + +pub mod node_identity; + pub mod peer; -pub mod routing_table; -pub mod storage; + +pub mod peer_connection; + +pub mod peer_connection_context; + +pub mod peer_manager; + +pub trait TestFactory: Default { + type Object; + + fn build(self) -> Result; +} + +#[derive(Debug, Error)] +pub enum TestFactoryError { + /// Failed to build object + #[error(msg_embedded, non_std, no_from)] + BuildFailed(String), +} + +impl TestFactoryError { + pub fn build_failed() -> impl Fn(E) -> Self + where E: Debug { + |err| TestFactoryError::BuildFailed(format!("Factory failed to build: {:?}", err)) + } +} diff --git a/comms/tests/support/factories/net_address.rs b/comms/tests/support/factories/net_address.rs new file mode 100644 index 0000000000..078da5d9fe --- /dev/null +++ b/comms/tests/support/factories/net_address.rs @@ -0,0 +1,119 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{TestFactory, TestFactoryError}; + +use crate::support::helpers::ports::get_next_local_port; + +use tari_comms::connection::NetAddress; + +use rand::OsRng; +use std::iter::repeat_with; + +pub fn create_many(n: usize) -> NetAddressesFactory { + NetAddressesFactory::default().with_count(n) +} + +pub fn create() -> NetAddressFactory { + NetAddressFactory::default() +} + +#[derive(Default, Clone)] +pub struct NetAddressesFactory { + count: Option, + net_address_factory: NetAddressFactory, +} + +impl NetAddressesFactory { + factory_setter!(with_count, count, Option); + + factory_setter!(with_net_address_factory, net_address_factory, NetAddressFactory); + + fn make_one(&self) -> NetAddress { + self.net_address_factory.clone().build().unwrap() + } +} + +impl TestFactory for NetAddressesFactory { + type Object = Vec; + + fn build(self) -> Result { + Ok(repeat_with(|| self.make_one()) + .take(self.count.or(Some(1)).unwrap()) + .collect::>()) + } +} + +//---------------------------------- NetAddressFactory --------------------------------------------// + +#[derive(Clone)] +pub struct NetAddressFactory { + rng: OsRng, + port: Option, + host: Option, + is_use_os_port: bool, +} + +impl Default for NetAddressFactory { + fn default() -> Self { + Self { + rng: OsRng::new().unwrap(), + port: None, + host: None, + is_use_os_port: false, + } + } +} + +impl NetAddressFactory { + factory_setter!(with_port, port, Option); + + factory_setter!(with_host, host, Option); + + pub fn use_os_port(mut self) -> Self { + self.is_use_os_port = true; + self + } +} + +impl TestFactory for NetAddressFactory { + type Object = NetAddress; + + fn build(self) -> Result { + let host = self.host.clone().or(Some("127.0.0.1".to_string())).unwrap(); + let port = self + .port + .clone() + .or_else(|| { + if self.is_use_os_port { + Some(0) + } else { + Some(get_next_local_port()) + } + }) + .unwrap(); + + format!("{}:{}", host, port) + .parse() + .map_err(|err| TestFactoryError::BuildFailed(format!("Failed to build NetAddress: {:?}", err))) + } +} diff --git a/comms/tests/support/factories/node_identity.rs b/comms/tests/support/factories/node_identity.rs new file mode 100644 index 0000000000..efd4e3cc49 --- /dev/null +++ b/comms/tests/support/factories/node_identity.rs @@ -0,0 +1,77 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{TestFactory, TestFactoryError}; +use rand::OsRng; +use tari_comms::{ + connection::NetAddress, + peer_manager::NodeIdentity, + types::{CommsPublicKey, CommsSecretKey}, +}; +use tari_crypto::keys::{PublicKey, SecretKey}; + +pub fn create() -> NodeIdentityFactory { + NodeIdentityFactory::default() +} + +#[derive(Default, Clone)] +pub struct NodeIdentityFactory { + control_service_address: Option, + secret_key: Option, + public_key: Option, +} + +impl NodeIdentityFactory { + factory_setter!( + with_control_service_address, + control_service_address, + Option + ); + + factory_setter!(with_secret_key, secret_key, Option); + + factory_setter!(with_public_key, public_key, Option); +} + +impl TestFactory for NodeIdentityFactory { + type Object = NodeIdentity; + + fn build(self) -> Result { + // Generate a test identity, set it and return it + let secret_key = self + .secret_key + .or(Some(CommsSecretKey::random( + &mut OsRng::new().map_err(TestFactoryError::build_failed())?, + ))) + .unwrap(); + let public_key = self + .public_key + .or(Some(CommsPublicKey::from_secret_key(&secret_key))) + .unwrap(); + let control_service_address = self + .control_service_address + .or(Some(super::net_address::create().build()?)) + .unwrap(); + + NodeIdentity::new(secret_key, public_key, control_service_address).map_err(TestFactoryError::build_failed()) + } +} diff --git a/comms/tests/support/factories/peer.rs b/comms/tests/support/factories/peer.rs new file mode 100644 index 0000000000..738912f493 --- /dev/null +++ b/comms/tests/support/factories/peer.rs @@ -0,0 +1,121 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{net_address::NetAddressesFactory, TestFactory, TestFactoryError}; + +use tari_comms::{ + connection::NetAddress, + peer_manager::{NodeId, Peer, PeerFlags}, + types::CommsPublicKey, +}; + +use crate::support::makers::{comms_keys as ristretto_maker, node_id as node_id_maker}; + +use std::iter::repeat_with; + +pub fn create_many(n: usize) -> PeersFactory { + PeersFactory::default().with_count(n) +} + +pub fn create() -> PeerFactory { + PeerFactory::default() +} + +#[derive(Default, Clone)] +pub struct PeerFactory { + node_id: Option, + flags: Option, + public_key: Option, + net_addresses_factory: NetAddressesFactory, + net_addresses: Option>, +} + +impl PeerFactory { + factory_setter!(with_node_id, node_id, Option); + + factory_setter!(with_flags, flags, Option); + + factory_setter!(with_public_key, public_key, Option); + + factory_setter!(with_net_addresses_factory, net_addresses_factory, NetAddressesFactory); + + factory_setter!(with_net_addresses, net_addresses, Option>); +} + +impl TestFactory for PeerFactory { + type Object = Peer; + + fn build(self) -> Result { + let node_id = self.node_id.clone().or(Some(node_id_maker::make_node_id())).unwrap(); + let flags = self.flags.clone().or(Some(PeerFlags::empty())).unwrap().clone(); + let public_key = self + .public_key + .clone() + .or_else(|| { + let (_, pk) = ristretto_maker::make_random_keypair(); + Some(pk) + }) + .unwrap(); + + let addresses = + self.net_addresses + .or(self.net_addresses_factory.build().ok()) + .ok_or(TestFactoryError::BuildFailed(format!( + "Failed to build net addresses for peer" + )))?; + + Ok(Peer { + node_id, + flags, + public_key, + addresses: addresses.into(), + }) + } +} + +//---------------------------------- PeersFactory --------------------------------------------// + +#[derive(Default)] +pub struct PeersFactory { + count: Option, + peer_factory: PeerFactory, +} + +impl PeersFactory { + factory_setter!(with_count, count, Option); + + factory_setter!(with_factory, peer_factory, PeerFactory); + + fn create_peer(&self) -> Peer { + self.peer_factory.clone().build().unwrap() + } +} + +impl TestFactory for PeersFactory { + type Object = Vec; + + fn build(self) -> Result { + Ok(repeat_with(|| self.create_peer()) + .take(self.count.or(Some(1)).unwrap()) + .collect::>()) + } +} diff --git a/comms/tests/support/factories/peer_connection.rs b/comms/tests/support/factories/peer_connection.rs new file mode 100644 index 0000000000..8eb4475761 --- /dev/null +++ b/comms/tests/support/factories/peer_connection.rs @@ -0,0 +1,59 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{peer_connection_context::PeerConnectionContextFactory, TestFactory, TestFactoryError}; +use std::thread::JoinHandle; +use tari_comms::connection::{ConnectionError, PeerConnection}; + +pub fn create<'c>() -> PeerConnectionFactory<'c> { + PeerConnectionFactory::default() +} + +#[derive(Default)] +pub struct PeerConnectionFactory<'c> { + peer_connection_context_factory: PeerConnectionContextFactory<'c>, +} + +impl<'c> PeerConnectionFactory<'c> { + pub fn with_peer_connection_context_factory(mut self, context_factory: PeerConnectionContextFactory<'c>) -> Self { + self.peer_connection_context_factory = context_factory; + self + } +} + +impl<'c> TestFactory for PeerConnectionFactory<'c> { + type Object = (PeerConnection, JoinHandle>); + + fn build(self) -> Result { + let peer_conn_context = self + .peer_connection_context_factory + .build() + .map_err(TestFactoryError::build_failed())?; + + let mut conn = PeerConnection::new(); + let handle = conn + .start(peer_conn_context) + .map_err(TestFactoryError::build_failed())?; + + Ok((conn, handle)) + } +} diff --git a/comms/tests/support/factories/peer_connection_context.rs b/comms/tests/support/factories/peer_connection_context.rs new file mode 100644 index 0000000000..b7ce3a0fb0 --- /dev/null +++ b/comms/tests/support/factories/peer_connection_context.rs @@ -0,0 +1,130 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{TestFactory, TestFactoryError}; + +use rand::{OsRng, Rng}; +use tari_comms::connection::{ + peer_connection::{ConnectionId, PeerConnectionContext}, + types::Linger, + CurveEncryption, + CurvePublicKey, + CurveSecretKey, + Direction, + InprocAddress, + NetAddress, + PeerConnectionContextBuilder, + ZmqContext, +}; + +pub fn create<'c>() -> PeerConnectionContextFactory<'c> { + PeerConnectionContextFactory::default() +} + +#[derive(Default)] +pub struct PeerConnectionContextFactory<'c> { + direction: Option, + context: Option<&'c ZmqContext>, + connection_id: Option, + message_sink_address: Option, + server_public_key: Option, + curve_keypair: Option<(CurveSecretKey, CurvePublicKey)>, + address: Option, + linger: Option, +} + +fn random_connection_id() -> ConnectionId { + let rng = &mut OsRng::new().unwrap(); + (0..8).map(|_| rng.gen::()).collect::>().into() +} + +impl<'c> PeerConnectionContextFactory<'c> { + factory_setter!(with_direction, direction, Option); + + factory_setter!(with_connection_id, connection_id, Option); + + factory_setter!(with_message_sink_address, message_sink_address, Option); + + factory_setter!(with_server_public_key, server_public_key, Option); + + factory_setter!( + with_curve_keypair, + curve_keypair, + Option<(CurveSecretKey, CurvePublicKey)> + ); + + factory_setter!(with_address, address, Option); + + factory_setter!(with_linger, linger, Option); + + pub fn with_context(mut self, context: &'c ZmqContext) -> Self { + self.context = Some(context); + self + } +} + +impl<'c> TestFactory for PeerConnectionContextFactory<'c> { + type Object = PeerConnectionContext; + + fn build(self) -> Result { + let context = self.context.ok_or(TestFactoryError::BuildFailed( + "Context must be set for PeerConnectionContextFactory".into(), + ))?; + + let direction = self.direction.ok_or(TestFactoryError::BuildFailed( + "Must set direction on PeerConnectionContextFactory".into(), + ))?; + + let address = self.address.or(Some("127.0.0.1:0".parse().unwrap())).unwrap(); + + let mut builder = PeerConnectionContextBuilder::new() + .set_id(self.connection_id.clone().or(Some(random_connection_id())).unwrap()) + .set_linger(self.linger.or(Some(Linger::Indefinitely)).unwrap()) + .set_direction(direction.clone()) + .set_context(context) + .set_address(address) + .set_message_sink_address(self.message_sink_address.or(Some(InprocAddress::random())).unwrap()); + + let (secret_key, public_key) = self + .curve_keypair + .unwrap_or(CurveEncryption::generate_keypair().map_err(TestFactoryError::build_failed())?); + match direction { + Direction::Inbound => { + builder = builder.set_curve_encryption(CurveEncryption::Server { + secret_key: secret_key.clone(), + }); + }, + Direction::Outbound => { + let server_public_key = self.server_public_key.or(Some(public_key.clone())).unwrap(); + builder = builder.set_curve_encryption(CurveEncryption::Client { + secret_key: secret_key.clone(), + public_key: public_key.clone(), + server_public_key, + }); + }, + } + + let peer_context = builder.build().map_err(TestFactoryError::build_failed())?; + + Ok(peer_context) + } +} diff --git a/comms/tests/support/factories/peer_manager.rs b/comms/tests/support/factories/peer_manager.rs new file mode 100644 index 0000000000..9d04ddb5a2 --- /dev/null +++ b/comms/tests/support/factories/peer_manager.rs @@ -0,0 +1,71 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{peer::PeersFactory, TestFactory, TestFactoryError}; + +use tari_comms::{ + peer_manager::{Peer, PeerManager}, + types::CommsDatabase, +}; + +pub fn create() -> PeerManagerFactory { + PeerManagerFactory::default() +} + +#[derive(Default)] +pub struct PeerManagerFactory { + peers_factory: PeersFactory, + peers: Option>, + database: Option, +} + +impl PeerManagerFactory { + factory_setter!(with_peers_factory, peers_factory, PeersFactory); + + factory_setter!(with_peers, peers, Option>); + + factory_setter!(with_database, database, Option); +} + +impl TestFactory for PeerManagerFactory { + type Object = PeerManager; + + fn build(self) -> Result { + let database = self.database.ok_or(TestFactoryError::BuildFailed( + "Failed to build peer manager: database undefined".into(), + ))?; + + let pm = PeerManager::new(database) + .map_err(|err| TestFactoryError::BuildFailed(format!("Failed to build peer manager: {:?}", err)))?; + + let peers = self + .peers + .or(self.peers_factory.build().ok()) + .ok_or(TestFactoryError::BuildFailed("Failed to build peers".into()))?; + for peer in peers { + pm.add_peer(peer) + .map_err(|err| TestFactoryError::BuildFailed(format!("Failed to build peer manager: {:?}", err)))?; + } + + Ok(pm) + } +} diff --git a/comms/tests/support/helpers/asserts.rs b/comms/tests/support/helpers/asserts.rs new file mode 100644 index 0000000000..ffbd923512 --- /dev/null +++ b/comms/tests/support/helpers/asserts.rs @@ -0,0 +1,49 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{fmt::Debug, thread, time::Duration}; + +pub fn assert_change(func: F, to: T, poll_count: usize) +where + F: Fn() -> T, + T: Eq + Debug, +{ + let mut i = 0; + loop { + let last_val = func(); + if last_val == to { + break; + } + + i += 1; + if i >= poll_count { + panic!( + "Value did not change to {:?} within {}ms (last value: {:?})", + to, + poll_count * 100, + last_val, + ); + } + + thread::sleep(Duration::from_millis(100)); + } +} diff --git a/comms/tests/support/helpers/connection_message_counter.rs b/comms/tests/support/helpers/connection_message_counter.rs new file mode 100644 index 0000000000..148a37b7dd --- /dev/null +++ b/comms/tests/support/helpers/connection_message_counter.rs @@ -0,0 +1,120 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use log::*; + +use tari_comms::connection::{zmq::ZmqEndpoint, Connection, Direction, ZmqContext}; + +use std::{ + sync::{Arc, RwLock}, + thread, + time::Duration, +}; +use tari_comms::message::FrameSet; + +const LOG_TARGET: &str = "comms::test_support::connection_message_counter"; + +/// Set the allocated stack size for each ConnectionMessageCounter thread +const THREAD_STACK_SIZE: usize = 64 * 1024; // 64kb + +pub struct ConnectionMessageCounter<'c> { + counter: Arc>, + context: &'c ZmqContext, + response: Option, +} + +impl<'c> ConnectionMessageCounter<'c> { + pub fn new(context: &'c ZmqContext) -> Self { + Self { + counter: Arc::new(RwLock::new(0)), + context, + response: None, + } + } + + pub fn set_response(&mut self, response: FrameSet) -> &mut Self { + self.response = Some(response); + self + } + + pub fn count(&self) -> u32 { + let counter_lock = acquire_read_lock!(self.counter); + *counter_lock + } + + pub fn assert_count(&self, count: u32, num_polls: usize) -> () { + for _i in 0..num_polls { + thread::sleep(Duration::from_millis(100)); + let curr_count = self.count(); + if curr_count == count { + return; + } + if curr_count > count { + panic!( + "Message count exceeded the expected count. Expected={} Actual={}", + count, curr_count + ); + } + } + panic!( + "Message count did not reach {} within {}ms. Count={}", + count, + num_polls * 100, + self.count() + ); + } + + pub fn start(&self, address: A) { + let counter = self.counter.clone(); + let context = self.context.clone(); + let address = address.clone(); + let response = self.response.clone(); + thread::Builder::new() + .name("connection-message-counter-thread".to_string()) + .stack_size(THREAD_STACK_SIZE) + .spawn(move || { + let connection = Connection::new(&context, Direction::Inbound) + .set_name("Message Counter") + .establish(&address) + .unwrap(); + + loop { + match connection.receive(1000) { + Ok(frames) => { + let mut counter_lock = acquire_write_lock!(counter); + *counter_lock += 1; + debug!(target: LOG_TARGET, "Added to message count (count={})", *counter_lock); + if let Some(ref r) = response { + let mut payload = vec![frames[0].clone()]; + payload.extend(r.clone()); + connection.send(payload).unwrap(); + } + }, + _ => { + debug!(target: LOG_TARGET, "Nothing received for 1 second..."); + }, + } + } + }) + .unwrap(); + } +} diff --git a/comms/tests/support/helpers/mod.rs b/comms/tests/support/helpers/mod.rs new file mode 100644 index 0000000000..cd2485c233 --- /dev/null +++ b/comms/tests/support/helpers/mod.rs @@ -0,0 +1,27 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod asserts; +pub mod connection_message_counter; +pub mod ports; + +pub use self::connection_message_counter::ConnectionMessageCounter; diff --git a/comms/tests/support/helpers/ports.rs b/comms/tests/support/helpers/ports.rs new file mode 100644 index 0000000000..6618555b0f --- /dev/null +++ b/comms/tests/support/helpers/ports.rs @@ -0,0 +1,44 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{cmp, ops::Range, sync::Mutex}; + +const PORT_RANGE: Range = 40000..48000; + +lazy_static! { + /// Shared counter of ports which have been used + static ref PORT_COUNTER: Mutex = Mutex::new(PORT_RANGE.start); +} + +/// Maintains a counter of ports within a range (40000..48000), returning them in +/// sequence. Port numbers will wrap back to 40000 once the upper bound is exceeded. +pub fn get_next_local_port() -> u16 { + let mut lock = match PORT_COUNTER.lock() { + Ok(guard) => guard, + Err(_) => panic!("Poisoned PORT_COUNTER"), + }; + let port = { + *lock = cmp::max((*lock + 1) % PORT_RANGE.end, PORT_RANGE.start); + *lock + }; + port +} diff --git a/comms/tests/support/macros.rs b/comms/tests/support/macros.rs new file mode 100644 index 0000000000..f87b7e601c --- /dev/null +++ b/comms/tests/support/macros.rs @@ -0,0 +1,42 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +macro_rules! acquire_lock { + ($e:expr, $m:ident) => { + match $e.$m() { + Ok(lock) => lock, + Err(poisoned) => poisoned.into_inner(), + } + }; +} + +macro_rules! acquire_write_lock { + ($e:expr) => { + acquire_lock!($e, write) + }; +} + +macro_rules! acquire_read_lock { + ($e:expr) => { + acquire_lock!($e, read) + }; +} diff --git a/comms/tests/support/makers/comms_keys.rs b/comms/tests/support/makers/comms_keys.rs new file mode 100644 index 0000000000..b589bc598b --- /dev/null +++ b/comms/tests/support/makers/comms_keys.rs @@ -0,0 +1,33 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use rand::OsRng; +use tari_crypto::{ + keys::PublicKey, + ristretto::{RistrettoPublicKey, RistrettoSecretKey}, +}; + +/// Creates a random keypair using OsRng +#[inline] +pub fn make_random_keypair() -> (RistrettoSecretKey, RistrettoPublicKey) { + RistrettoPublicKey::random_keypair(&mut OsRng::new().unwrap()) +} diff --git a/infrastructure/comms/src/rpc/mod.rs b/comms/tests/support/makers/mod.rs similarity index 97% rename from infrastructure/comms/src/rpc/mod.rs rename to comms/tests/support/makers/mod.rs index 4b45b45c20..1f8e15d1e9 100644 --- a/infrastructure/comms/src/rpc/mod.rs +++ b/comms/tests/support/makers/mod.rs @@ -20,5 +20,5 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -mod deserialize; -mod serialize; +pub mod comms_keys; +pub mod node_id; diff --git a/comms/tests/support/makers/node_id.rs b/comms/tests/support/makers/node_id.rs new file mode 100644 index 0000000000..487bc89c1b --- /dev/null +++ b/comms/tests/support/makers/node_id.rs @@ -0,0 +1,36 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use rand::{CryptoRng, OsRng, Rng}; +use tari_comms::peer_manager::NodeId; +use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; + +/// Creates a random node ID witht he given RNG +pub fn make_random_node_id_with_rng(rng: &mut RNG) -> NodeId { + let (_sk, pk) = RistrettoPublicKey::random_keypair(rng); + NodeId::from_key(&pk).unwrap() +} + +/// Creates a random node ID using OsRng +pub fn make_node_id() -> NodeId { + make_random_node_id_with_rng(&mut OsRng::new().unwrap()) +} diff --git a/infrastructure/comms/src/connection/i2p/connection.rs b/comms/tests/support/mod.rs similarity index 93% rename from infrastructure/comms/src/connection/i2p/connection.rs rename to comms/tests/support/mod.rs index 68b5374b91..b6831c0ae0 100644 --- a/infrastructure/comms/src/connection/i2p/connection.rs +++ b/comms/tests/support/mod.rs @@ -20,8 +20,11 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::connection::Connection; +#[macro_use] +mod macros; -pub struct I2PConnection {} +pub mod comms_patterns; -impl Connection for I2PConnection {} +pub mod factories; +pub mod helpers; +pub mod makers; diff --git a/digital_assets_layer/core/Cargo.toml b/digital_assets_layer/core/Cargo.toml index 99146da208..7da1ddc221 100644 --- a/digital_assets_layer/core/Cargo.toml +++ b/digital_assets_layer/core/Cargo.toml @@ -1,5 +1,5 @@ [package] name = "da_core" -version = "0.0.1" +version = "0.0.2" [dependencies] diff --git a/doc/build.md b/doc/build.md new file mode 100644 index 0000000000..3ca1bec7fd --- /dev/null +++ b/doc/build.md @@ -0,0 +1,26 @@ +# Build Instructions and Notes + +## Prerequisites + +Install ZeroMQ + +### Mac OS +```brew install pkg-config zmq``` + +### Debian, Ubuntu, Fedora, CentOS, RHEL, SUSE + +``` +echo "deb http://download.opensuse.org/repositories/network:/messaging:/zeromq:/release-stable/Debian_9.0/ ./" >> /etc/apt/sources.list +wget https://download.opensuse.org/repositories/network:/messaging:/zeromq:/release-stable/Debian_9.0/Release.key -O- | sudo apt-key add +apt-get install libzmq3-dev +``` + +### Other platforms +See [ZeroMQ documentation](http://zeromq.org/intro:get-the-software) + +## Build + +Make sure you are running nightly version: `nightly-2019-07-15` + +To build run +`cargo build` diff --git a/infrastructure/comms/Cargo.toml b/infrastructure/comms/Cargo.toml deleted file mode 100644 index 3d2c45314e..0000000000 --- a/infrastructure/comms/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "tari_comms" -description = "A peer-to-peer messaging system" -repository = "https://github.com/tari-project/tari" -homepage = "https://tari.com" -readme = "README.md" -license = "BSD-3-Clause" -version = "0.0.1" -edition = "2018" - -[dependencies] -zmq = "0.8" -derive-error = "0.0.4" -tari_crypto = { path = "../crypto"} \ No newline at end of file diff --git a/infrastructure/comms/README.md b/infrastructure/comms/README.md deleted file mode 100644 index 71e3f8ca45..0000000000 --- a/infrastructure/comms/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Tari Comms - -A peer-to-peer messaging system. - -This crate is part of the [Tari Cryptocurrency](https://tari.com) project. diff --git a/infrastructure/comms/src/lib.rs b/infrastructure/comms/src/lib.rs deleted file mode 100644 index 1ef6700ef2..0000000000 --- a/infrastructure/comms/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod connection; -pub mod peer_manager; -pub mod pubsub; -pub mod router; -pub mod rpc; diff --git a/infrastructure/comms/src/peer_manager/ban_list.rs b/infrastructure/comms/src/peer_manager/ban_list.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/peer_manager/ban_list.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/src/peer_manager/liveness.rs b/infrastructure/comms/src/peer_manager/liveness.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/peer_manager/liveness.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/src/peer_manager/manager.rs b/infrastructure/comms/src/peer_manager/manager.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/peer_manager/manager.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/src/peer_manager/message.rs b/infrastructure/comms/src/peer_manager/message.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/peer_manager/message.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/src/peer_manager/routing_table.rs b/infrastructure/comms/src/peer_manager/routing_table.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/peer_manager/routing_table.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/src/peer_manager/storage.rs b/infrastructure/comms/src/peer_manager/storage.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/peer_manager/storage.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/src/pubsub/mod.rs b/infrastructure/comms/src/pubsub/mod.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/pubsub/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/src/router.rs b/infrastructure/comms/src/router.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/router.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/src/rpc/deserialize.rs b/infrastructure/comms/src/rpc/deserialize.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/rpc/deserialize.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/src/rpc/serialize.rs b/infrastructure/comms/src/rpc/serialize.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/src/rpc/serialize.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/comms/tests/mod.rs b/infrastructure/comms/tests/mod.rs deleted file mode 100644 index 7c8005227e..0000000000 --- a/infrastructure/comms/tests/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/infrastructure/crypto/Cargo.toml b/infrastructure/crypto/Cargo.toml index 734aad7716..535cc555b7 100644 --- a/infrastructure/crypto/Cargo.toml +++ b/infrastructure/crypto/Cargo.toml @@ -7,11 +7,12 @@ categories = ["cryptography"] homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.0.2" +version = "0.0.5" edition = "2018" [dependencies] tari_utilities = { path = "../tari_util", version = "^0.0" } +base64 = "0.10.1" digest = "0.8.0" rand = "0.5.5" clear_on_drop = "0.2.3" @@ -21,8 +22,9 @@ merlin = "1.0.3" sha2 = "0.8.0" derive-error = "0.0.4" blake2 = "0.8.0" +rmp-serde = "0.13.7" serde = "1.0.89" -serde_json = { version = "1.0.39" } +serde_json = "1.0" lazy_static = "1.3.0" [features] @@ -32,6 +34,11 @@ avx2 = ["curve25519-dalek/avx2_backend", "bulletproofs/avx2_backend"] criterion = "0.2" bincode = "1.1.4" +[lib] +# Disable benchmarks to allow Criterion to take over +bench = false + [[bench]] -name = "signatures" +name = "benches" +path = "benches/mod.rs" harness = false diff --git a/base_layer/core/src/range_proof.rs b/infrastructure/crypto/benches/mod.rs similarity index 88% rename from base_layer/core/src/range_proof.rs rename to infrastructure/crypto/benches/mod.rs index ef31231229..1210fd15c1 100644 --- a/base_layer/core/src/range_proof.rs +++ b/infrastructure/crypto/benches/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2018 The Tari Project +// Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the // following conditions are met: @@ -23,9 +23,12 @@ // Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, // Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. -const RANGE_PROOF_LENGTH: usize = 1; // This will be changed +use criterion::criterion_main; -#[derive(Debug, Clone)] -pub struct RangeProof(pub [u8; RANGE_PROOF_LENGTH]); +pub mod range_proof; +pub mod signatures; -impl Copy for RangeProof {} +use range_proof::range_proofs; +use signatures::signatures; + +criterion_main!(signatures, range_proofs); diff --git a/infrastructure/crypto/benches/range_proof.rs b/infrastructure/crypto/benches/range_proof.rs new file mode 100644 index 0000000000..87d209412b --- /dev/null +++ b/infrastructure/crypto/benches/range_proof.rs @@ -0,0 +1,78 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, +// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. + +use criterion::{criterion_group, Criterion}; +use rand::{OsRng, Rng}; +use std::time::Duration; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::SecretKey, + range_proof::RangeProofService, + ristretto::{ + dalek_range_proof::DalekRangeProofService, + pedersen::{PedersenCommitment, PedersenCommitmentFactory}, + RistrettoSecretKey, + }, +}; + +fn setup(n: usize) -> (DalekRangeProofService, RistrettoSecretKey, u64, PedersenCommitment) { + let mut rng = OsRng::new().unwrap(); + let base = PedersenCommitmentFactory::default(); + let prover = DalekRangeProofService::new(n, &base).unwrap(); + let k = RistrettoSecretKey::random(&mut rng); + let n_max = 1u64 << (n - 1); + let v = rng.gen_range(1, n_max); + let c = base.commit_value(&k, v); + (prover, k, v, c) +} + +pub fn generate_rangeproof(c: &mut Criterion) { + c.bench_function_over_inputs( + "Generate range proofs", + |b, range| { + let (prover, k, v, _) = setup(**range); + b.iter(move || prover.construct_proof(&k, v).unwrap()); + }, + &[8, 16, 32, 64], + ); +} + +pub fn verify_rangeproof_valid(c: &mut Criterion) { + c.bench_function_over_inputs( + "Validate valid range proofs", + |b, range| { + let (prover, k, v, c) = setup(**range); + let proof = prover.construct_proof(&k, v).unwrap(); + b.iter(move || assert!(prover.verify(&proof, &c))); + }, + &[8, 16, 32, 64], + ); +} + +criterion_group!( +name = range_proofs; +config = Criterion::default().warm_up_time(Duration::from_millis(1_500)); +targets = generate_rangeproof, verify_rangeproof_valid +); diff --git a/infrastructure/crypto/benches/signatures.rs b/infrastructure/crypto/benches/signatures.rs index ea0523c3f6..0858eee71d 100644 --- a/infrastructure/crypto/benches/signatures.rs +++ b/infrastructure/crypto/benches/signatures.rs @@ -1,7 +1,4 @@ -#[macro_use] -extern crate criterion; - -use criterion::{BatchSize, Criterion}; +use criterion::{criterion_group, BatchSize, Criterion}; use rand::{OsRng, RngCore}; use std::time::Duration; use tari_crypto::{ @@ -11,7 +8,7 @@ use tari_crypto::{ use tari_utilities::byte_array::ByteArray; fn generate_secret_key(c: &mut Criterion) { - c.bench_function("generate secret key", |b| { + c.bench_function("Generate secret key", |b| { let mut rng = OsRng::new().unwrap(); b.iter(|| { let _ = RistrettoSecretKey::random(&mut rng); @@ -76,4 +73,3 @@ name = signatures; config = Criterion::default().warm_up_time(Duration::from_millis(500)); targets = generate_secret_key, native_keypair, sign_message, verify_message ); -criterion_main!(signatures); diff --git a/infrastructure/crypto/src/challenge.rs b/infrastructure/crypto/src/challenge.rs deleted file mode 100644 index 2c1fe84e36..0000000000 --- a/infrastructure/crypto/src/challenge.rs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -use digest::{generic_array::typenum::U32, FixedOutput}; -use sha2::Digest; - -pub type MessageHash = Vec; -pub type Challenge256Bit = [u8; 32]; - -/// A Challenge of the form H(P || R || ... || m). -/// Challenges are often used in constructing signatures. Given some hash function H, the value is a -/// scalar and can be used as a secret key. -/// `Challenge` is really just a thin wrapper around Digest, but we make use of Rust's typing system to prevent type -/// mixing -/// ## Usage -/// -/// Challenge makes use of a fluent interface to build up the challenge parts: -/// -/// ```edition2018 -/// use tari_crypto::challenge::*; -/// use sha2::Sha256; -/// -/// let challenge = Challenge::::new(); -/// challenge -/// .concat(b"The colour of magic") -/// .concat(b"The light fantastic") -/// .hash(); -/// ``` -#[derive(Clone, Copy)] -pub struct Challenge { - hasher: D, -} - -#[allow(non_snake_case)] -impl Challenge { - /// Create a new challenge instance with the [Digest](trait.Digest.html) provided - pub fn new() -> Challenge { - let hasher = D::new(); - Challenge { hasher } - } - - /// Append a new set of bytes to the hash. `concat` returns the `Challenge` instance so that you can easily chain - /// concatenation calls together. - pub fn concat(mut self, value: &[u8]) -> Self { - self.hasher.input(value); - self - } - - /// Hash the challenge input, consuming the challenge in the process. - pub fn hash(self) -> MessageHash { - self.hasher.result().to_vec() - } - - /// Convenience function that returns the hash of the input - pub fn hash_input(data: Vec) -> MessageHash { - Challenge::::new().concat(&data).hash() - } -} - -impl From> for Challenge256Bit -where D: Digest + FixedOutput -{ - fn from(challenge: Challenge) -> Challenge256Bit { - let mut v = [0u8; 32]; - let h = challenge.hash(); - v.copy_from_slice(&h[0..32]); - v as Challenge256Bit - } -} - -#[cfg(test)] -mod test { - use super::*; - use blake2::Blake2b; - use sha2::Sha256; - use tari_utilities::ByteArray; - - #[test] - fn hash_with_sha256() { - let e = Challenge::::new(); - let result = e.concat(b"Hi").concat(b"World").hash(); - assert_eq!( - result.to_hex(), - "f1007761429621683d6f843fdc3d0de3c8c02497f38cf73789cb9e41ce49fa6e" - ); - } - - #[test] - fn hash_with_blake() { - let e = Challenge::::new(); - let result = e - .concat("Now is the winter".as_bytes()) - .concat("of our discontent".as_bytes()) - .hash(); - assert_eq!(result.to_hex(), - "521143c1e862cd458164c5c48ffa354ada324ff4f20b830a5c98de205ed0c8b8b49170101a209386608fc1bc5715f6c536b4a5a74d65a02c609b80231d3d72bd"); - } -} diff --git a/infrastructure/crypto/src/commitment.rs b/infrastructure/crypto/src/commitment.rs index 84669ffbbc..fe7f597d6f 100644 --- a/infrastructure/crypto/src/commitment.rs +++ b/infrastructure/crypto/src/commitment.rs @@ -24,6 +24,7 @@ use crate::keys::PublicKey; use serde::{Deserialize, Serialize}; use std::{ cmp::Ordering, + hash::{Hash, Hasher}, ops::{Add, Sub}, }; use tari_utilities::{ByteArray, ByteArrayError}; @@ -116,6 +117,12 @@ where } } +impl Hash for HomomorphicCommitment

{ + fn hash(&self, state: &mut H) { + state.write(self.as_bytes()) + } +} + pub trait HomomorphicCommitmentFactory { type P: PublicKey; diff --git a/infrastructure/crypto/src/ristretto/dalek_range_proof.rs b/infrastructure/crypto/src/ristretto/dalek_range_proof.rs index 6c4f688404..f00c37838a 100644 --- a/infrastructure/crypto/src/ristretto/dalek_range_proof.rs +++ b/infrastructure/crypto/src/ristretto/dalek_range_proof.rs @@ -51,8 +51,8 @@ impl DalekRangeProofService { return Err(RangeProofError::InitializationError); } let pc_gens = PedersenGens { - B_blinding: base.G.clone(), - B: base.H.clone(), + B_blinding: base.G, + B: base.H, }; let bp_gens = BulletproofGens::new(64, 1); Ok(DalekRangeProofService { diff --git a/infrastructure/crypto/src/ristretto/musig.rs b/infrastructure/crypto/src/ristretto/musig.rs index 3cba6faef0..3f6c0a9126 100644 --- a/infrastructure/crypto/src/ristretto/musig.rs +++ b/infrastructure/crypto/src/ristretto/musig.rs @@ -239,7 +239,7 @@ impl RistrettoMuSig { /// but you will also know exactly _which_ signature failed. /// Otherwise pass `false` to `should_validate` and verify the aggregate signature. pub fn add_signature(self, s: &RistrettoSchnorr, should_validate: bool) -> Self { - self.handle_event(MuSigEvent::AddPartialSig(s.clone(), should_validate)) + self.handle_event(MuSigEvent::AddPartialSig(Box::new(s.clone()), should_validate)) } /// Return a reference to the standard challenge $$ H(R_{agg} || P_{agg} || m) $$, or `None` if the requisite data @@ -346,7 +346,7 @@ impl RistrettoMuSig { }, // Signature collection MuSigState::SignatureCollection(s) => match event { - MuSigEvent::AddPartialSig(sig, validate) => s.add_partial_signature::(sig, validate), + MuSigEvent::AddPartialSig(sig, validate) => s.add_partial_signature::(*sig, validate), _ => RistrettoMuSig::::invalid_transition(), }, // There's no way back from a Failed State. @@ -375,7 +375,7 @@ pub enum MuSigEvent<'a> { AddNonce(&'a RistrettoPublicKey, RistrettoPublicKey), /// In the 3rd round of MuSig, participants provide their partial signatures, after which any party can /// calculate the aggregated signature. - AddPartialSig(RistrettoSchnorr, bool), + AddPartialSig(Box, bool), } //------------------------------------- RistrettoMuSig State Definitions ------------------------------------------// @@ -387,10 +387,10 @@ pub enum MuSigEvent<'a> { /// transition attempt leads to the `Failed` state. enum MuSigState { Initialization(Initialization), - NonceHashCollection(NonceHashCollection), - NonceCollection(NonceCollection), - SignatureCollection(SignatureCollection), - Finalized(FinalizedMuSig), + NonceHashCollection(Box), + NonceCollection(Box), + SignatureCollection(Box), + Finalized(Box), Failed(MuSigError), } @@ -417,7 +417,7 @@ impl Initialization { Ok(_) => { if self.joint_key_builder.is_full() { match self.joint_key_builder.build::() { - Ok(jk) => MuSigState::NonceHashCollection(NonceHashCollection::new(jk, self.message)), + Ok(jk) => MuSigState::NonceHashCollection(Box::new(NonceHashCollection::new(jk, self.message))), Err(e) => MuSigState::Failed(e), } } else { @@ -458,9 +458,9 @@ impl NonceHashCollection { Ok(i) => { self.nonce_hashes.set_item(i, hash); if self.nonce_hashes.is_full() { - MuSigState::NonceCollection(NonceCollection::new(self)) + MuSigState::NonceCollection(Box::new(NonceCollection::new(self))) } else { - MuSigState::NonceHashCollection(self) + MuSigState::NonceHashCollection(Box::new(self)) } }, Err(_) => MuSigState::Failed(MuSigError::ParticipantNotFound), @@ -472,7 +472,7 @@ impl NonceHashCollection { return MuSigState::Failed(MuSigError::MessageAlreadySet); } self.message = Some(msg); - MuSigState::NonceHashCollection(self) + MuSigState::NonceHashCollection(Box::new(self)) } } @@ -514,9 +514,9 @@ impl NonceCollection { self.public_nonces.set_item(i, nonce); // Transition to round three iff we have all the nonces and the message has been set if self.public_nonces.is_full() && self.message.is_some() { - MuSigState::SignatureCollection(SignatureCollection::new::(self)) + MuSigState::SignatureCollection(Box::new(SignatureCollection::new::(self))) } else { - MuSigState::NonceCollection(self) + MuSigState::NonceCollection(Box::new(self)) } }, Err(_) => MuSigState::Failed(MuSigError::ParticipantNotFound), @@ -529,9 +529,9 @@ impl NonceCollection { } self.message = Some(msg); if self.public_nonces.is_full() { - MuSigState::SignatureCollection(SignatureCollection::new::(self)) + MuSigState::SignatureCollection(Box::new(SignatureCollection::new::(self))) } else { - MuSigState::NonceCollection(self) + MuSigState::NonceCollection(Box::new(self)) } } } @@ -591,9 +591,9 @@ impl SignatureCollection { return MuSigState::Failed(MuSigError::MismatchedSignatures); } if self.partial_signatures.is_full() { - MuSigState::Finalized(FinalizedMuSig::new(self)) + MuSigState::Finalized(Box::new(FinalizedMuSig::new(self))) } else { - MuSigState::SignatureCollection(self) + MuSigState::SignatureCollection(Box::new(self)) } } diff --git a/infrastructure/crypto/src/ristretto/pedersen.rs b/infrastructure/crypto/src/ristretto/pedersen.rs index 357ad6127b..fd0bfedc03 100644 --- a/infrastructure/crypto/src/ristretto/pedersen.rs +++ b/infrastructure/crypto/src/ristretto/pedersen.rs @@ -56,7 +56,7 @@ impl PedersenCommitmentFactory { /// The default Ristretto Commitment factory uses the Base point for x25519 and its first Blake256 hash. impl Default for PedersenCommitmentFactory { fn default() -> Self { - PedersenCommitmentFactory::new(RISTRETTO_PEDERSEN_G.clone(), RISTRETTO_PEDERSEN_H.clone()) + PedersenCommitmentFactory::new(RISTRETTO_PEDERSEN_G, *RISTRETTO_PEDERSEN_H) } } @@ -98,7 +98,7 @@ where T: Borrow let mut total = RistrettoPoint::default(); for c in iter { let commitment = c.borrow(); - total = total + (commitment.0).point + total += (commitment.0).point } let sum = RistrettoPublicKey::new_from_pk(total); HomomorphicCommitment(sum) diff --git a/infrastructure/crypto/src/ristretto/ristretto_keys.rs b/infrastructure/crypto/src/ristretto/ristretto_keys.rs index cc06ecc3b1..5eb4721a31 100644 --- a/infrastructure/crypto/src/ristretto/ristretto_keys.rs +++ b/infrastructure/crypto/src/ristretto/ristretto_keys.rs @@ -32,13 +32,13 @@ use curve25519_dalek::{ }; use digest::Digest; use rand::{CryptoRng, Rng}; -use serde::{Deserialize, Serialize}; use std::{ cmp::Ordering, + fmt, hash::{Hash, Hasher}, ops::{Add, Mul, Sub}, }; -use tari_utilities::{ByteArray, ByteArrayError, ExtendBytes, Hashable}; +use tari_utilities::{hex::Hex, ByteArray, ByteArrayError, ExtendBytes, Hashable}; type HashDigest = Blake2b; @@ -61,7 +61,7 @@ type HashDigest = Blake2b; /// let _k2 = RistrettoSecretKey::from_hex(&"100000002000000030000000040000000"); /// let _k3 = RistrettoSecretKey::random(&mut rng); /// ``` -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug)] pub struct RistrettoSecretKey(pub(crate) Scalar); const SCALAR_LENGTH: usize = 32; @@ -119,6 +119,13 @@ impl ByteArray for RistrettoSecretKey { } } +impl Hash for RistrettoSecretKey { + /// Require the implementation of the Hash trait for Hashmaps + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + //---------------------------------- RistrettoSecretKey Mul / Add / Sub --------------------------------------------// impl<'a, 'b> Mul<&'b RistrettoPublicKey> for &'a RistrettoSecretKey { @@ -193,10 +200,9 @@ impl From for RistrettoSecretKey { /// let sk = RistrettoSecretKey::random(&mut rng); /// let _p3 = RistrettoPublicKey::from_secret_key(&sk); /// ``` -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug)] pub struct RistrettoPublicKey { pub(crate) point: RistrettoPoint, - #[serde(skip)] pub(crate) compressed: CompressedRistretto, } @@ -260,7 +266,7 @@ impl ExtendBytes for RistrettoPublicKey { impl Hash for RistrettoPublicKey { /// Require the implementation of the Hash trait for Hashmaps fn hash(&self, state: &mut H) { - self.to_vec().hash(state); + self.as_bytes().hash(state); } } @@ -272,6 +278,14 @@ impl Default for RistrettoPublicKey { } } +//------------------------------------ PublicKey Display impl ---------------------------------------------// + +impl fmt::Display for RistrettoPublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + //------------------------------------ PublicKey PartialEq, Eq, Ord impl ---------------------------------------------// impl PartialEq for RistrettoPublicKey { @@ -418,7 +432,13 @@ mod test { use super::*; use crate::{keys::PublicKey, ristretto::test_common::get_keypair}; use rand; - use tari_utilities::{hex::Hex, message_format::MessageFormat, ByteArray}; + use tari_utilities::{message_format::MessageFormat, ByteArray}; + + fn assert_completely_equal(k1: &RistrettoPublicKey, k2: &RistrettoPublicKey) { + assert_eq!(k1, k2); + assert_eq!(k1.point, k2.point); + assert_eq!(k1.compressed, k2.compressed); + } #[test] fn test_generation() { @@ -482,7 +502,7 @@ mod test { let pk = RistrettoPublicKey::from_secret_key(&sk); let hex = pk.to_hex(); let pk2 = RistrettoPublicKey::from_hex(&hex).unwrap(); - assert_eq!(pk, pk2); + assert_completely_equal(&pk, &pk2); } #[test] @@ -501,7 +521,7 @@ mod test { let pk = RistrettoPublicKey::from_secret_key(&sk); let vec = pk.to_vec(); let pk2 = RistrettoPublicKey::from_vec(&vec).unwrap(); - assert_eq!(pk, pk2); + assert_completely_equal(&pk, &pk2); } #[test] @@ -563,14 +583,14 @@ mod test { let (k2, p2) = get_keypair(); let p_slow = &(&k1 * &p1) + &(&k2 * &p2); let b_batch = RistrettoPublicKey::batch_mul(&[k1, k2], &vec![p1, p2]); - assert_eq!(p_slow, b_batch); + assert_completely_equal(&p_slow, &b_batch); } #[test] fn create_keypair() { let mut rng = rand::OsRng::new().unwrap(); let (k, pk) = RistrettoPublicKey::random_keypair(&mut rng); - assert_eq!(pk, RistrettoPublicKey::from_secret_key(&k)); + assert_completely_equal(&pk, &RistrettoPublicKey::from_secret_key(&k)); } #[test] @@ -620,6 +640,38 @@ mod test { let k2: RistrettoSecretKey = RistrettoSecretKey::from_base64(&ser_k).unwrap(); assert_eq!(k, k2, "Deserialised secret key"); let pk2: RistrettoPublicKey = RistrettoPublicKey::from_base64(&ser_pk).unwrap(); - assert_eq!(pk, pk2, "Deserialized public key"); + assert_completely_equal(&pk, &pk2); + } + + #[test] + fn serialize_deserialize_json() { + let mut rng = rand::OsRng::new().unwrap(); + let (k, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let ser_k = k.to_json().unwrap(); + let ser_pk = pk.to_json().unwrap(); + println!("JSON pubkey: {} privkey: {}", ser_pk, ser_k); + let k2: RistrettoSecretKey = RistrettoSecretKey::from_json(&ser_k).unwrap(); + assert_eq!(k, k2, "Deserialised secret key"); + let pk2: RistrettoPublicKey = RistrettoPublicKey::from_json(&ser_pk).unwrap(); + assert_completely_equal(&pk, &pk2); + } + + #[test] + fn serialize_deserialize_binary() { + let mut rng = rand::OsRng::new().unwrap(); + let (k, pk) = RistrettoPublicKey::random_keypair(&mut rng); + let ser_k = k.to_binary().unwrap(); + let ser_pk = pk.to_binary().unwrap(); + let k2: RistrettoSecretKey = RistrettoSecretKey::from_binary(&ser_k).unwrap(); + assert_eq!(k, k2); + let pk2: RistrettoPublicKey = RistrettoPublicKey::from_binary(&ser_pk).unwrap(); + assert_completely_equal(&pk, &pk2); + } + + #[test] + fn display() { + let hex = "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76"; + let pk = RistrettoPublicKey::from_hex(hex).unwrap(); + assert_eq!(format!("{}", pk), hex); } } diff --git a/infrastructure/crypto/src/ristretto/serialize.rs b/infrastructure/crypto/src/ristretto/serialize.rs index c74920096a..0f6fda27c4 100644 --- a/infrastructure/crypto/src/ristretto/serialize.rs +++ b/infrastructure/crypto/src/ristretto/serialize.rs @@ -41,63 +41,89 @@ //! } //! ``` -use crate::keys::{PublicKey, SecretKey}; -use serde::{de, Deserializer, Serializer}; -use std::{fmt, marker::PhantomData}; -use tari_utilities::hex::Hex; +use crate::ristretto::{RistrettoPublicKey, RistrettoSecretKey}; +use serde::{ + de::{self, Visitor}, + Deserialize, + Deserializer, + Serialize, + Serializer, +}; +use std::fmt; +use tari_utilities::{byte_array::ByteArray, hex::Hex}; -pub fn serialize_to_hex(k: &K, ser: S) -> Result -where - S: Serializer, - K: Hex, -{ - ser.serialize_str(&k.to_hex()) -} +impl<'de> Deserialize<'de> for RistrettoPublicKey { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + struct RistrettoPubKeyVisitor; + + impl<'de> Visitor<'de> for RistrettoPubKeyVisitor { + type Value = RistrettoPublicKey; -pub fn secret_from_hex<'de, D, K>(des: D) -> Result -where - D: Deserializer<'de>, - K: Hex + SecretKey, -{ - struct KeyStringVisitor { - marker: PhantomData, - }; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a public key in binary format") + } - impl<'de, K: SecretKey> de::Visitor<'de> for KeyStringVisitor { - type Value = K; + fn visit_bytes(self, v: &[u8]) -> Result + where E: de::Error { + RistrettoPublicKey::from_bytes(v).map_err(E::custom) + } + } - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a secret key in hex format") + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + RistrettoPublicKey::from_hex(&s).map_err(de::Error::custom) + } else { + deserializer.deserialize_bytes(RistrettoPubKeyVisitor) } + } +} - fn visit_str(self, v: &str) -> Result - where E: de::Error { - K::from_hex(v).map_err(E::custom) +impl Serialize for RistrettoPublicKey { + fn serialize(&self, serializer: S) -> Result + where S: Serializer { + if serializer.is_human_readable() { + self.to_hex().serialize(serializer) + } else { + serializer.serialize_bytes(self.as_bytes()) } } - des.deserialize_str(KeyStringVisitor { marker: PhantomData }) } -pub fn pubkey_from_hex<'de, D, K>(des: D) -> Result -where - D: Deserializer<'de>, - K: Hex + PublicKey, -{ - struct KeyStringVisitor { - marker: PhantomData, - }; +impl<'de> Deserialize<'de> for RistrettoSecretKey { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + struct RistrettoVisitor; - impl<'de, K: PublicKey> de::Visitor<'de> for KeyStringVisitor { - type Value = K; + impl<'de> Visitor<'de> for RistrettoVisitor { + type Value = RistrettoSecretKey; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a public key in hex format") + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a secret key in binary format") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where E: de::Error { + RistrettoSecretKey::from_bytes(v).map_err(E::custom) + } + } + + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + RistrettoSecretKey::from_hex(&s).map_err(de::Error::custom) + } else { + deserializer.deserialize_bytes(RistrettoVisitor) } + } +} - fn visit_str(self, v: &str) -> Result - where E: de::Error { - K::from_hex(v).map_err(E::custom) +impl Serialize for RistrettoSecretKey { + fn serialize(&self, serializer: S) -> Result + where S: Serializer { + if serializer.is_human_readable() { + self.to_hex().serialize(serializer) + } else { + serializer.serialize_bytes(self.as_bytes()) } } - des.deserialize_str(KeyStringVisitor { marker: PhantomData }) } diff --git a/infrastructure/crypto/src/signatures.rs b/infrastructure/crypto/src/signatures.rs index a56d942adf..2b249b3d4d 100644 --- a/infrastructure/crypto/src/signatures.rs +++ b/infrastructure/crypto/src/signatures.rs @@ -18,7 +18,7 @@ pub enum SchnorrSignatureError { } #[allow(non_snake_case)] -#[derive(PartialEq, Eq, Copy, Debug, Clone, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Copy, Debug, Clone, Serialize, Deserialize, Hash)] pub struct SchnorrSignature { public_nonce: P, signature: K, diff --git a/infrastructure/derive/Cargo.toml b/infrastructure/derive/Cargo.toml index 5ca713b5bd..98a9727344 100644 --- a/infrastructure/derive/Cargo.toml +++ b/infrastructure/derive/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.0.1" +version = "0.0.5" edition = "2018" [lib] @@ -14,5 +14,6 @@ proc-macro = true [dependencies] quote = "0.6.11" -syn = "0.15.26" - +syn = "0.15.29" +blake2 = "0.8.0" +proc-macro2 = "0.4.27" diff --git a/infrastructure/derive/src/extend_bytes.rs b/infrastructure/derive/src/extend_bytes.rs new file mode 100644 index 0000000000..b214d93750 --- /dev/null +++ b/infrastructure/derive/src/extend_bytes.rs @@ -0,0 +1,111 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use quote::{quote, quote_spanned}; +use syn::{spanned::Spanned, Data, DeriveInput, Fields, Index}; + +// this is the actual code for the derive macro, the function in lib points to this one +pub fn create_derive_extend_bytes(input: DeriveInput) -> proc_macro2::TokenStream { + let object_name = &input.ident; + let item = input.data; + let fields_text = handle_fields_for_extend_bytes(&item); + let gen = quote! { + impl ExtendBytes for #object_name { + fn append_raw_bytes(&self, buf: &mut Vec) { + #fields_text + } + } + }; + gen +} + +// this function processes the individual fields of the hashable trait macro: derive_hashable +fn handle_fields_for_extend_bytes(item: &Data) -> proc_macro2::TokenStream { + match item { + Data::Struct(ref item) => { + match item.fields { + Fields::Named(ref fields) => { + let recurse = fields.named.iter().map(|f| { + let mut do_we_ignore_field = false; + for attr in &f.attrs { + match attr.interpret_meta().unwrap() { + syn::Meta::NameValue(ref val) => { + if val.ident.to_string() == "ExtendBytes" { + if let syn::Lit::Str(lit) = &val.lit { + if lit.value() == "Ignore" { + do_we_ignore_field = true; + } + } + } + }, + syn::Meta::List(ref val) => { + // we have more than one property + if val.ident.to_string() == "ExtendBytes" { + // we have a hash command here, lets search for the sub command + for nestedmeta in val.nested.iter() { + if let syn::NestedMeta::Meta(meta) = nestedmeta { + if let syn::Meta::Word(ref val) = meta { + if val.to_string() == "Ignore" { + do_we_ignore_field = true; + } + } + } + } + } + }, + _ => (), + }; + } + if !do_we_ignore_field { + let name = &f.ident; + quote_spanned! {f.span()=> + (&self.#name).append_raw_bytes(buf); + } + } else { + quote_spanned! {f.span()=> + } + } + }); + quote! {#( #recurse)* + } + }, + Fields::Unnamed(ref fields) => { + let recurse = fields.unnamed.iter().enumerate().map(|(i, f)| { + let index = Index::from(i); + quote_spanned! {f.span()=> + (&self.#index).append_raw_bytes(buf); + } + }); + quote! { + #( #recurse)* + } + }, + Fields::Unit => { + // dont hash units + quote!(0) + }, + } + }, + // have not yet implemented enums and unions + Data::Enum(_) | Data::Union(_) => unimplemented!(), + } +} diff --git a/infrastructure/derive/src/hashable.rs b/infrastructure/derive/src/hashable.rs new file mode 100644 index 0000000000..7466ce9af5 --- /dev/null +++ b/infrastructure/derive/src/hashable.rs @@ -0,0 +1,132 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use proc_macro2::{Ident, Span}; +use quote::{quote, quote_spanned}; +use syn::{spanned::Spanned, Data, DeriveInput, Fields, Index}; + +// this is the actual code for the derive macro, the function in lib points to this one +pub fn create_derive_hashable(input: DeriveInput) -> proc_macro2::TokenStream { + let object_name = &input.ident; + let mut digest = None; + for attr in &input.attrs { + match attr.interpret_meta().unwrap() { + syn::Meta::NameValue(val) => { + if val.ident.to_string() == "digest" { + if let syn::Lit::Str(lit) = &val.lit { + digest = Some(lit.value()); + } + } + }, + _ => (), + }; + } + let item = input.data; + let fields_text = handle_fields_for_hashable(&item); + + let digest = digest.expect("Could not find Digest attribute"); // this is for the error, if the Digest was not given, this error message will be displayed + let varname = Ident::new(&digest, Span::call_site()); + let gen = quote! { + impl Hashable for #object_name { + fn hash(&self) -> Vec { + let mut hasher = <#varname>::new(); + let mut buf:Vec = Vec::new(); + #fields_text + hasher.input(&buf); + hasher.result().to_vec() + } + } + }; + gen +} + +// this function processes the individual fields of the hashable trait macro: derive_hashable +fn handle_fields_for_hashable(item: &Data) -> proc_macro2::TokenStream { + match item { + Data::Struct(ref item) => { + match item.fields { + Fields::Named(ref fields) => { + let recurse = fields.named.iter().map(|f| { + let mut do_we_ignore_field = false; + for attr in &f.attrs { + match attr.interpret_meta().unwrap() { + syn::Meta::NameValue(ref val) => { + if val.ident.to_string() == "Hashable" { + if let syn::Lit::Str(lit) = &val.lit { + if lit.value() == "Ignore" { + do_we_ignore_field = true; + } + } + } + }, + syn::Meta::List(ref val) => { + // we have more than one property + if val.ident.to_string() == "Hashable" { + // we have a hash command here, lets search for the sub command + for nestedmeta in val.nested.iter() { + if let syn::NestedMeta::Meta(meta) = nestedmeta { + if let syn::Meta::Word(ref val) = meta { + if val.to_string() == "Ignore" { + do_we_ignore_field = true; + } + } + } + } + } + }, + _ => (), + }; + } + if !do_we_ignore_field { + let name = &f.ident; + quote_spanned! {f.span()=> + (&self.#name).append_raw_bytes(&mut buf); + } + } else { + quote_spanned! {f.span()=> + } + } + }); + quote! {#( #recurse)* + } + }, + Fields::Unnamed(ref fields) => { + let recurse = fields.unnamed.iter().enumerate().map(|(i, f)| { + let index = Index::from(i); + quote_spanned! {f.span()=> + (&self.#index).append_raw_bytes(&mut buf); + } + }); + quote! { + #( #recurse)* + } + }, + Fields::Unit => { + // dont hash units + quote!(0) + }, + } + }, + // have not yet implemented enums and unions + Data::Enum(_) | Data::Union(_) => unimplemented!(), + } +} diff --git a/infrastructure/derive/src/hashable_ordering.rs b/infrastructure/derive/src/hashable_ordering.rs new file mode 100644 index 0000000000..2fc9410888 --- /dev/null +++ b/infrastructure/derive/src/hashable_ordering.rs @@ -0,0 +1,50 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use proc_macro::TokenStream; +use quote::quote; + +// this is the actual code for the hashable ordering macro, function in lib points to this one +pub fn create_hashable_ordering(tokens: TokenStream) -> TokenStream { + // Parse TokenStream into AST + let ast: syn::DeriveInput = syn::parse(tokens).unwrap(); + let name = &ast.ident; + let gen = quote! { + impl Ord for #name { + fn cmp(&self, other: &#name) -> Ordering { + self.hash().cmp(&other.hash()) + } + } + impl PartialOrd for #name { + fn partial_cmp(&self, other: &#name) -> Option { + Some(self.cmp(other)) + } + } + impl PartialEq for #name { + fn eq(&self, other: &#name) -> bool { + self.hash() == other.hash() + } + } + impl Eq for #name {} + }; + gen.into() +} diff --git a/infrastructure/derive/src/lib.rs b/infrastructure/derive/src/lib.rs index ddc252fc29..3aab516790 100644 --- a/infrastructure/derive/src/lib.rs +++ b/infrastructure/derive/src/lib.rs @@ -1,35 +1,66 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #![recursion_limit = "128"] extern crate proc_macro; -extern crate syn; -#[macro_use] -extern crate quote; +extern crate proc_macro2; use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +mod extend_bytes; +mod hashable; +mod hashable_ordering; /// This macro will produce the 4 trait implementations required for an hashable struct to be sorted #[proc_macro_derive(HashableOrdering)] pub fn derive_hashable_ordering(tokens: TokenStream) -> TokenStream { - // Parse TokenStream into AST - let ast: syn::DeriveInput = syn::parse(tokens).unwrap(); - let name = &ast.ident; - let gen = quote! { - impl Ord for #name { - fn cmp(&self, other: &#name) -> Ordering { - self.hash().cmp(&other.hash()) - } - } - impl PartialOrd for #name { - fn partial_cmp(&self, other: &#name) -> Option { - Some(self.cmp(other)) - } - } - impl PartialEq for #name { - fn eq(&self, other: &#name) -> bool { - self.hash() == other.hash() - } - } - impl Eq for #name {} + hashable_ordering::create_hashable_ordering(tokens) +} + +/// This macro will provide a Hashable implementation to the a given struct using a Digest implementing Hash function +/// To use this provide #[derive(Hashable)] to the struct and #[Digest = ""] with being the included +/// digest the macro should use to impl Hashable individual fields can be skipped by providing them with: +/// #[Hashable(Ignore)] +#[proc_macro_derive(Hashable, attributes(digest, Hashable, ExtendBytes))] +pub fn derive_hashable(tokens: TokenStream) -> TokenStream { + let input = parse_macro_input!(tokens as DeriveInput); + let hash = hashable::create_derive_hashable(input.clone()); + let extendbytes = extend_bytes::create_derive_extend_bytes(input); + let tokens = quote! { + #hash + #extendbytes }; - gen.into() + tokens.into() +} + +/// This macro will provide a To_bytes implementation to the a given struct +/// To use this provide #[derive(ExtendBytes)] to the struct +/// digest the macro should use to impl Hashable individual fields can be skipped by providing them with: +/// #[ExtendBytes(Ignore)] +#[proc_macro_derive(ExtendBytes, attributes(ExtendBytes))] +pub fn derive_to_bytes(tokens: TokenStream) -> TokenStream { + let input = parse_macro_input!(tokens as DeriveInput); + extend_bytes::create_derive_extend_bytes(input).into() } diff --git a/infrastructure/merklemountainrange/Cargo.toml b/infrastructure/merklemountainrange/Cargo.toml deleted file mode 100644 index b3dd73cbed..0000000000 --- a/infrastructure/merklemountainrange/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "merklemountainrange" -description = "A general Merkle Mountain Range implementation and API" -authors = ["The Tari Development Community"] -repository = "https://github.com/tari-project/tari" -homepage = "https://tari.com" -readme = "README.md" -license = "BSD-3-Clause" -version = "0.0.1" -edition = "2018" - -[dependencies] -tari_utilities = { path = "../tari_util", version = "^0.0" } -derive-error = "0.0.4" -digest = "0.8.0" - -[dev-dependencies] -blake2 = "0.8.0" diff --git a/infrastructure/merklemountainrange/src/merklemountainrange.rs b/infrastructure/merklemountainrange/src/merklemountainrange.rs index 24788a5d1f..e69de29bb2 100644 --- a/infrastructure/merklemountainrange/src/merklemountainrange.rs +++ b/infrastructure/merklemountainrange/src/merklemountainrange.rs @@ -1,334 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -use crate::merklenode::{MerkleNode, ObjectHash}; -use digest::Digest; -use std::{collections::HashMap, marker::PhantomData}; -use tari_utilities::Hashable; - -pub struct MerkleMountainRange -where - T: Hashable, - D: Digest, -{ - // todo convert these to a bitmap - mmr: Vec, - data: HashMap, - hasher: PhantomData, - current_peak_height: (usize, usize), // we store a tuple of peak height,index -} - -impl MerkleMountainRange -where - T: Hashable, - D: Digest, -{ - /// This function creates a new empty Merkle Mountain Range - pub fn new() -> MerkleMountainRange { - MerkleMountainRange { - mmr: Vec::new(), - data: HashMap::new(), - hasher: PhantomData, - current_peak_height: (0, 0), - } - } - - /// This function returns a reference to the data stored in the mmr - /// It will return none if the hash does not exist - pub fn get_object(&self, hash: &ObjectHash) -> Option<&T> { - self.data.get(hash) - } - - /// This function returns a mut reference to the data stored in the MMR - /// It will return none if the hash does not exist - pub fn get_mut_object(&mut self, hash: &ObjectHash) -> Option<&mut T> { - self.data.get_mut(hash) - } - - pub fn get_hash(&self, index: usize) -> Option { - if index > self.get_last_added_index() { - return None; - }; - Some(self.mmr[index].hash.clone()) - } - - /// This function returns the hash proof tree of a given hash. - /// If the given hash is not in the tree, the vec will be empty. - /// The Vec will be created in form of the Lchild-Rchild-parent(Lchild)-Rchild-parent-.. - /// This pattern will be repeated until the parent is the root of the MMR - pub fn get_hash_proof(&self, hash: &ObjectHash) -> Vec { - let mut result = Vec::new(); - let mut i = self.mmr.len(); - for counter in 0..self.mmr.len() { - if self.mmr[counter].hash == *hash { - i = counter; - break; - } - } - if i == self.mmr.len() { - return result; - }; - self.get_ordered_hash_proof(i, &mut result); - - if self.current_peak_height.1 == self.get_last_added_index() { - // we know there is no bagging as the mmr is a balanced binary tree - return result; - } - - let mut peaks = self.bag_mmr(); - let mut i = peaks.len(); - let mut was_on_correct_height = false; - while i > 1 { - // was_on_correct_height is used to track should we add from this point onwards both left and right - // siblings. This loop tracks from bottom of the peaks, so we keep going up until we hit a known - // point, we then add the missing sibling from that point - if was_on_correct_height { - result.push(peaks[i - 2].clone()); - result.push(peaks[i - 1].clone()); - } else if peaks[i - 1] == result[result.len() - 1] { - result.insert(result.len() - 1, peaks[i - 2].clone()); - was_on_correct_height = true; - } else if peaks[i - 2] == result[result.len() - 1] { - result.push(peaks[i - 1].clone()); - was_on_correct_height = true; - } - - let mut hasher = D::new(); - hasher.input(&peaks[i - 2]); - hasher.input(&peaks[i - 1]); - peaks[i - 2] = hasher.result().to_vec(); - i -= 1; - } - // lets calculate the final new peak - let mut hasher = D::new(); - hasher.input(&self.mmr[self.current_peak_height.1].hash); - hasher.input(&peaks[0]); - if was_on_correct_height { - // edge case, our node is in the largest peak, we have already added it - result.push(self.mmr[self.current_peak_height.1].hash.clone()); - } - result.push(peaks[0].clone()); - result.push(hasher.result().to_vec()); - - result - } - - // This function is an iterative function. It will add the left node first then the right node to the provided array - // on the index. It will return when it reaches a single highest point. - // this function will return the index of the local peak, negating the need to search for it again. - fn get_ordered_hash_proof(&self, index: usize, results: &mut Vec) { - let sibling = sibling_index(index); - let mut next_index = index + 1; - if sibling >= self.mmr.len() { - // we are at a peak - results.push(self.mmr[index].hash.clone()); - return; - } - if sibling < index { - results.push(self.mmr[sibling].hash.clone()); - results.push(self.mmr[index].hash.clone()); - } else { - results.push(self.mmr[index].hash.clone()); - results.push(self.mmr[sibling].hash.clone()); - next_index = sibling + 1; - } - self.get_ordered_hash_proof(next_index, results); - } - - /// This function will verify the provided proof. Internally it uses the get_hash_proof function to construct a - /// similar proof. This function will return true if the proof is valid - /// If the order does not match Lchild-Rchild-parent(Lchild)-Rchild-parent-.. the validation will fail - /// This function will only succeed if the given hash is of height 0 - pub fn verify_proof(&self, hashes: &Vec) -> bool { - if hashes.len() == 0 { - return false; - } - if self.get_object(&hashes[0]).is_none() && self.get_object(&hashes[1]).is_none() { - // we only want to search for valid object's proofs, either 0 or 1 must be a valid object - return false; - } - let proof = self.get_hash_proof(&hashes[0]); - hashes.eq(&proof) - } - - // This function calculates the peak height of the mmr - fn calc_peak_height(&self) -> (usize, usize) { - let mut height_counter = 0; - let mmr_len = self.get_last_added_index(); - let mut index: usize = (1 << height_counter + 2) - 2; - let mut actual_height_index = 0; - while mmr_len >= index { - // find the height of the tree by finding if we can subtract the height +1 - height_counter += 1; - actual_height_index = index; - index = (1 << height_counter + 2) - 2; - } - (height_counter, actual_height_index) - } - - /// This function returns the peak height of the mmr - pub fn get_peak_height(&self) -> usize { - self.current_peak_height.0 - } - - /// This function will return the single merkle root of the MMR. - pub fn get_merkle_root(&self) -> ObjectHash { - let mut peaks = self.bag_mmr(); - let mut i = peaks.len(); - while i > 1 { - // lets bag all the other peaks - let mut hasher = D::new(); - hasher.input(&peaks[i - 2]); - hasher.input(&peaks[i - 1]); - peaks[i - 2] = hasher.result().to_vec(); - i -= 1; - } - if peaks.len() > 0 { - // if there was other peaks, lets bag them with the highest peak - let mut hasher = D::new(); - hasher.input(&self.mmr[self.current_peak_height.1].hash); - hasher.input(&peaks[0]); - return hasher.result().to_vec(); - } - // there was no other peaks, return the highest peak - return self.mmr[self.current_peak_height.1].hash.clone(); - } - - /// This function adds a vec of leaf nodes to the mmr. - pub fn add_vec(&mut self, objects: Vec) { - for object in objects { - self.add_single(object); - } - } - - /// This function adds a new leaf node to the mmr. - pub fn add_single(&mut self, object: T) { - let node_hash = object.hash(); - let node = MerkleNode::new(node_hash.clone()); - self.data.insert(node_hash, object); - self.mmr.push(node); - if is_node_right(self.get_last_added_index()) { - self.add_single_no_leaf(self.get_last_added_index()) - } - } - - // This function adds non leaf nodes, eg nodes that are not directly a hash of data - // This is iterative and will continue to up and till it hits the top, will be a future left child - fn add_single_no_leaf(&mut self, index: usize) { - let mut hasher = D::new(); - hasher.input(&self.mmr[sibling_index(index)].hash); - hasher.input(&self.mmr[index].hash); - let new_hash = hasher.result().to_vec(); - let new_node = MerkleNode::new(new_hash); - self.mmr.push(new_node); - if is_node_right(self.get_last_added_index()) { - self.add_single_no_leaf(self.get_last_added_index()) - } else { - self.current_peak_height = self.calc_peak_height(); // because we have now stopped adding right nodes, we need to update the height of the mmr - } - } - - // This function is just a private function to return the index of the last added node - fn get_last_added_index(&self) -> usize { - self.mmr.len() - 1 - } - - fn bag_mmr(&self) -> Vec { - // lets find all peaks of the mmr - let mut peaks = Vec::new(); - self.find_bagging_indexes( - self.current_peak_height.0 as i64, - self.current_peak_height.1, - &mut peaks, - ); - peaks - } - - fn find_bagging_indexes(&self, mut height: i64, index: usize, peaks: &mut Vec) { - let mut new_index = index + (1 << height + 1) - 1; // go the potential right sibling - while (new_index > self.get_last_added_index()) && (height > 0) { - // lets go down left child till we hit a valid node or we reach height 0 - new_index = new_index - (1 << height); - height -= 1; - } - if (new_index <= self.get_last_added_index()) && (height >= 0) { - // is this a valid peak which needs to be bagged - peaks.push(self.mmr[new_index].hash.clone()); - self.find_bagging_indexes(height, new_index, peaks); // lets go look for more peaks - } - } -} -/// This function takes in the index and calculates the index of the sibling. -pub fn sibling_index(index: usize) -> usize { - let height = get_node_height(index); - let index_count = (1 << height + 1) - 1; - if is_node_right(index) { - index - index_count - } else { - index + index_count - } -} - -/// This function takes in the index and calculates if the node is the right child node or not. -/// If the node is the tree root it will still give the answer as if it is a child of a node. -/// This function is an iterative function as we might have to subtract the largest left_most tree. -pub fn is_node_right(index: usize) -> bool { - let mut height_counter = 0; - while index >= ((1 << height_counter + 2) - 2) { - // find the height of the tree by finding if we can subtract the height +1 - height_counter += 1; - } - let height_index = (1 << height_counter + 1) - 2; - if index == height_index { - // If this is the first peak then subtracting the height of first peak will be 0 - return false; - }; - if index == (height_index + ((1 << height_counter + 1) - 1)) { - // we are looking if its the right sibling - return true; - }; - // if we are here means it was not a right node at height counter, we therefor search lower - let new_index = index - height_index - 1; - is_node_right(new_index) -} - -/// This function takes in the index and calculates the height of the node -/// This function is an iterative function as we might have to subtract the largest left_most tree. -pub fn get_node_height(index: usize) -> usize { - let mut height_counter = 0; - while index >= ((1 << height_counter + 2) - 2) { - // find the height of the tree by finding if we can subtract the height +1 - height_counter += 1; - } - let height_index = (1 << height_counter + 1) - 2; - if index == height_index { - // If this is the first peak then subtracting the height of first peak will be 0 - return height_counter; - }; - if index == (height_index + ((1 << height_counter + 1) - 1)) { - // we are looking if its the right sibling - return height_counter; - }; - // if we are here means it was not a right node at height counter, we therefor search lower - let new_index = index - height_index - 1; - get_node_height(new_index) -} diff --git a/infrastructure/merklemountainrange/tests/support/hashvalues.rs b/infrastructure/merklemountainrange/tests/support/hashvalues.rs deleted file mode 100644 index 12228089b1..0000000000 --- a/infrastructure/merklemountainrange/tests/support/hashvalues.rs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// This file only contains hashvalue lookups for the mmr tests. The values are stored in an array representing the same -// storage of the mmr. All values are Hex encoded -use digest::Digest; -use tari_utilities::hex::*; - -// this struct is used to contain already computed hashes used by blake2b -pub struct HashValues { - values: Vec, -} - -impl HashValues { - #[allow(dead_code)] // function saved for future use - pub fn get_value(&self, index: usize) -> String { - self.values[index].clone() - } - - pub fn copy_slice(&self, start: usize, end: usize) -> Vec { - self.values[start..end + 1].to_vec() - } - - pub fn copy_from_indices(&self, indices: Vec) -> Vec { - let mut result = Vec::new(); - for num in indices { - result.push(self.values[num].clone()); - } - result - } - - pub fn new() -> HashValues { - let mut hashvalues = HashValues { values: Vec::new() }; - // list of hex values of blake2b hashes - hashvalues.values.push("1ced8f5be2db23a6513eba4d819c73806424748a7bc6fa0d792cc1c7d1775a9778e894aa91413f6eb79ad5ae2f871eafcc78797e4c82af6d1cbfb1a294a10d10".to_string()); // 1 - hashvalues.values.push("c5faca15ac2f93578b39ef4b6bbb871bdedce4ddd584fd31f0bb66fade3947e6bb1353e562414ed50638a8829ff3daccac7ef4a50acee72a5384ba9aeb604fc9".to_string()); // 2 - hashvalues.values.push("4d3d9d4c8da746e2dcf236f31b53850e0e35a07c1d6082be51b33e7c1e11c39cf5e309953bf56866b0ccede95cdf3ae5f9823f6cf3bcc6ada19cf21b09884717".to_string()); // (1-2) - hashvalues.values.push("6f760b9e9eac89f07ab0223b0f4acb04d1e355d893a1b86a83f4d4b405adee99913dacb7bc3d6e6a46f996e59b965e82b1ffa1994062bcd8bef867bcf743c07c".to_string()); // 3 - hashvalues.values.push("e8e70dc170e14333627b32c20ac6051fb9b6bd369c036afbaca2d9cd7ac3de65aeda9d9651423af4343fd8e13f6481081b473e22a58f3f0e2a28143e4fb70bc2".to_string()); // 4 - hashvalues.values.push("ebf20fe26f69ab804b760fbf55eac3eba8f6cffa3f85d7b0c29ffd4a66a28deecc6f9eaaae758c49334f8b10ccfc743cee732e5486166cd3313a1881f7e0519e".to_string()); // (3-4) - hashvalues.values.push("8989c1ea10efac5b9897e9c227b307fd029005ba4f8e1590ec23942c3e788d7d280bb3cdbbd76cc9814755ee508174cb1d79a45f575a33240ac4b892ada7f850".to_string()); // (1-2)(3-4) - hashvalues.values.push("73776e3e4cd3684316d26ec93cc6c438497ace5b08e359698667af6dbded88b6750ba0b2c11ba7d52b69180f1924884a158d0b83d87ca9c65d2dae9d73387e43".to_string()); // 5 - hashvalues.values.push("8d322d4b02d9fcfb05bc70e486406e53c3cf9b97a252bf64752cafc5c2aaf95baef7f6e30d0a64826921ad01ec9d8c010805367078e5b5963ab4be3efd8f4a78".to_string()); // 6 - hashvalues.values.push("4a10141b2ba124991ddd81b4df78655f582872ba67928bbfc48282609de20ca40f745f622989cf3b71c790de6136173f6282780b2b7770b561f239ecddd40b78".to_string()); // (5-6) - // index 10 follows - hashvalues.values.push("d5c47f63555ae063383c2a0df82bf309d90932bc8dd66a056d80e4d913e821faacf7e0e962c7bbac6c193e1e638b58b8baa1e71f57a945958b84c11536b7a82d".to_string()); // 7 - hashvalues.values.push("818af2ae014b14c85a35639901ac6bfc47908bcbd94a7f5211627b1f52f316a994e1296503701dd6827a8e5969d33d1d0b68c452eb95e481035b168a6c0f09c4".to_string()); // 8 - hashvalues.values.push("5b2abeb00cacb7465131a995bd4f5463032e69e1d3d9a55823536660d130a41bb23b529eec173ddd88a42e5db97cf6983cad0b36ef3de452ac66aba9f37b08ee".to_string()); // (7-8) - hashvalues.values.push("53d5d4b1b2f78468fea0292af1cb9e63a2e7460a66cc741756166e135817f20a6b96b60a76dee7f83615d881dfd58e3830003177d4aff13e392889e36f8c5718".to_string()); // (5-6)(7-8) - hashvalues.values.push("84247c7a397b4e7314a2a5edc993b12196fcbd2d8b3793d7cf8a63e9c5c8004103874260defe34a4ac739ed21d58bb9c325f96ba9d917d63295f71f45ce0054c".to_string()); // (1-2)(3-4)(5-6)(7-8) - hashvalues.values.push("2b57bf7664a4de943d93e4f5473a42da0d7a35065afd559303196fcc33414e73a91042f8d238fcaca45a93b17e577ad15191f95c6d7cf7c19e240a1e05100ad6".to_string()); // 9 - hashvalues.values.push("f2e74cbc3eff574bbc45333c30edb947858543afda4cafdde2903324c9de0bd908b00575c556bd7b8aa2e32a32598a4d5f95cd4490b60a567a3d53680a3310f2".to_string()); // 10 - hashvalues.values.push("7c7f5fb40b9d000435c001b05ab6e1409160d24292d8acb9bbd0936a07613fa82ccb01d65b92d5cd3f2103514fba108bdf1d960eeb4c75948cb716cde5c7fb4a".to_string()); // (9-10) - hashvalues.values.push("7aa7e388f8145d395ac616bb526eaa35b10069f49e2b36d7327157d1d4af360dfbbfea805aa7e405ed025ce5eadd56c27c40b92991727a5a16b51df5604ad006".to_string()); // 11 - hashvalues.values.push("b7a5a0f0fb0c4a128b8a3e042fc860775d68d825bb3bf180479d0e12b1884e2652fe51ddb9c991b73824fc15609d82cb1cc19053db7dc7637288091f6027bbce".to_string()); // 12 - // index 20 follows - hashvalues.values.push("354db9c951738783a2d7c8c7301b1aedb4ed469df4b3bfa0368a69ab260ef0087952a7aca45ea67e7cd646aaacff6c9d75b60f194b39e6ad1f194df8b35a27c0".to_string()); // (11-12) - hashvalues.values.push("aea22e000365db9566cdab7d709c3c26e738bd41ac1f71cd2e4ad4d6f99e4286801e10d77cdea087b49ad135446130a0a32792250ba28bd211ffb68fe5d04fb0".to_string()); // (9-10)(11-12) - hashvalues.values.push("1da541ba91a8560c5dd0c1a4adc836dc4ac96bf5c407a89edb0a49d46de058a713c7b3d3fc8e0324f602c3a41978ef01dccb989eed22aa65bddc5621765713d3".to_string()); // 13 - hashvalues.values.push("2b789cf44e92c3eacb652124e394b132337fc19378664e376a932723cebf2e0da057319d509a04fe403f2c563542932d1f44476b8f4cad6ccefbd2693c432d1c".to_string()); // 14 - hashvalues.values.push("e4c46b221c1a82165c03816066af4c9546440705328dd1e419a04a17fbba70a717f67423fe1a553043c51e49cd369f02da979245007a5d09fd6ce0f2cb745491".to_string()); // (13-14) - hashvalues.values.push("4a9bb12a4834e77430779ea6759d0f4eb45abb9400a67b81985cd4b85e0a28b5d6b59f896ccc72cd6aad3390b51b02c7d6aeeb8f0dce205f425697e5180b35ae".to_string()); // 15 - hashvalues.values.push("3346703bc50521b2bf93e8d581605de18ad415c3dcdc38373e37c1800fd332e67c9ef7267d546913b63f5e24324d0c5565c177030d6c30c254d647440191d95f".to_string()); // 16 - hashvalues.values.push("39495d1ad29c6469ae18bc7316d98977754e0fdbb04a9e3e17c86c34f7fa751e09bbec588e8cfd5d4e55824b9705b1f52ab1a37b5b1fa5c8ea57b0951bdbccf3".to_string()); // (15-16) - hashvalues.values.push("77f4a6e8cb87bc79fea9893eeb2dde8a047b0d5786d324a2fb53f43414cfc8051d704f6088102fdf244de046fd5f8ea6cef854dc97488b173a0bb8d540c406ef".to_string()); // (13-14)(15-16) - hashvalues.values.push("af3f03f275e586e4449ff44146a27792b0f5a2143483a6dd6fe8405bd66a7ebe13f916d56bd3a152c2a25b6423f8b1bb4620f6d27fe55f1b82da61ff9b0825da".to_string()); // (9-10)(11-12)(13-14)(15-16) - // index 30 follows - hashvalues.values.push("9a9a504247f809735602e7fdbe191c6129c075f6e1e1530bcfd45ab5e0f1c5974cce5d3eafed04b64b5c881ce369a272f6eca5f403178a51f677aedd6fe66d84".to_string()); // (1-2)(3-4)(5-6)(7-8)(9-10)(11-12)(13-14)(15-16) - hashvalues.values.push("5c3f20d14860fb11dca47a3ea972842763165f4cd657608df25fc8afe0cd67666d906cc36b556dccf7d0f9deafbd934fa466391a4f97d03b9fd3cf48f43346ad".to_string()); // 17 - hashvalues.values.push("2344823c898d803bb0421d8e0e99dafb3feabd3fff02f98a9dae1eabf748c99c6beeb899a65c6a1a83ce60dc8c58332571ccefd11515447d69c73cb4415903a4".to_string()); // 18 - hashvalues.values.push("a6ff99c73df5c5e9e01b2d6ffd923deba66c1eaa5c60699665c941569b09c756af55aaec9ff8469c7ffe9abd3ca5a3d1ada50ed4ee2cd3ff949177975f4f5141".to_string()); // (17-18) - hashvalues.values.push("33a389ba39d39595f2e43650eeaac81187c3a11c56f2930b042325c67adad310dad7ff9ed8077cfb0fa5136a2cfa725e55d567e7dac3483d5fb0ee787a0765ec".to_string()); // 19 - hashvalues.values.push("92ce61bf50a5c299bc88d6adad5db7b68c4b61abb7760947e8b9898c99312b18ba974d427e1699ede1be7c1c25b03440235a41a71ab2b4d1410399b72da87111".to_string()); // 20 - hashvalues.values.push("74d84a50748a78c7b98dcc9e22a62d64c726cb0e30126a26d8168e7252f4a67149506a4acde7a307372ebd0a0bcd3ef5f5670434262783d41675448ab7d06e3f".to_string()); // (19-20) - hashvalues.values.push("a14d655ecdf12d3dacc2bb9c6779345db9e08fa8ddaa2163ada5d2ff3c21b9bd5b9d59f4f7fbe489543deafd0e2ca45b75d7f7fd047b83e74b85b1ea0a5ec5ff".to_string()); // (17-18)(19-20) - hashvalues.values.push("8c715c0b894785852fbc391d662e2131bf0f0c703852f25b1c07429f35dc67ec8df5998acd4cafd4f1ff7019ebfda0877f79d6b91c1b98084efbb7314258608c".to_string()); // 21 - hashvalues.values.push("3733d5bf4f3d2608ba160adf4a8cddbf545f77b417e3ee3a9e5d3b0afb351579125db853e5bce15d5e82c723f29de1ef294341f0ca3e8b3d3431cec7ac316f34".to_string()); // 22 - // index 40 follows - hashvalues.values.push("77288840877c30ddc8769efac9786505e15729f3a4736996a3b4aed483e896f001acee59b8592ae3d37acbdc60467239dac09bf80a999675b0c2aca058a4003d".to_string()); // (21-22) - hashvalues.values.push("08949f758439c6293fe5924defaf3e32bb79b9a93c1331f019c51b386557a9412b27f5a60a80bfa1f524c0d0c2e1f63c5b93d108a9a3af8cdb7fc87c765fca3f".to_string()); // 23 - - // bagging values //index start at 42 - hashvalues.values.push("bef53036fadadc8e0fb94bb5c9f33e621fa194e92dfd561c7fa752e7bbf40a164d4d8277cccc68569f30e32e3fe50aac4a7963cdb5b79111a308f8e1542c8f2d".to_string()); // (1-2)(3-4)(5-6)(7-8)(9-10)(11-12)(13-14)(15-16)(17-18)(19-20) //bagging for height 4 + 2 - hashvalues.values.push("36082c0156744c2dfaf7c73010d12b27152a10a737a7f761199bcacb702d0787cbb37540203b7741959e2aa29c2848d930ffc55f7f9dfe264086862d171bc264".to_string()); // (17-18)(19-20)(21) //bagging for height 0 + 2 - hashvalues.values.push("66b495d2f1e2ac66d24d61ea1702e965aafe645d2c49ed7ee8634a3a8c4428306274ef4e1acb40007b81ff2c68a305e90e884fb67d09f4c9c5e1564e5fabce31".to_string()); // (1-2)(3-4)(5-6)(7-8)(9-10)(11-12)(13-14)(15-16)(17-18)(19-20)(21) //bagging for height 4 + (0 + 2) - hashvalues.values.push("8af58c24745b1090ab67500d4cecef07b5e774eba09138734af78b4462db0bac8546dade7399d2733d81e84a8bec89c957cfa5cafcce51636a6abab2cc2f52e9".to_string()); // (21-22)(23) //bagging for height 0 + 1 - hashvalues.values.push("6d1af85f8083fdd4244180ba91de0a9ac7c5f9cddf7d56be6ba8c3307d986305615324966749b4206fab3f8a04102e8a571bc3bcad35cf362cad22e8eff06bc1".to_string()); // (17-18)(19-20)(21-22)(23) //bagging for height (2)(0 + 1) - hashvalues.values.push("c4ca9da79bb7c513fe8ef13f1b2c25cb990338a2f7667444c8f27170e207488b3ce375feedf14510ae888ce8eceba06eeb5ff89a11f6b1c21beb19fc8c295dba".to_string()); // (1-2)(3-4)(5-6)(7-8)(9-10)(11-12)(13-14)(15-16)(17-18)(19-20)(21-22)(23) //bagging for height (4)(2)(0 + 1) - hashvalues - } - - #[allow(dead_code)] // This function is used to generate hashvalues for the hashvalue struct - pub fn get_hash_in_hex(hash1: &Vec, hash2: &Vec) -> String { - let mut hasher = D::new(); - hasher.input(hash1); - hasher.input(hash2); - let new_hash = hasher.result().to_vec(); - to_hex(&new_hash) - } - - #[allow(dead_code)] // This function is used to generate hashvalues for the hashvalue struct - pub fn get_hash_in_u8(hash1: &Vec, hash2: &Vec) -> Vec { - let mut hasher = D::new(); - hasher.input(hash1); - hasher.input(hash2); - hasher.result().to_vec() - } -} diff --git a/infrastructure/merklemountainrange/tests/unit/mmr.rs b/infrastructure/merklemountainrange/tests/unit/mmr.rs deleted file mode 100644 index f775e6e432..0000000000 --- a/infrastructure/merklemountainrange/tests/unit/mmr.rs +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -use crate::support::{hashvalues::HashValues, testobject::TestObject}; -use blake2::Blake2b; -use merklemountainrange::mmr::{self, *}; -use tari_utilities::hex::*; - -fn create_mmr(leaves: u32) -> MerkleMountainRange, Blake2b> { - let mut mmr: MerkleMountainRange, Blake2b> = MerkleMountainRange::new(); - for i in 1..leaves + 1 { - let object: TestObject = TestObject::new(i.to_string()); - mmr.add_single(object); - } - mmr -} - -#[test] -fn create_small_mmr() { - let mmr = create_mmr(2); - assert_eq!(1, mmr.get_peak_height()); - let hash_values = HashValues::new(); - let hash0 = mmr.get_hash(0).unwrap(); - let proof = mmr.get_hash_proof(&hash0); - let mut our_proof = Vec::new(); - for i in 0..3 { - our_proof.push(mmr.get_hash(i).unwrap()); - } - assert_eq!(hash_values.copy_slice(0, 2), to_hex_multiple(&proof)); - assert_eq!(mmr.verify_proof(&our_proof), true); - assert_eq!(mmr.get_merkle_root(), mmr.get_hash(2).unwrap()) -} - -#[test] -fn create_mmr_with_2_peaks() { - let mmr = create_mmr(20); - assert_eq!(4, mmr.get_peak_height()); - let hash_values = HashValues::new(); - - let hash0 = mmr.get_hash(0).unwrap(); - let proof = mmr.get_hash_proof(&hash0); - let our_proof = hash_values.copy_from_indices(vec![0, 1, 2, 5, 6, 13, 14, 29, 30, 37, 42]); - assert_eq!(to_hex_multiple(&proof), our_proof); - assert_eq!(mmr.verify_proof(&proof), true); - - let proof = mmr.get_hash_proof(&mmr.get_hash(1).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![0, 1, 2, 5, 6, 13, 14, 29, 30, 37, 42]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - // test some more proofs - let proof = mmr.get_hash_proof(&mmr.get_hash(6).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![6, 13, 14, 29, 30, 37, 42]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(22).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![22, 23, 24, 27, 21, 28, 14, 29, 30, 37, 42]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(26).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![25, 26, 24, 27, 21, 28, 14, 29, 30, 37, 42]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(14).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![14, 29, 30, 37, 42]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(11).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![10, 11, 9, 12, 6, 13, 14, 29, 30, 37, 42]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - assert_eq!(to_hex(&mmr.get_merkle_root()), hash_values.get_value(42)); -} - -#[test] -fn mmr_with_3_peaks() { - let mmr = create_mmr(21); - assert_eq!(4, mmr.get_peak_height()); - let mut raw = Vec::new(); - for i in 0..39 { - raw.push(mmr.get_hash(i).unwrap()); - } - let hash_values = HashValues::new(); - let proof = mmr.get_hash_proof(&mmr.get_hash(35).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![34, 35, 33, 36, 37, 38, 30, 43, 44]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(38).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![37, 38, 30, 43, 44]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(0).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![0, 1, 2, 5, 6, 13, 14, 29, 30, 43, 44]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - assert_eq!(to_hex(&mmr.get_merkle_root()), hash_values.get_value(44)); -} - -#[test] -fn mmr_with_4_peaks() { - let mmr = create_mmr(23); - assert_eq!(4, mmr.get_peak_height()); - let mut raw = Vec::new(); - for i in 0..42 { - raw.push(mmr.get_hash(i).unwrap()); - } - let hash_values = HashValues::new(); - assert_eq!(to_hex(&mmr.get_merkle_root()), hash_values.get_value(47)); - - let proof = mmr.get_hash_proof(&mmr.get_hash(35).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![34, 35, 33, 36, 37, 45, 30, 46, 47]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(34).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![34, 35, 33, 36, 37, 45, 30, 46, 47]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(21).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![21, 28, 14, 29, 30, 46, 47]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(41).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![40, 41, 37, 45, 30, 46, 47]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(0).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![0, 1, 2, 5, 6, 13, 14, 29, 30, 46, 47]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(1).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![0, 1, 2, 5, 6, 13, 14, 29, 30, 46, 47]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(21).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![21, 28, 14, 29, 30, 46, 47]); - assert_eq!(to_hex_multiple(&proof), our_proof); - - let proof = mmr.get_hash_proof(&mmr.get_hash(28).unwrap()); - let our_proof = hash_values.copy_from_indices(vec![21, 28, 14, 29, 30, 46, 47]); - assert_eq!(to_hex_multiple(&proof), our_proof); -} - -#[test] -fn very_large_mmr() { - // test test only tests that it doesn't crash currently, we need to create fuzz testing to test this properly - let mmr = create_mmr(23000); - let _merkle_root = mmr.get_merkle_root(); - let proof = mmr.get_hash_proof(&mmr.get_hash(1).unwrap()); - assert_eq!(mmr.verify_proof(&proof), true); -} - -#[test] -fn test_node_sides() { - // test some true - assert_eq!(mmr::is_node_right(11), true); - assert_eq!(mmr::is_node_right(20), true); - assert_eq!(mmr::is_node_right(35), true); - assert_eq!(mmr::is_node_right(36), true); - assert_eq!(mmr::is_node_right(29), true); - assert_eq!(mmr::is_node_right(13), true); - assert_eq!(mmr::is_node_right(1), true); - assert_eq!(mmr::is_node_right(28), true); - assert_eq!(mmr::is_node_right(5), true); - assert_eq!(mmr::is_node_right(23), true); - // test some false - assert_eq!(mmr::is_node_right(0), false); - assert_eq!(mmr::is_node_right(34), false); - assert_eq!(mmr::is_node_right(21), false); - assert_eq!(mmr::is_node_right(7), false); - assert_eq!(mmr::is_node_right(34), false); - assert_eq!(mmr::is_node_right(14), false); - assert_eq!(mmr::is_node_right(10), false); - assert_eq!(mmr::is_node_right(30), false); - assert_eq!(mmr::is_node_right(15), false); - assert_eq!(mmr::is_node_right(37), false); -} - -#[test] -fn test_node_heights() { - // test some 0 - assert_eq!(mmr::get_node_height(11), 0); - assert_eq!(mmr::get_node_height(10), 0); - assert_eq!(mmr::get_node_height(0), 0); - assert_eq!(mmr::get_node_height(11), 0); - assert_eq!(mmr::get_node_height(1), 0); - assert_eq!(mmr::get_node_height(16), 0); - assert_eq!(mmr::get_node_height(23), 0); - assert_eq!(mmr::get_node_height(35), 0); - assert_eq!(mmr::get_node_height(32), 0); - assert_eq!(mmr::get_node_height(34), 0); - assert_eq!(mmr::get_node_height(19), 0); - assert_eq!(mmr::get_node_height(8), 0); - - // test some 1 - assert_eq!(mmr::get_node_height(2), 1); - assert_eq!(mmr::get_node_height(5), 1); - assert_eq!(mmr::get_node_height(20), 1); - assert_eq!(mmr::get_node_height(27), 1); - assert_eq!(mmr::get_node_height(36), 1); - - // some larger - assert_eq!(mmr::get_node_height(6), 2); - assert_eq!(mmr::get_node_height(13), 2); - assert_eq!(mmr::get_node_height(21), 2); - assert_eq!(mmr::get_node_height(37), 2); - assert_eq!(mmr::get_node_height(14), 3); - assert_eq!(mmr::get_node_height(29), 3); - assert_eq!(mmr::get_node_height(30), 4); - assert_eq!(mmr::get_node_height(62), 5); -} diff --git a/infrastructure/storage/Cargo.toml b/infrastructure/storage/Cargo.toml index cac56184c2..0e9c80660c 100644 --- a/infrastructure/storage/Cargo.toml +++ b/infrastructure/storage/Cargo.toml @@ -5,17 +5,22 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.0.1" +version = "0.0.5" edition = "2018" [dependencies] -bincode = "1.0.1" +bincode = "1.1" derive-error = "0.0.4" +log = "0.4.0" lmdb-zero = "0.4.4" rmp = "0.8.7" rmp-serde = "0.13.7" serde = "1.0.80" serde_derive = "1.0.80" +tari_utilities = { path = "../tari_util", version = "^0.0" } +bytes = "0.4.12" [dev-dependencies] rand = "0.5.5" +sys-info = "0.5.6" +simple_logger = "1.2.0" diff --git a/infrastructure/storage/src/key_val_store/error.rs b/infrastructure/storage/src/key_val_store/error.rs new file mode 100644 index 0000000000..4b18d626aa --- /dev/null +++ b/infrastructure/storage/src/key_val_store/error.rs @@ -0,0 +1,40 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; + +#[derive(Debug, Error)] +pub enum KeyValStoreError { + /// The Thread Safety has been breached and the data access has become poisoned + PoisonedAccess, + /// An error occurred with the key value query or store + #[error(no_from, non_std)] + DatabaseError(String), + /// An error occurred during serialization + #[error(no_from, non_std)] + SerializationError(String), + /// An error occurred during deserialization + #[error(no_from, non_std)] + DeserializationError(String), + /// The specified key did not exist in the key-val store + KeyNotFound, +} diff --git a/infrastructure/storage/src/key_val_store/hmap_database.rs b/infrastructure/storage/src/key_val_store/hmap_database.rs new file mode 100644 index 0000000000..96cc6a03da --- /dev/null +++ b/infrastructure/storage/src/key_val_store/hmap_database.rs @@ -0,0 +1,199 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::key_val_store::{error::KeyValStoreError, key_val_store::KeyValueStore}; +use std::{collections::HashMap, hash::Hash, sync::RwLock}; + +/// The HMapDatabase mimics the behaviour of LMDBDatabase without keeping a persistent copy of the key-value records. +/// It allows key-value pairs to be inserted, retrieved and removed in a thread-safe manner. +#[derive(Default)] +pub struct HMapDatabase { + db: RwLock>, +} + +impl HMapDatabase { + /// Creates a new empty HMapDatabase with the specified name + pub fn new() -> Self { + Self { + db: RwLock::new(HashMap::new()), + } + } + + /// Inserts a key-value record into the database. Internally, `insert` serializes the key and value using bincode + /// and adds the pair into HashMap guarded with a RwLock. + pub fn insert(&self, key: K, value: V) -> Result<(), KeyValStoreError> { + self.db + .write() + .map_err(|_| KeyValStoreError::PoisonedAccess)? + .insert(key, value); + Ok(()) + } + + /// Get a value from the key-value database. The retrieved value is deserialized from bincode into `V` + pub fn get(&self, key: &K) -> Result, KeyValStoreError> { + match self.db.read().map_err(|_| KeyValStoreError::PoisonedAccess)?.get(key) { + Some(val) => Ok(Some(val.clone())), + None => Ok(None), + } + } + + /// Returns if the key-value database is empty + pub fn is_empty(&self) -> Result { + Ok(self.db.read().map_err(|_| KeyValStoreError::PoisonedAccess)?.is_empty()) + } + + /// Returns the total number of entries recorded in the key-value database. + pub fn len(&self) -> Result { + Ok(self.db.read().map_err(|_| KeyValStoreError::PoisonedAccess)?.len()) + } + + /// Iterate over all the stored records and execute the function `f` for each pair in the key-value database. + pub fn for_each(&self, mut f: F) -> Result<(), KeyValStoreError> + where F: FnMut(Result<(K, V), KeyValStoreError>) { + for (key, val) in self.db.read().map_err(|_| KeyValStoreError::PoisonedAccess)?.iter() { + f(Ok((key.clone(), val.clone()))); + } + Ok(()) + } + + /// Checks whether a record exist in the key-value database that corresponds to the provided `key`. + pub fn contains_key(&self, key: &K) -> Result { + Ok(self + .db + .read() + .map_err(|_| KeyValStoreError::PoisonedAccess)? + .contains_key(key)) + } + + /// Remove the record from the key-value database that corresponds with the provided `key`. + pub fn remove(&self, key: &K) -> Result<(), KeyValStoreError> { + match self + .db + .write() + .map_err(|_| KeyValStoreError::PoisonedAccess)? + .remove(key) + { + Some(_) => Ok(()), + None => Err(KeyValStoreError::KeyNotFound), + } + } +} + +impl KeyValueStore for HMapDatabase { + /// Inserts a key-value pair into the key-value database. + fn insert(&self, key: K, value: V) -> Result<(), KeyValStoreError> { + self.insert(key, value) + } + + /// Get the value corresponding to the provided key from the key-value database. + fn get(&self, key: &K) -> Result, KeyValStoreError> { + self.get(key) + } + + /// Returns the total number of entries recorded in the key-value database. + fn size(&self) -> Result { + self.len() + } + + /// Iterate over all the stored records and execute the function `f` for each pair in the key-value database. + fn for_each(&self, f: F) -> Result<(), KeyValStoreError> + where F: FnMut(Result<(K, V), KeyValStoreError>) { + self.for_each(f) + } + + /// Checks whether a record exist in the key-value database that corresponds to the provided `key`. + fn exists(&self, key: &K) -> Result { + self.contains_key(key) + } + + /// Remove the record from the key-value database that corresponds with the provided `key`. + fn delete(&self, key: &K) -> Result<(), KeyValStoreError> { + self.remove(key) + } +} + +#[cfg(test)] +mod test { + use super::*; + use serde::{Deserialize, Serialize}; + + #[test] + fn test_hmap_kvstore() { + let db = HMapDatabase::new(); + + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] + struct Foo { + value: String, + } + + let val1 = Foo { + value: "one".to_string(), + }; + let val2 = Foo { + value: "two".to_string(), + }; + let val3 = Foo { + value: "three".to_string(), + }; + + db.insert(1, val1.clone()).unwrap(); + db.insert(2, val2.clone()).unwrap(); + db.insert(3, val3.clone()).unwrap(); + + assert_eq!(db.get(&1).unwrap().unwrap(), val1); + assert_eq!(db.get(&2).unwrap().unwrap(), val2); + assert_eq!(db.get(&3).unwrap().unwrap(), val3); + assert!(db.get(&4).unwrap().is_none()); + assert_eq!(db.size().unwrap(), 3); + assert!(db.exists(&1).unwrap()); + assert!(db.exists(&2).unwrap()); + assert!(db.exists(&3).unwrap()); + assert!(!db.exists(&4).unwrap()); + + db.remove(&2).unwrap(); + assert_eq!(db.get(&1).unwrap().unwrap(), val1); + assert!(db.get(&2).unwrap().is_none()); + assert_eq!(db.get(&3).unwrap().unwrap(), val3); + assert!(db.get(&4).unwrap().is_none()); + assert_eq!(db.size().unwrap(), 2); + assert!(db.exists(&1).unwrap()); + assert!(!db.exists(&2).unwrap()); + assert!(db.exists(&3).unwrap()); + assert!(!db.exists(&4).unwrap()); + + // Only Key1 and Key3 should be in key-value database, but order is not known + let mut key1_found = false; + let mut key3_found = false; + let _res = db.for_each(|pair| { + let (key, val) = pair.unwrap(); + if key == 1 { + key1_found = true; + assert_eq!(val, val1); + } else if key == 3 { + key3_found = true; + assert_eq!(val, val3); + } + }); + assert!(key1_found); + assert!(key3_found); + } +} diff --git a/infrastructure/storage/src/key_val_store/key_val_store.rs b/infrastructure/storage/src/key_val_store/key_val_store.rs new file mode 100644 index 0000000000..2a5c0b7011 --- /dev/null +++ b/infrastructure/storage/src/key_val_store/key_val_store.rs @@ -0,0 +1,55 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::key_val_store::KeyValStoreError; + +/// General CRUD behaviour of Key-value store implementations. +pub trait KeyValueStore { + /// Inserts a key-value pair into the key-value database. + fn insert(&self, key: K, value: V) -> Result<(), KeyValStoreError>; + + /// Get the value corresponding to the provided key from the key-value database. + fn get(&self, key: &K) -> Result, KeyValStoreError>; + + /// Returns the total number of entries recorded in the key-value database. + fn size(&self) -> Result; + + /// Execute function `f` for each value in the database. + /// + /// `f` is a closure of form `|pair: Result<(K,V), KeyValStoreError>| -> ()`. You will usually need to include type + /// inference to let Rust know which type to deserialise to: + /// ```nocompile + /// let res = db.for_each::(|pair| { + /// let (key, val) = pair.unwrap(); + /// //.. do stuff with key and val.. + /// }); + fn for_each(&self, f: F) -> Result<(), KeyValStoreError> + where + Self: Sized, + F: FnMut(Result<(K, V), KeyValStoreError>); + + /// Checks whether the provided `key` exists in the key-value database. + fn exists(&self, key: &K) -> Result; + + /// Delete a key-pair record associated with the provided `key` from the key-pair database. + fn delete(&self, key: &K) -> Result<(), KeyValStoreError>; +} diff --git a/infrastructure/storage/src/key_val_store/lmdb_database.rs b/infrastructure/storage/src/key_val_store/lmdb_database.rs new file mode 100644 index 0000000000..becb0bffee --- /dev/null +++ b/infrastructure/storage/src/key_val_store/lmdb_database.rs @@ -0,0 +1,201 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + key_val_store::{key_val_store::KeyValueStore, KeyValStoreError}, + lmdb_store::LMDBDatabase, +}; +use lmdb_zero::traits::AsLmdbBytes; +use serde::{de::DeserializeOwned, export::PhantomData, Serialize}; +use std::sync::Arc; + +/// This is a simple wrapper struct that lifts the generic parameters so that KeyValStore can be implemented on +/// LMDBDatabase. LMDBDatabase doesn't have the generics at the struct level because the LMDBStore can contain many +/// instances of LMDBDatabase each with different K and V types. +pub struct LMDBWrapper { + inner: Arc, + _k: PhantomData, + _v: PhantomData, +} + +impl LMDBWrapper { + /// Wrap a LMDBDatabase instance so that it implements [KeyValueStore] + pub fn new(db: Arc) -> LMDBWrapper { + LMDBWrapper { + inner: db, + _k: PhantomData, + _v: PhantomData, + } + } + + /// Get access to the underlying LMDB database + pub fn inner(&self) -> Arc { + Arc::clone(&self.inner) + } +} + +impl KeyValueStore for LMDBWrapper +where + K: AsLmdbBytes + DeserializeOwned, + V: Serialize + DeserializeOwned, +{ + /// Inserts a key-value pair into the key-value database. + fn insert(&self, key: K, value: V) -> Result<(), KeyValStoreError> { + self.inner + .insert::(&key, &value) + .map_err(|e| KeyValStoreError::DatabaseError(e.to_string())) + } + + /// Get the value corresponding to the provided key from the key-value database. + fn get(&self, key: &K) -> Result, KeyValStoreError> + where for<'t> V: serde::de::DeserializeOwned { + self.inner + .get::(key) + .map_err(|e| KeyValStoreError::DatabaseError(e.to_string())) + } + + /// Returns the total number of entries recorded in the key-value database. + fn size(&self) -> Result { + self.inner + .len() + .map_err(|e| KeyValStoreError::DatabaseError(e.to_string())) + } + + /// Iterate over all the stored records and execute the function `f` for each pair in the key-value database. + fn for_each(&self, f: F) -> Result<(), KeyValStoreError> + where F: FnMut(Result<(K, V), KeyValStoreError>) { + self.inner + .for_each::(f) + .map_err(|e| KeyValStoreError::DatabaseError(e.to_string())) + } + + /// Checks whether a record exist in the key-value database that corresponds to the provided `key`. + fn exists(&self, key: &K) -> Result { + self.inner + .contains_key::(key) + .map_err(|e| KeyValStoreError::DatabaseError(e.to_string())) + } + + /// Remove the record from the key-value database that corresponds with the provided `key`. + fn delete(&self, key: &K) -> Result<(), KeyValStoreError> { + self.inner + .remove::(key) + .map_err(|e| KeyValStoreError::DatabaseError(e.to_string())) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::lmdb_store::{LMDBBuilder, LMDBError, LMDBStore}; + use serde::{Deserialize, Serialize}; + use std::path::PathBuf; + + fn get_path(name: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name); + path.to_str().unwrap().to_string() + } + + fn init_datastore(name: &str) -> Result { + let path = get_path(name); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + LMDBBuilder::new() + .set_path(&path) + .set_environment_size(10) + .set_max_number_of_databases(2) + .add_database(name, lmdb_zero::db::CREATE) + .build() + } + + fn clean_up_datastore(name: &str) { + std::fs::remove_dir_all(get_path(name)).unwrap(); + } + + #[test] + fn test_lmdb_kvstore() { + let database_name = "test_lmdb_kvstore"; // Note: every test should have unique database + let datastore = init_datastore(database_name).unwrap(); + let db = datastore.get_handle(database_name).unwrap(); + let db = LMDBWrapper::new(Arc::new(db)); + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] + struct Foo { + value: String, + } + let key1 = 1 as u64; + let key2 = 2 as u64; + let key3 = 3 as u64; + let key4 = 4 as u64; + let val1 = Foo { + value: "one".to_string(), + }; + let val2 = Foo { + value: "two".to_string(), + }; + let val3 = Foo { + value: "three".to_string(), + }; + db.insert(1, val1.clone()).unwrap(); + db.insert(2, val2.clone()).unwrap(); + db.insert(3, val3.clone()).unwrap(); + + assert_eq!(db.get(&1).unwrap().unwrap(), val1); + assert_eq!(db.get(&2).unwrap().unwrap(), val2); + assert_eq!(db.get(&3).unwrap().unwrap(), val3); + assert!(db.get(&4).unwrap().is_none()); + assert_eq!(db.size().unwrap(), 3); + assert!(db.exists(&key1).unwrap()); + assert!(db.exists(&key2).unwrap()); + assert!(db.exists(&key3).unwrap()); + assert!(!db.exists(&key4).unwrap()); + + db.delete(&key2).unwrap(); + assert_eq!(db.get(&key1).unwrap().unwrap(), val1); + assert!(db.get(&key2).unwrap().is_none()); + assert_eq!(db.get(&key3).unwrap().unwrap(), val3); + assert!(db.get(&key4).unwrap().is_none()); + assert_eq!(db.size().unwrap(), 2); + assert!(db.exists(&key1).unwrap()); + assert!(!db.exists(&key2).unwrap()); + assert!(db.exists(&key3).unwrap()); + assert!(!db.exists(&key4).unwrap()); + + // Only Key1 and Key3 should be in key-value database, but order is not known + let mut key1_found = false; + let mut key3_found = false; + let _res = db.for_each(|pair| { + let (key, val) = pair.unwrap(); + if key == key1 { + key1_found = true; + assert_eq!(val, val1); + } else if key == key3 { + key3_found = true; + assert_eq!(val, val3); + } + }); + assert!(key1_found); + assert!(key3_found); + + clean_up_datastore(database_name); + } +} diff --git a/infrastructure/storage/src/key_val_store/mod.rs b/infrastructure/storage/src/key_val_store/mod.rs new file mode 100644 index 0000000000..5e19529409 --- /dev/null +++ b/infrastructure/storage/src/key_val_store/mod.rs @@ -0,0 +1,30 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod error; +pub mod hmap_database; +pub mod key_val_store; +pub mod lmdb_database; + +pub use error::KeyValStoreError; +pub use hmap_database::HMapDatabase; +pub use key_val_store::KeyValueStore; diff --git a/infrastructure/storage/src/keyvalue_store.rs b/infrastructure/storage/src/keyvalue_store.rs deleted file mode 100644 index b88545c5b0..0000000000 --- a/infrastructure/storage/src/keyvalue_store.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! An abstraction layer for persistent key-value storage. The Tari domain layer classes should only make use of -//! these traits and objects and let the underlying implementations handle the details. - -use bincode::{deserialize, serialize, ErrorKind}; -use derive_error::Error; -use serde::{de::DeserializeOwned, Serialize}; -use std::error::Error; - -#[derive(Debug, Error)] -pub enum DatastoreError { - /// An error occurred with the underlying data store implementation - #[error(embedded_msg, no_from, non_std)] - InternalError(String), - /// An error occurred during serialization - #[error(no_from, non_std)] - SerializationErr(String), - /// An error occurred during deserialization - #[error(no_from, non_std)] - DeserializationErr(String), - /// Occurs when trying to perform an action that requires us to be in a live transaction - TransactionNotLiveError, - /// A transaction or query was attempted while no database was open. - DatabaseNotOpen, - /// A database with the requested name does not exist - UnknownDatabase, - /// An error occurred during a put query - #[error(embedded_msg, no_from, non_std)] - PutError(String), - /// An error occurred during a get query - #[error(embedded_msg, no_from, non_std)] - GetError(String), -} - -impl From for DatastoreError { - fn from(e: Box) -> Self { - let msg = format!("Datastore conversion error: {}", e.description()); - DatastoreError::DeserializationErr(msg) - } -} - -/// General CRUD behaviour of KVStore implementations. Datastore is agnostic of the underlying implementation, but -/// does assume that key-value pairs are stored using byte arrays (`&[u8]`). You can use `get_raw` or `put_raw` to -/// read and write binary data directly, or use `#[derive(Serialize, Deserialize, PartialEq, Debug)]` on a trait to -/// generate code for automatically de/serializing your data structures to byte strings using `bincode`. -pub trait DataStore { - /// Connect to the logical database with `name`. If the Datastore does not support multiple logical databases, - /// this function has no effect - fn connect(&mut self, name: &str) -> Result<(), DatastoreError>; - - /// Get the raw value at the given key, or None if the key does not exist - fn get_raw(&self, key: &[u8]) -> Result>, DatastoreError>; - - /// Retrieve a value from the store, deserialize it using bincode and return the value or None if the key does not - /// exist - fn get(&self, key: &str) -> Result, DatastoreError> { - let key = key.as_bytes(); - let result = self.get_raw(key)?; - match result { - None => Ok(None), - Some(val) => Ok(Some(deserialize(&val[..])?)), - } - } - - /// Check whether the given key exists in the database - fn exists(&self, key: &[u8]) -> Result; - - /// Save a value at the given key. Existing values are overwritten - fn put_raw(&mut self, key: &[u8], value: Vec) -> Result<(), DatastoreError>; - - /// Serialize a value using Bincode and then save it value at the given key. Existing values are overwritten - fn put(&mut self, key: &str, value: &T) -> Result<(), DatastoreError> { - let key = key.as_bytes(); - let val = serialize(value)?; - self.put_raw(key, val) - } -} - -/// BatchWrite is implemented on Datastores if it supports batch writes, or transactions, to efficiently write -/// multiple puts to the Datastore. -pub trait BatchWrite { - type Store: DataStore; - type Batcher: BatchWrite + Sized; - - fn new(store: &Self::Store) -> Result; - - /// Save a value at the given key. Existing values are overwritten - fn put_raw(&mut self, key: &[u8], value: Vec) -> Result<(), DatastoreError>; - - /// Serialize a value and then save it value at the given key. Existing values are overwritten - fn put(&mut self, key: &str, value: &T) -> Result<(), DatastoreError> { - let key = key.as_bytes(); - let val = serialize(value)?; - self.put_raw(key, val) - } - - /// Commit all puts in the batch write to the database - fn commit(self) -> Result<(), DatastoreError>; - - /// Discard all puts that have been made in this batch - fn abort(self) -> Result<(), DatastoreError>; -} diff --git a/infrastructure/storage/src/lib.rs b/infrastructure/storage/src/lib.rs index 63bb68a048..5b7bc06d8a 100644 --- a/infrastructure/storage/src/lib.rs +++ b/infrastructure/storage/src/lib.rs @@ -1,2 +1,4 @@ -pub mod keyvalue_store; -pub mod lmdb; +mod key_val_store; +pub mod lmdb_store; + +pub use key_val_store::{lmdb_database::LMDBWrapper, HMapDatabase, KeyValStoreError, KeyValueStore}; diff --git a/infrastructure/storage/src/lmdb.rs b/infrastructure/storage/src/lmdb.rs deleted file mode 100644 index 63c0295b6e..0000000000 --- a/infrastructure/storage/src/lmdb.rs +++ /dev/null @@ -1,407 +0,0 @@ -//! An implementation of [KVStore](trait.KVStore.html) using [LMDB](http://www.lmdb.tech) - -use crate::keyvalue_store::{BatchWrite, DataStore, DatastoreError}; -use lmdb_zero as lmdb; -use lmdb_zero::error::LmdbResultExt; -use std::{collections::HashMap, sync::Arc}; - -/// A builder for [LMDBStore](struct.lmdbstore.html) -/// ## Example -/// -/// Create a new LMDB database of 500MB in the `db` directory with two named databases: "db1" and "db2" -/// -/// ``` -/// # use tari_storage::lmdb::LMDBBuilder; -/// let mut store = LMDBBuilder::new() -/// .set_path("/tmp/") -/// .set_mapsize(500) -/// .add_database("db1") -/// .add_database("db2") -/// .build() -/// .unwrap(); -/// ``` -pub struct LMDBBuilder { - path: String, - db_size_mb: usize, - db_names: Vec, -} - -impl LMDBBuilder { - /// Create a new LMDBStore builder. Set up the database by calling `set_nnnn` and then create the database - /// with `build()`. The default values for the database parameters are: - /// - /// | Parameter | Default | - /// |:----------|---------| - /// | path | ./store/| - /// | size | 64 MB | - /// | named DBs | none | - pub fn new() -> LMDBBuilder { - LMDBBuilder { - path: "./store/".into(), - db_size_mb: 64, - db_names: Vec::new(), - } - } - - /// Set the directory where the LMDB database exists, or must be created. - /// Note: The directory must exist already; it is not created for you. If it does not exist, `build()` will - /// return `DataStoreError::InternalError`. - /// the `path` must have a trailing slash - pub fn set_path(mut self, path: &str) -> LMDBBuilder { - self.path = path.into(); - self - } - - /// Sets the size of the database, in MB. - /// The actual memory will only be allocated when #build() is called - pub fn set_mapsize(mut self, size: usize) -> LMDBBuilder { - self.db_size_mb = size; - self - } - - /// Add an additional named database to the LMDB environment.If `add_database` isn't called at least once, only the - /// `default` database is created. - pub fn add_database(mut self, name: &str) -> LMDBBuilder { - // There will always be a 'default' database - if name != "default" { - self.db_names.push(name.into()); - } - self - } - - /// Create a new LMDBStore instance and open the underlying database environment - pub fn build(self) -> Result { - let env = unsafe { - let mut builder = lmdb::EnvBuilder::new()?; - builder.set_mapsize(self.db_size_mb * 1024 * 1024)?; - builder.set_maxdbs(self.db_names.len() as u32 + 1)?; - builder.open(&self.path, lmdb::open::Flags::empty(), 0o600)? - }; - let env = Arc::new(env); - let mut databases: HashMap>> = HashMap::new(); - let opt = lmdb::DatabaseOptions::new(lmdb::db::CREATE); - // Add the default db - let default = Arc::new(lmdb::Database::open(env.clone(), None, &opt)?); - let curr_db = default.clone(); - databases.insert("default".to_string(), default); - for name in &self.db_names { - let db = Arc::new(lmdb::Database::open(env.clone(), Some(name), &opt)?); - databases.insert(name.to_string(), db); - } - Ok(LMDBStore { - env, - databases, - curr_db, - }) - } -} - -/// A Struct for holding state for the LMDB implementation of DataStore and BatchWrite. To create an instance of -/// LMDBStore, use [LMDBBuilder](struct.lmdbbuilder.html). -pub struct LMDBStore { - pub(crate) env: Arc, - pub(crate) databases: HashMap>>, - pub(crate) curr_db: Arc>, -} - -impl DataStore for LMDBStore { - fn connect(&mut self, name: &str) -> Result<(), DatastoreError> { - match self.databases.get(name) { - Some(db) => { - self.curr_db = db.clone(); - Ok(()) - }, - None => Err(DatastoreError::UnknownDatabase), - } - } - - fn get_raw(&self, key: &[u8]) -> Result>, DatastoreError> { - let txn = lmdb::ReadTransaction::new(self.env.clone())?; - let accessor = txn.access(); - match accessor.get::<[u8], [u8]>(&self.curr_db, key).to_opt() { - Ok(None) => Ok(None), - Ok(Some(v)) => Ok(Some(v.to_vec())), - Err(e) => Err(DatastoreError::GetError(format!("LMDB get error: {}", e.to_string()))), - } - } - - fn exists(&self, key: &[u8]) -> Result { - let txn = lmdb::ReadTransaction::new(self.env.clone())?; - let accessor = txn.access(); - let res: lmdb::error::Result<&lmdb::Ignore> = accessor.get(&self.curr_db, key); - Ok(res.to_opt()?.is_some()) - } - - fn put_raw(&mut self, key: &[u8], value: Vec) -> Result<(), DatastoreError> { - let tx = lmdb::WriteTransaction::new(self.env.clone())?; - { - let mut accessor = tx.access(); - accessor.put(&self.curr_db, key, &value, lmdb::put::Flags::empty())?; - } - tx.commit().map_err(|e| e.into()) - } -} - -struct LMDBBatch<'a> { - db: Arc>, - tx: lmdb::WriteTransaction<'a>, -} - -impl<'a> BatchWrite for LMDBBatch<'a> { - type Batcher = LMDBBatch<'a>; - type Store = LMDBStore; - - fn new(store: &LMDBStore) -> Result, DatastoreError> { - Ok(LMDBBatch { - db: store.curr_db.clone(), - tx: lmdb::WriteTransaction::new(store.env.clone())?, - }) - } - - fn put_raw(&mut self, key: &[u8], value: Vec) -> Result<(), DatastoreError> { - { - let mut accessor = self.tx.access(); - accessor.put(&self.db, key, &value, lmdb::put::Flags::empty())?; - } - Ok(()) - } - - fn commit(self) -> Result<(), DatastoreError> { - self.tx.commit().map_err(|e| e.into()) - } - - fn abort(self) -> Result<(), DatastoreError> { - Ok(()) - } -} - -impl From for DatastoreError { - fn from(err: lmdb::error::Error) -> Self { - let err_msg = format!("LMDB Error: {}", err.to_string()); - DatastoreError::InternalError(err_msg) - } -} - -#[cfg(test)] -mod test { - use super::{LMDBBuilder, LMDBStore}; - use crate::{ - keyvalue_store::{BatchWrite, DataStore, DatastoreError}, - lmdb::LMDBBatch, - }; - use bincode::{deserialize, serialize}; - use rand::{OsRng, RngCore}; - use serde_derive::{Deserialize, Serialize}; - use std::{fs, str}; - - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct Entity { - x: f32, - y: f32, - } - - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct World(Vec); - - fn to_bytes(i: u32) -> Vec { - i.to_le_bytes().to_vec() - } - - fn from_bytes(v: &[u8]) -> u32 { - u32::from_le_bytes([v[0], v[1], v[2], v[3]]) - } - - fn make_vector(len: usize) -> Vec { - let mut vec = vec![0; len]; - let mut rng = OsRng::new().unwrap(); - rng.fill_bytes(&mut vec); - vec - } - - #[test] - fn path_must_exist() { - let builder = LMDBBuilder::new(); - match builder.set_mapsize(1).set_path("./tests/not_here/").build() { - Err(DatastoreError::InternalError(s)) => assert_eq!(s, "LMDB Error: No such file or directory"), - _ => panic!(), - } - } - - #[test] - fn batch_writes() { - fs::create_dir("./tests/test_tx").unwrap(); - let builder = LMDBBuilder::new(); - let store = builder.set_mapsize(5).set_path("./tests/test_tx/").build().unwrap(); - let mut batch = LMDBBatch::new(&store).unwrap(); - batch.put_raw(b"a", b"apple".to_vec()).unwrap(); - batch.put_raw(b"b", b"banana".to_vec()).unwrap(); - batch.put_raw(b"c", b"carrot".to_vec()).unwrap(); - batch.commit().unwrap(); - let banana = store.get_raw(b"b").unwrap().unwrap(); - assert_eq!(&banana, b"banana"); - assert!(fs::remove_dir_all("./tests/test_tx").is_ok()); - } - - #[test] - fn writes_to_default_db() { - fs::create_dir("./tests/test_default").unwrap(); - let mut store = LMDBBuilder::new().set_path("./tests/test_default/").build().unwrap(); - store.connect("default").unwrap(); - // Write some values - store.put_raw(b"England", b"rose".to_vec()).unwrap(); - store.put_raw(b"SouthAfrica", b"protea".to_vec()).unwrap(); - store.put_raw(b"Scotland", b"thistle".to_vec()).unwrap(); - // And read them back - let val = store.get_raw(b"Scotland").unwrap().unwrap(); - assert_eq!(str::from_utf8(&val).unwrap(), "thistle"); - let val = store.get_raw(b"England").unwrap().unwrap(); - assert_eq!(str::from_utf8(&val).unwrap(), "rose"); - // Clean up - assert!(fs::remove_dir_all("./tests/test_default").is_ok()); - } - - #[test] - fn aborts_write() { - fs::create_dir("./tests/test_abort").unwrap(); - let mut store = LMDBBuilder::new().set_path("./tests/test_abort/").build().unwrap(); - store.connect("default").unwrap(); - // Write some values - let mut batch = LMDBBatch::new(&store).unwrap(); - batch.put_raw(b"England", b"rose".to_vec()).unwrap(); - batch.put_raw(b"SouthAfrica", b"protea".to_vec()).unwrap(); - batch.put_raw(b"Scotland", b"thistle".to_vec()).unwrap(); - batch.abort().unwrap(); - // And check nothing was written - let check = |k: &[u8], store: &LMDBStore| { - let val = store.get_raw(k).unwrap(); - assert!(val.is_none()); - }; - check(b"Scotland", &store); - check(b"SouthAfrica", &store); - check(b"England", &store); - // Clean up - assert!(fs::remove_dir_all("./tests/test_abort").is_ok()); - } - - /// Set the DB size to 1MB and write more than a MB to it - #[test] - fn overflow_db() { - fs::create_dir("./tests/test_overflow").unwrap(); - let builder = LMDBBuilder::new(); - let mut store = builder - .set_path("./tests/test_overflow/") - // Set the max DB size to 1MB - .set_mapsize(1) - .build() - .unwrap(); - assert!(store.connect("default").is_ok()); - // Write 500,000 bytes - store.put_raw(b"key", make_vector(500_000)).unwrap(); - // Try write another 600,000 bytes and watch it fail - match store.put_raw(b"key2", make_vector(600_000)).unwrap_err() { - DatastoreError::InternalError(s) => { - assert_eq!(s, "LMDB Error: MDB_MAP_FULL: Environment mapsize limit reached"); - }, - err => { - println!("{:?}", err); - assert!(fs::remove_dir_all("./tests/test_overflow").is_ok()); - panic!() - }, - } - assert!(fs::remove_dir_all("./tests/test_overflow").is_ok()); - } - - #[test] - fn read_and_write_10k_values() { - fs::create_dir("./tests/test_10k").unwrap(); - let builder = LMDBBuilder::new(); - let mut store = builder - .set_path("./tests/test_10k/") - .add_database("test") - .build() - .unwrap(); - assert!(store.connect("test").is_ok()); - // Write 100,000 integers to the DB with val = 2*key - let mut batch = LMDBBatch::new(&store).unwrap(); - for i in 0u32..10_000 { - batch.put_raw(&to_bytes(i), to_bytes(2 * i)).unwrap(); - } - batch.commit().unwrap(); - // And read them back - for i in 0u32..10_000 { - let val = store.get_raw(&to_bytes(i)).unwrap().unwrap(); - assert_eq!(from_bytes(&val), i * 2); - } - assert!(fs::remove_dir_all("./tests/test_10k").is_ok()); - } - - #[test] - fn test_exist_on_different_databases() { - fs::create_dir("./tests/test_exist").unwrap(); - let mut store = LMDBBuilder::new() - .set_path("./tests/test_exist/") - .add_database("db1") - .add_database("db2") - .build() - .unwrap(); - store.connect("db1").unwrap(); - // Write some values - store.put_raw(b"db1-a", b"val1".to_vec()).unwrap(); - store.put_raw(b"db1-b", b"val2".to_vec()).unwrap(); - store.put_raw(b"common", b"db1".to_vec()).unwrap(); - // Change databases - store.connect("db2").unwrap(); - store.put_raw(b"db2-a", b"val3".to_vec()).unwrap(); - store.put_raw(b"db2-b", b"val4".to_vec()).unwrap(); - store.put_raw(b"common", b"db2".to_vec()).unwrap(); - // Check existence and non-existence of keys - assert!(!store.exists(b"db1-a").unwrap()); - assert!(!store.exists(b"db1-b").unwrap()); - assert!(store.exists(b"db2-a").unwrap()); - assert!(store.exists(b"db2-b").unwrap()); - assert!(store.exists(b"common").unwrap()); - // Change back to db1 - store.connect("db1").unwrap(); - // Check existence and non-existence of keys - assert!(store.exists(b"db1-a").unwrap()); - assert!(store.exists(b"db1-b").unwrap()); - assert!(!store.exists(b"db2-a").unwrap()); - assert!(!store.exists(b"db2-b").unwrap()); - assert!(store.exists(b"common").unwrap()); - // Finally check the value of 'common' - let val = store.get_raw(b"common").unwrap().unwrap(); - assert_eq!(&val, b"db1"); - // Clean up - assert!(fs::remove_dir_all("./tests/test_exist").is_ok()); - } - - #[test] - fn write_structs() { - fs::create_dir("./tests/test_struct").unwrap(); - let builder = LMDBBuilder::new(); - let mut store = builder.set_path("./tests/test_struct/").build().unwrap(); - let world = World(vec![Entity { x: 0.0, y: 4.0 }, Entity { x: 10.0, y: 20.5 }]); - let encoded: Vec = serialize(&world).unwrap(); - // 8 bytes for the length of the vector, 4 bytes per float. - assert_eq!(encoded.len(), 8 + 4 * 4); - store.put_raw(b"world", encoded).unwrap(); - // Write using `put` - let world_2 = World(vec![Entity { x: 100.0, y: -123.45 }, Entity { x: 42.0, y: -42.0 }]); - store.put("brave new world", &world_2).unwrap(); - // Get world back using get_raw - let val = store.get_raw(b"world").unwrap().unwrap(); - let decoded: World = deserialize(&val[..]).unwrap(); - assert_eq!(world, decoded); - // Get world2 back using get_raw - let val = store.get_raw(b"brave new world").unwrap().unwrap(); - let decoded: World = deserialize(&val[..]).unwrap(); - assert_eq!(world_2, decoded); - // Get world_2 back using get - let val = store.get("brave new world").unwrap().unwrap(); - assert_eq!(world_2, val); - // And check that get returns None - let val = store.get::("not here").unwrap(); - assert!(val.is_none()); - assert!(fs::remove_dir_all("./tests/test_struct").is_ok()); - } -} diff --git a/infrastructure/storage/src/lmdb_store/error.rs b/infrastructure/storage/src/lmdb_store/error.rs new file mode 100644 index 0000000000..21b2ad3316 --- /dev/null +++ b/infrastructure/storage/src/lmdb_store/error.rs @@ -0,0 +1,54 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; + +#[derive(Debug, Error)] +pub enum LMDBError { + /// Cannot create LMDB. The path does not exist + InvalidPath, + /// An error occurred with the underlying data store implementation + #[error(embedded_msg, no_from, non_std)] + InternalError(String), + /// An error occurred during serialization + #[error(no_from, non_std)] + SerializationErr(String), + /// An error occurred during deserialization + #[error(no_from, non_std)] + DeserializationErr(String), + /// Occurs when trying to perform an action that requires us to be in a live transaction + TransactionNotLiveError, + /// A transaction or query was attempted while no database was open. + DatabaseNotOpen, + /// A database with the requested name does not exist + UnknownDatabase, + /// An error occurred during a put query + #[error(embedded_msg, no_from, non_std)] + PutError(String), + /// An error occurred during a get query + #[error(embedded_msg, no_from, non_std)] + GetError(String), + #[error(embedded_msg, no_from, non_std)] + CommitError(String), + /// An LMDB error occurred + DatabaseError(lmdb_zero::error::Error), +} diff --git a/infrastructure/storage/src/lmdb_store/mod.rs b/infrastructure/storage/src/lmdb_store/mod.rs new file mode 100644 index 0000000000..bddd296c41 --- /dev/null +++ b/infrastructure/storage/src/lmdb_store/mod.rs @@ -0,0 +1,31 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod store; + +pub use error::LMDBError; +pub use lmdb_zero::{ + db, + traits::{AsLmdbBytes, FromLmdbBytes}, +}; +pub use store::{LMDBBuilder, LMDBDatabase, LMDBStore}; diff --git a/infrastructure/storage/src/lmdb_store/store.rs b/infrastructure/storage/src/lmdb_store/store.rs new file mode 100644 index 0000000000..3f86dcc58d --- /dev/null +++ b/infrastructure/storage/src/lmdb_store/store.rs @@ -0,0 +1,595 @@ +//! An ergonomic, multithreaded API for an LMDB datastore + +use crate::{key_val_store::error::KeyValStoreError, lmdb_store::error::LMDBError}; +use lmdb_zero::{ + db, + error::{self, LmdbResultExt}, + open, + put, + traits::AsLmdbBytes, + ConstAccessor, + Cursor, + CursorIter, + Database, + DatabaseOptions, + EnvBuilder, + Environment, + Ignore, + MaybeOwned, + ReadTransaction, + Stat, + WriteAccessor, + WriteTransaction, +}; +use log::*; +use serde::{de::DeserializeOwned, Serialize}; +use std::{cmp::max, collections::HashMap, path::Path, sync::Arc}; + +const LOG_TARGET: &str = "lmdb"; + +/// An atomic pointer to an LMDB database instance +type DatabaseRef = Arc>; + +/// A builder for [LMDBStore](struct.lmdbstore.html) +/// ## Example +/// +/// Create a new LMDB database of 500MB in the `db` directory with two named databases: "db1" and "db2" +/// +/// ``` +/// # use tari_storage::lmdb_store::LMDBBuilder; +/// # use lmdb_zero::db; +/// let mut store = LMDBBuilder::new() +/// .set_path("/tmp/") +/// .set_environment_size(500) +/// .set_max_number_of_databases(10) +/// .add_database("db1", db::CREATE) +/// .add_database("db2", db::CREATE) +/// .build() +/// .unwrap(); +/// ``` +#[derive(Default)] +pub struct LMDBBuilder { + path: String, + db_size_mb: usize, + max_dbs: usize, + db_names: HashMap, +} + +impl LMDBBuilder { + /// Create a new LMDBStore builder. Set up the database by calling `set_nnnn` and then create the database + /// with `build()`. The default values for the database parameters are: + /// + /// | Parameter | Default | + /// |:----------|---------| + /// | path | ./store/| + /// | size | 64 MB | + /// | named DBs | none | + pub fn new() -> LMDBBuilder { + LMDBBuilder { + path: "./store/".into(), + db_size_mb: 64, + db_names: HashMap::new(), + max_dbs: 8, + } + } + + /// Set the directory where the LMDB database exists, or must be created. + /// Note: The directory must exist already; it is not created for you. If it does not exist, `build()` will + /// return `LMDBError::InvalidPath`. + pub fn set_path(mut self, path: &str) -> LMDBBuilder { + self.path = path.into(); + if !self.path.ends_with('/') { + self.path += "/"; + } + self + } + + /// Sets the size of the environment, in MB. + /// The actual memory will only be allocated when #build() is called + pub fn set_environment_size(mut self, size: usize) -> LMDBBuilder { + self.db_size_mb = size; + self + } + + /// Sets the maximum number of databases (tables) in the environment. If this value is less than the number of + /// DBs that will be created when the environment is built, this value will be ignored. + pub fn set_max_number_of_databases(mut self, size: usize) -> LMDBBuilder { + self.max_dbs = size; + self + } + + /// Add an additional named database to the LMDB environment.If `add_database` isn't called at least once, only the + /// `default` database is created. + pub fn add_database(mut self, name: &str, flags: db::Flags) -> LMDBBuilder { + // There will always be a 'default' database + let _ = self.db_names.insert(name.into(), flags); + self + } + + /// Create a new LMDBStore instance and open the underlying database environment + pub fn build(mut self) -> Result { + let max_dbs = max(self.db_names.len(), self.max_dbs) as u32; + let path = Path::new(&self.path); + if !path.exists() { + return Err(LMDBError::InvalidPath); + } + let path = if let Some(path) = path.to_str() { + path.to_string() + } else { + return Err(LMDBError::InvalidPath); + }; + let env = unsafe { + let mut builder = EnvBuilder::new()?; + builder.set_mapsize(self.db_size_mb * 1024 * 1024)?; + builder.set_maxdbs(max_dbs)?; + builder.open(&self.path, open::Flags::empty(), 0o600)? + }; + let env = Arc::new(env); + info!( + target: LOG_TARGET, + "({}) LMDB environment created with a capacity of {} MB.", path, self.db_size_mb + ); + let mut databases: HashMap = HashMap::new(); + if self.db_names.is_empty() { + self = self.add_database("default", db::CREATE); + } + for (name, flags) in self.db_names.iter() { + let db = Database::open(env.clone(), Some(name), &DatabaseOptions::new(*flags))?; + let db = LMDBDatabase { + name: name.to_string(), + env: env.clone(), + db: Arc::new(db), + }; + databases.insert(name.to_string(), db); + info!(target: LOG_TARGET, "({}) LMDB database '{}' is ready", path, name); + } + Ok(LMDBStore { path, env, databases }) + } +} + +/// A Struct for holding state for an LM Database. LMDB is memory mapped, so you can treat the DB as an (essentially) +/// infinitely large memory-backed hashmap. A single environment is stored in one file. The individual databases +/// are key-value tables stored within the file. +/// +/// LMDB databases are thread-safe. +/// +/// To create an instance of LMDBStore, use [LMDBBuilder](struct.lmdbbuilder.html). +/// +/// ## Memory efficiency +/// +/// LMDB really only understands raw byte arrays. Complex structures need to be referenced as (what looks like) a +/// single contiguous blob of memory. This presents some trade offs we need to make when `insert`ing and `get`ting +/// data to/from LMDB. +/// +/// ### Writing +/// +/// For simple types, like `PublickKey([u8; 32])`, it's most efficient to pass a pointer to the memory position; and +/// LMDB will do (at most) a single copy into its memory structures. the lmdb-zero crate assumes this by only +/// requiring the `AsLmdbBytes` trait when `insert`ing data. i.e. `insert` does does take ownership of the key or +/// value; it just wants to be able to read the `[u8]`. +/// +/// This poses something of a problem for complex structures. Structs typically don't have a contiguous block of +/// memory backing the instance, and so you either need to impose one (which isn't a great idea-- now you have to write +/// some sort of memory management software), or you eat the cost of doing an intermediate copy into a buffer every +/// time you need to commit a structure to LMDB. +/// +/// However, this cost is mitigated if there's any kind of processing that needs to be done in converting `T` to +/// `[u8]` (e.g. if an IP address is stored as a string for some reason, you might want to represent it as `[u8; 4]`) +/// , which probably happens more often than we think, and offers maximum flexibility. +/// +/// Furthermore, the "simple" types are typically quite small, so an additional copy is not usually incurring much +/// overhead. +/// +/// So this library makes the trade-off of carrying out two copies per write whilst gaining a significant amount of +/// flexibility in the process. +/// +/// ### Reading +/// +/// When LMDB returns data from a `get` request, it returns a `&[u8]` - you cannot take ownership of this data. +/// Therefore we necessarily need to copy data anyway in order to pull data into the final Struct instance. +/// So the `From<&[u8]> for T` trait implementation will work for reading, and this works fine for both simple and +/// complex data structures. +/// +/// `FromLmdbBytes` is not quite what we want because the trait function returns a reference to an object, rather +/// than the object itself. +/// +/// An additional consideration is: how was this data serialised? If the writing was a straight memory dump, we +/// don't always have enough information to reconstruct our data object (how long was a string? How many elements +/// were in the array? Was it big- or little-endian ordering of integers?). +/// +/// If we have to store this metadata when reading in byte strings, it means it had to be stored too. This is a +/// further roadblock to the "zero-copy" ideal for writing. And since we're now basically serialising and +/// de-serialising, we may as well use a well-known, highly efficient binary format to do so. +/// +/// ## Serialisation +/// +/// The ideal serialiasation format is the one that does the least "bit-twiddling" between memory and the byte array; +/// as well as being as compact as possible. +/// +/// Candidates include: Bincode, MsgPack, and Protobuf / Cap'nProto. Without spending ages on a comparison, I just +/// took the benchmark results from [this project](https://github.com/erickt/rust-serialization-benchmarks): +/// +/// ```text +/// test clone ... bench: 1,179 ns/iter (+/- 115) = 444 MB/s +/// +/// test capnp_deserialize ... bench: 277 ns/iter (+/- 27) = 1617 MB/s ** +/// test flatbuffers_deserialize ... bench: 0 ns/iter (+/- 0) = 472000 MB/s *** +/// test rust_bincode_deserialize ... bench: 1,533 ns/iter (+/- 228) = 260 MB/s +/// test rmp_serde_deserialize ... bench: 1,859 ns/iter (+/- 186) = 154 MB/s +/// test rust_protobuf_deserialize ... bench: 558 ns/iter (+/- 29) = 512 MB/s * +/// test serde_json_deserialize ... bench: 2,244 ns/iter (+/- 249) = 269 MB/s +/// +/// test capnp_serialize ... bench: 28 ns/iter (+/- 5) = 16000 MB/s ** +/// test flatbuffers_serialize ... bench: 0 ns/iter (+/- 0) = 472000 MB/s *** +/// test rmp_serde_serialize ... bench: 278 ns/iter (+/- 27) = 1032 MB/s +/// test rust_bincode_serialize ... bench: 190 ns/iter (+/- 43) = 2105 MB/s * +/// test rust_protobuf_serialize ... bench: 468 ns/iter (+/- 18) = 611 MB/s +/// test serde_json_serialize ... bench: 1,012 ns/iter (+/- 55) = 597 MB/s +/// ``` +/// +/// Based on these benchmarks, Flatbuffers and Cap'nProto are far and away the quickest. However, looking at the +/// benchmarks more closely, we see that these aren't strictly Orange to Orange comparisons. The flatbuffers and +/// capnproto tests don't actually serialise to and from the general Rust struct (an HTTP request type template), but +/// from specially generated structs based on the schema. +/// +/// Strictly speaking, if we're going to serialise arbitrary key-value types, these benchmarks should include the +/// time it takes to populate a flatbuffer / capnproto structure. +/// +/// A quick modification of the benchmarks to take this int account this reveals: +/// +/// ```text +/// test rust_bincode_deserialize ... bench: 1,505 ns/iter (+/- 361) = 265 MB/s * +/// test capnp_deserialize ... bench: 282 ns/iter (+/- 37) = 1588 MB/s *** +/// test rmp_serde_deserialize ... bench: 1,800 ns/iter (+/- 144) = 159 MB/s * +/// +/// test capnp_serialize ... bench: 941 ns/iter (+/- 40) = 476 MB/s * +/// test rmp_serde_serialize ... bench: 269 ns/iter (+/- 19) = 1066 MB/s ** +/// test rust_bincode_serialize ... bench: 191 ns/iter (+/- 41) = 1114 MB/s *** +/// ``` +/// +/// Now bincode emerges as a reasonable contender. Another positive to bincode is that one doesn't have to update and +/// maintain a schema for the data types begin serialized, nor is a separate compilation step required. +/// +/// So after all this, we'll use bincode for the time being to handle serialisation to- and from- LMDB +pub struct LMDBStore { + path: String, + pub(crate) env: Arc, + pub(crate) databases: HashMap, +} + +/// Close all databases and close the environment. You cannot be guaranteed that the dbs will be closed after calling +/// this function because there still may be threads accessing / writing to a database that will block this call. +/// However, in that case `shutdown` returns an error. +impl LMDBStore { + pub fn flush(&self) -> Result<(), lmdb_zero::error::Error> { + debug!(target: LOG_TARGET, "Forcing flush of buffers to disk"); + self.env.sync(true)?; + debug!(target: LOG_TARGET, "Buffers have been flushed"); + Ok(()) + } + + pub fn log_info(&self) { + match self.env.info() { + Err(e) => warn!( + target: LOG_TARGET, + "Could not retrieve LMDB information for {}. {}", + self.path, + e.to_string() + ), + Ok(info) => { + let size_mb = info.mapsize / 1024 / 1024; + info!( + target: LOG_TARGET, + "LMDB Environment information ({}). Map Size={} MB. Last page no={}. Last tx id={}", + self.path, + size_mb, + info.last_pgno, + info.last_txnid + ) + }, + } + match self.env.stat() { + Err(e) => warn!( + target: LOG_TARGET, + "Could not retrieve LMDB statistics for {}. {}", + self.path, + e.to_string() + ), + Ok(stats) => { + let page_size = stats.psize / 1024; + info!( + target: LOG_TARGET, + "LMDB Environment statistics ({}). Page size={}kB. Tree depth={}. Branch pages={}. Leaf Pages={}, \ + Overflow pages={}, Entries={}", + self.path, + page_size, + stats.depth, + stats.branch_pages, + stats.leaf_pages, + stats.overflow_pages, + stats.entries + ); + }, + } + } + + /// Returns a handle to the database given in `db_name`, if it exists, otherwise return None. + pub fn get_handle(&self, db_name: &str) -> Option { + match self.databases.get(db_name) { + Some(db) => Some(db.clone()), + None => None, + } + } +} + +#[derive(Clone)] +pub struct LMDBDatabase { + name: String, + env: Arc, + db: DatabaseRef, +} + +impl LMDBDatabase { + /// Inserts a record into the database. This is an atomic operation. Internally, `insert` creates a new + /// write transaction, writes the value, and then commits the transaction. + pub fn insert(&self, key: &K, value: &V) -> Result<(), LMDBError> + where + K: AsLmdbBytes + ?Sized, + V: Serialize, + { + let env = &(*self.db.env()); + let tx = WriteTransaction::new(env)?; + { + let mut accessor = tx.access(); + let buf = LMDBWriteTransaction::convert_value(value, 512)?; + accessor.put(&*self.db, key, &buf, put::Flags::empty())?; + } + tx.commit().map_err(LMDBError::from) + } + + /// Get a value from the database. This is an atomic operation. A read transaction is created, the value + /// extracted, copied and converted to V before closing the transaction. A copy is unavoidable because the + /// extracted byte string is released when the transaction is closed. If you are doing many `gets`, it is more + /// efficient to use `with_read_transaction` + pub fn get(&self, key: &K) -> Result, LMDBError> + where + K: AsLmdbBytes + ?Sized, + for<'t> V: DeserializeOwned, // read this as, for *any* lifetime, t, we can convert a [u8] to V + { + let env = &(*self.db.env()); + let txn = ReadTransaction::new(env)?; + let accessor = txn.access(); + let val = accessor.get(&self.db, key).to_opt(); + LMDBReadTransaction::convert_value(val) + } + + /// Return statistics about the database, See [Stat](lmdb_zero/struct.Stat.html) for more details. + pub fn get_stats(&self) -> Result { + let env = &(*self.db.env()); + ReadTransaction::new(env) + .and_then(|txn| txn.db_stat(&self.db)) + .map_err(LMDBError::DatabaseError) + } + + /// Log some pretty printed stats.See [Stat](lmdb_zero/struct.Stat.html) for more details. + pub fn log_info(&self) { + match self.get_stats() { + Err(e) => warn!( + target: LOG_TARGET, + "Could not retrieve LMDB statistics for {}. {}", + self.name, + e.to_string() + ), + Ok(stats) => { + let page_size = stats.psize / 1024; + info!( + target: LOG_TARGET, + "LMDB Database statistics ({}). Page size={}kB. Tree depth={}. Branch pages={}. Leaf Pages={}, \ + Overflow pages={}, Entries={}", + self.name, + page_size, + stats.depth, + stats.branch_pages, + stats.leaf_pages, + stats.overflow_pages, + stats.entries + ); + }, + } + } + + /// Returns if the database is empty. + pub fn is_empty(&self) -> Result { + self.get_stats().and_then(|s| Ok(s.entries > 0)) + } + + /// Returns the total number of entries in this database. + pub fn len(&self) -> Result { + self.get_stats().and_then(|s| Ok(s.entries)) + } + + /// Execute function `f` for each value in the database. + /// + /// The underlying LMDB library does not permit database cursors to be returned from functions to preserve Rust + /// memory guarantees, so this is the closest thing to an iterator that you're going to get :/ + /// + /// `f` is a closure of form `|pair: Result<(K,V), LMDBError>| -> ()`. You will usually need to include type + /// inference to let Rust know which type to deserialise to: + /// ```nocompile + /// let res = db.for_each::(|pair| { + /// let (key, user) = pair.unwrap(); + /// //.. do stuff with key and user.. + /// }); + pub fn for_each(&self, mut f: F) -> Result<(), LMDBError> + where + K: DeserializeOwned, + V: DeserializeOwned, + F: FnMut(Result<(K, V), KeyValStoreError>), + { + let env = self.env.clone(); + let db = self.db.clone(); + let txn = ReadTransaction::new(env).map_err(LMDBError::DatabaseError)?; + + let access = txn.access(); + let cursor = txn.cursor(db).map_err(LMDBError::DatabaseError)?; + + let head = |c: &mut Cursor, a: &ConstAccessor| { + let (key_bytes, val_bytes) = c.first(a)?; + ReadOnlyIterator::deserialize::(key_bytes, val_bytes) + }; + + let cursor = MaybeOwned::Owned(cursor); + let iter = CursorIter::new(cursor, &access, head, ReadOnlyIterator::next).map_err(LMDBError::DatabaseError)?; + + for p in iter { + f(p.map_err(|e| KeyValStoreError::DatabaseError(e.to_string()))); + } + + Ok(()) + } + + /// Checks whether a key exists in this database + pub fn contains_key(&self, key: &K) -> Result + where K: AsLmdbBytes + ?Sized { + let txn = ReadTransaction::new(&(*self.db.env()))?; + let accessor = txn.access(); + let res: error::Result<&Ignore> = accessor.get(&self.db, key); + let res = res.to_opt()?.is_some(); + Ok(res) + } + + /// Delete a record associated with `key` from the database. If the key is not found, + pub fn remove(&self, key: &K) -> Result<(), LMDBError> + where K: AsLmdbBytes + ?Sized { + let tx = WriteTransaction::new(&(*self.db.env()))?; + { + let mut accessor = tx.access(); + accessor.del_key(&self.db, key)?; + } + tx.commit().map_err(Into::into) + } + + /// Create a read-only transaction on the current database and execute the instructions given in the closure. The + /// transaction is automatically committed when the closure goes out of scope. You may provide the results of the + /// transaction to the calling scope by populating a `Vec` with the results of `txn.get(k)`. Otherwise, if the + /// results are not needed, or you did not call `get`, just return `Ok(None)`. + pub fn with_read_transaction(&self, f: F) -> Result>, LMDBError> + where + V: serde::de::DeserializeOwned, + F: FnOnce(LMDBReadTransaction) -> Result>, LMDBError>, + { + let txn = ReadTransaction::new(self.env.clone())?; + let access = txn.access(); + let wrapper = LMDBReadTransaction { db: &self.db, access }; + f(wrapper) + } + + /// Create a transaction with write access on the current table. + pub fn with_write_transaction(&self, f: F) -> Result<(), LMDBError> + where F: FnOnce(LMDBWriteTransaction) -> Result<(), LMDBError> { + let txn = WriteTransaction::new(self.env.clone())?; + let access = txn.access(); + let wrapper = LMDBWriteTransaction { db: &self.db, access }; + f(wrapper)?; + txn.commit().map_err(|e| LMDBError::CommitError(e.to_string())) + } +} + +/// Helper functions for the `for_each` method +struct ReadOnlyIterator {} +impl ReadOnlyIterator { + fn deserialize(key_bytes: &[u8], val_bytes: &[u8]) -> Result<(K, V), error::Error> + where + for<'t> K: serde::de::DeserializeOwned, + for<'t> V: serde::de::DeserializeOwned, + { + let key = bincode::deserialize(key_bytes).map_err(|e| error::Error::ValRejected(e.to_string()))?; + let val = bincode::deserialize(val_bytes).map_err(|e| error::Error::ValRejected(e.to_string()))?; + Ok((key, val)) + } + + fn next<'r, K, V>(c: &mut Cursor, access: &'r ConstAccessor) -> Result<(K, V), error::Error> + where + K: serde::de::DeserializeOwned, + V: serde::de::DeserializeOwned, + { + let (key_bytes, val_bytes) = c.next(access)?; + ReadOnlyIterator::deserialize(key_bytes, val_bytes) + } +} + +pub struct LMDBReadTransaction<'txn, 'db: 'txn> { + db: &'db Database<'db>, + access: ConstAccessor<'txn>, +} + +impl<'txn, 'db: 'txn> LMDBReadTransaction<'txn, 'db> { + /// Get and deserialise a value from the database. + pub fn get(&self, key: &K) -> Result, LMDBError> + where + K: AsLmdbBytes + ?Sized, + for<'t> V: serde::de::DeserializeOwned, // read this as, for *any* lifetime, t, we can convert a [u8] to V + { + let val = self.access.get(&self.db, key).to_opt(); + LMDBReadTransaction::convert_value(val) + } + + /// Checks whether a key exists in this database + pub fn exists(&self, key: &K) -> Result + where K: AsLmdbBytes + ?Sized { + let res: error::Result<&Ignore> = self.access.get(&self.db, key); + let res = res.to_opt()?.is_some(); + Ok(res) + } + + fn convert_value(val: Result, error::Error>) -> Result, LMDBError> + where for<'t> V: serde::de::DeserializeOwned /* read this as, for *any* lifetime, t, we can convert a [u8] to V */ + { + match val { + Ok(None) => Ok(None), + Err(e) => Err(LMDBError::GetError(format!("LMDB get error: {}", e.to_string()))), + Ok(Some(v)) => match bincode::deserialize(v) { + // The reference to v is about to be dropped, so we must copy the data now + Ok(val) => Ok(Some(val)), + Err(e) => Err(LMDBError::GetError(format!("LMDB get error: {}", e))), + }, + } + } +} + +pub struct LMDBWriteTransaction<'txn, 'db: 'txn> { + db: &'db Database<'db>, + access: WriteAccessor<'txn>, +} + +impl<'txn, 'db: 'txn> LMDBWriteTransaction<'txn, 'db> { + pub fn insert(&mut self, key: &K, value: &V) -> Result<(), LMDBError> + where + K: AsLmdbBytes + ?Sized, + V: serde::Serialize, + { + let buf = LMDBWriteTransaction::convert_value(value, 512)?; + self.access.put(&self.db, key, &buf, put::Flags::empty())?; + Ok(()) + } + + /// Checks whether a key exists in this database + pub fn exists(&self, key: &K) -> Result + where K: AsLmdbBytes + ?Sized { + let res: error::Result<&Ignore> = self.access.get(&self.db, key); + let res = res.to_opt()?.is_some(); + Ok(res) + } + + pub fn delete(&mut self, key: &K) -> Result<(), LMDBError> + where K: AsLmdbBytes + ?Sized { + self.access.del_key(&self.db, key).map_err(LMDBError::DatabaseError) + } + + fn convert_value(value: &V, size_estimate: usize) -> Result, LMDBError> + where V: serde::Serialize { + let mut buf = Vec::with_capacity(size_estimate); + bincode::serialize_into(&mut buf, value).map_err(|e| LMDBError::SerializationErr(e.to_string()))?; + Ok(buf) + } +} diff --git a/infrastructure/storage/tests/data/.gitkeep b/infrastructure/storage/tests/data/.gitkeep new file mode 100644 index 0000000000..79e790c1e5 --- /dev/null +++ b/infrastructure/storage/tests/data/.gitkeep @@ -0,0 +1 @@ +Temp folder for LMDB database files \ No newline at end of file diff --git a/infrastructure/storage/tests/lmdb.rs b/infrastructure/storage/tests/lmdb.rs new file mode 100644 index 0000000000..5dcdb05d11 --- /dev/null +++ b/infrastructure/storage/tests/lmdb.rs @@ -0,0 +1,248 @@ +// Copyright 2019. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use serde::{Deserialize, Serialize}; +use std::{net::Ipv4Addr, path::PathBuf, str::FromStr, sync::Arc, thread}; +use tari_storage::lmdb_store::{db, LMDBBuilder, LMDBDatabase, LMDBError, LMDBStore}; +use tari_utilities::ExtendBytes; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +struct User { + id: u64, + first: String, + last: String, + email: String, + male: bool, + ip: Ipv4Addr, +} + +impl User { + fn new(csv: &str) -> Result { + let vals: Vec<&str> = csv.split(",").collect(); + if vals.len() != 6 { + return Err("Incomplete Record".into()); + } + let id = u64::from_str(vals[0]).map_err(|e| e.to_string())?; + let first = vals[1].to_string(); + let last = vals[2].to_string(); + let email = vals[3].to_string(); + let male = vals[4] == "Male"; + let ip = Ipv4Addr::from_str(vals[5]).map_err(|e| e.to_string())?; + Ok(User { + id, + first, + last, + email, + male, + ip, + }) + } +} + +impl ExtendBytes for User { + fn append_raw_bytes(&self, buf: &mut Vec) { + self.id.append_raw_bytes(buf); + self.first.append_raw_bytes(buf); + self.last.append_raw_bytes(buf); + self.email.append_raw_bytes(buf); + self.male.append_raw_bytes(buf); + buf.extend_from_slice(&self.ip.to_string().as_bytes()); + } +} + +fn get_path(name: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(name); + path.to_str().unwrap().to_string() +} + +fn init(name: &str) -> Result { + let path = get_path(name); + let _ = std::fs::create_dir(&path).unwrap_or_default(); + LMDBBuilder::new() + .set_path(&path) + .set_environment_size(10) + .set_max_number_of_databases(2) + .add_database("users", db::CREATE) + .build() +} + +fn clean_up(name: &str) { + std::fs::remove_dir_all(get_path(name)).unwrap(); +} + +fn load_users() -> Vec { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/users.csv"); + let f = std::fs::read_to_string(path).unwrap(); + f.split("\n").map(|s| User::new(s).unwrap()).collect() +} + +fn insert_all_users(name: &str) -> (Vec, LMDBDatabase) { + let users = load_users(); + let env = init(name).unwrap(); + let db = env.get_handle("users").unwrap(); + let res = db.with_write_transaction(|mut db| { + for user in &users { + db.insert(&user.id, &user)?; + } + Ok(()) + }); + assert!(res.is_ok()); + (users, db) +} + +#[test] +fn single_thread() { + let users = load_users(); + let env = init("single_thread").unwrap(); + let db = env.get_handle("users").unwrap(); + for user in &users { + db.insert(&user.id, &user).unwrap(); + } + for user in users.iter() { + let check: User = db.get(&user.id).unwrap().unwrap(); + assert_eq!(check, *user); + } + assert_eq!(db.len().unwrap(), 1000); + clean_up("single_thread"); +} + +#[test] +fn multi_thread() { + let users_arc = Arc::new(load_users()); + let env = init("multi_thread").unwrap(); + let mut threads = Vec::new(); + for i in 0..10 { + let db = env.get_handle("users").unwrap(); + let users = users_arc.clone(); + threads.push(thread::spawn(move || { + for j in 0..100 { + let user = &users[i * 100 + j]; + db.insert(&user.id, user).unwrap(); + } + })); + } + + for thread in threads { + thread.join().unwrap(); + } + + env.log_info(); + let db = env.get_handle("users").unwrap(); + for user in users_arc.iter() { + let check: User = db.get(&user.id).unwrap().unwrap(); + assert_eq!(check, *user); + } + clean_up("multi_thread"); +} + +#[test] +fn transactions() { + let (users, db) = insert_all_users("transactions"); + // Test the `exists` and value retrieval functions + let res = db.with_read_transaction::<_, User>(|txn| { + for user in users.iter() { + assert!(txn.exists(&user.id).unwrap()); + let check: User = txn.get(&user.id).unwrap().unwrap(); + assert_eq!(check, *user); + } + Ok(None) + }); + assert!(res.unwrap().is_none()); + clean_up("transactions"); +} + +/// Simultaneous writes in different threads +#[test] +fn multi_thread_writes() { + let env = init("multi-thread-writes").unwrap(); + let mut threads = Vec::new(); + for _ in 0..2 { + let db = env.get_handle("users").unwrap(); + threads.push(thread::spawn(move || { + let res = db.with_write_transaction(|mut txn| { + for j in 0..1000 { + txn.insert(&j, &j)?; + } + Ok(()) + }); + assert!(res.is_ok()); + })); + } + for thread in threads { + thread.join().unwrap() + } + env.log_info(); + + let db = env.get_handle("users").unwrap(); + + assert_eq!(db.len().unwrap(), 1000); + for i in 0..1000 { + let value: i32 = db.get(&i).unwrap().unwrap(); + assert_eq!(i, value); + } + + clean_up("multi-thread-writes"); +} + +/// Multiple write transactions in a single thread +#[test] +fn multi_writes() { + let env = init("multi-writes").unwrap(); + for i in 0..2 { + let db = env.get_handle("users").unwrap(); + let res = db.with_write_transaction(|mut txn| { + for j in 0..1000 { + let v = i * 1000 + j; + txn.insert(&v, &v)?; + } + db.log_info(); + Ok(()) + }); + assert!(res.is_ok()); + } + env.flush().unwrap(); + clean_up("multi-writes"); +} + +#[test] +fn pair_iterator() { + let (users, db) = insert_all_users("pair_iterator"); + let res = db.for_each::(|pair| { + let (key, user) = pair.unwrap(); + assert_eq!(user.id, key); + assert_eq!(users[key as usize - 1], user); + }); + assert!(res.is_ok()); + clean_up("pair_iterator"); +} + +#[test] +fn exists_and_delete() { + let (_, db) = insert_all_users("delete"); + assert!(db.contains_key(&525u64).unwrap()); + db.remove(&525u64).unwrap(); + assert_eq!(db.contains_key(&525u64).unwrap(), false); + clean_up("delete"); +} diff --git a/infrastructure/storage/tests/users.csv b/infrastructure/storage/tests/users.csv new file mode 100644 index 0000000000..5bc1e3c4e7 --- /dev/null +++ b/infrastructure/storage/tests/users.csv @@ -0,0 +1,1000 @@ +1,Werner,Laherty,wlaherty0@ed.gov,Male,45.65.157.196 +2,Sella,Epsley,sepsley1@nature.com,Female,219.22.145.127 +3,Urbano,Dubois,udubois2@skyrock.com,Male,70.90.71.206 +4,Etti,Claricoats,eclaricoats3@google.ca,Female,225.113.205.198 +5,Emylee,Bursell,ebursell4@chron.com,Female,157.217.212.190 +6,Daisie,Seleway,dseleway5@csmonitor.com,Female,179.135.47.129 +7,Don,Corsham,dcorsham6@printfriendly.com,Male,230.78.190.3 +8,Gustavo,Dacres,gdacres7@woothemes.com,Male,66.27.147.93 +9,Evy,Stockman,estockman8@moonfruit.com,Female,131.237.123.89 +10,Saudra,Scurrah,sscurrah9@webs.com,Female,184.245.189.127 +11,Dallas,Pindred,dpindreda@google.nl,Male,209.25.101.224 +12,Heddi,Eastope,heastopeb@dell.com,Female,50.252.174.210 +13,Guy,Vogl,gvoglc@boston.com,Male,114.152.100.240 +14,Roarke,Houndsom,rhoundsomd@yolasite.com,Male,147.254.80.147 +15,Ronica,Dust,rduste@microsoft.com,Female,3.201.20.99 +16,Alexine,Booty,abootyf@princeton.edu,Female,205.224.53.41 +17,Bronnie,Maffetti,bmaffettig@networksolutions.com,Male,76.188.183.179 +18,Ingeberg,Skotcher,iskotcherh@sina.com.cn,Female,77.171.142.72 +19,Ketti,Geoghegan,kgeoghegani@state.tx.us,Female,41.51.215.205 +20,Shermie,Paroni,sparonij@mtv.com,Male,104.254.190.22 +21,Forrester,Winchester,fwinchesterk@google.co.uk,Male,56.117.116.168 +22,Conant,Embery,cemberyl@home.pl,Male,53.145.20.142 +23,Nathanial,Mordecai,nmordecaim@shinystat.com,Male,72.73.17.214 +24,Torrie,Widd,twiddn@cnet.com,Female,120.45.208.115 +25,Elisabetta,Wildes,ewildeso@sfgate.com,Female,147.134.202.177 +26,Fayth,Pennick,fpennickp@hp.com,Female,133.183.2.48 +27,Carce,Garrettson,cgarrettsonq@macromedia.com,Male,46.214.253.145 +28,Merrel,Cumo,mcumor@blinklist.com,Male,185.100.202.86 +29,Maire,Antunes,mantuness@multiply.com,Female,225.91.204.14 +30,Astra,Howick,ahowickt@reference.com,Female,3.143.187.240 +31,Wendell,Huxham,whuxhamu@ow.ly,Male,10.205.193.189 +32,Roselle,Jahnisch,rjahnischv@webs.com,Female,192.108.73.86 +33,Carlina,Byng,cbyngw@google.com,Female,6.236.23.114 +34,Hildegarde,Podmore,hpodmorex@vkontakte.ru,Female,55.135.135.95 +35,Bernhard,Brahmer,bbrahmery@google.de,Male,99.211.210.129 +36,Kally,Nicholls,knichollsz@nih.gov,Female,79.115.98.20 +37,Lyndsie,Skey,lskey10@webnode.com,Female,199.45.121.56 +38,Gery,Bunclark,gbunclark11@privacy.gov.au,Male,203.133.23.197 +39,Nathanael,Roset,nroset12@yahoo.com,Male,80.31.5.242 +40,Sandro,Crews,screws13@goo.ne.jp,Male,37.221.242.125 +41,Consalve,Copner,ccopner14@toplist.cz,Male,59.152.160.40 +42,Sarene,Brunelleschi,sbrunelleschi15@boston.com,Female,21.190.72.75 +43,Giacinta,Anlay,ganlay16@twitter.com,Female,30.252.237.149 +44,Reube,Coveley,rcoveley17@ed.gov,Male,124.108.204.23 +45,Corine,Schouthede,cschouthede18@businessinsider.com,Female,48.254.220.209 +46,Hewet,McLeese,hmcleese19@drupal.org,Male,130.42.36.4 +47,Josiah,Suero,jsuero1a@paypal.com,Male,22.145.251.127 +48,Urbain,Gutans,ugutans1b@meetup.com,Male,118.107.187.163 +49,Lenka,Featherstone,lfeatherstone1c@tripod.com,Female,53.244.154.72 +50,Kimberlyn,Newlove,knewlove1d@e-recht24.de,Female,32.244.112.57 +51,Brinn,Voller,bvoller1e@facebook.com,Female,169.42.16.118 +52,Rhiamon,Dudmesh,rdudmesh1f@desdev.cn,Female,136.22.54.135 +53,Jacquenetta,Quiddinton,jquiddinton1g@trellian.com,Female,109.188.9.106 +54,Bethany,Kitcherside,bkitcherside1h@livejournal.com,Female,142.146.224.25 +55,Malchy,Walstow,mwalstow1i@ebay.co.uk,Male,195.37.242.58 +56,Denver,McGennis,dmcgennis1j@geocities.jp,Male,235.26.97.71 +57,Gil,Yakushkin,gyakushkin1k@vk.com,Male,22.14.235.53 +58,Priscilla,Farans,pfarans1l@ovh.net,Female,47.187.76.57 +59,Leif,Pickerin,lpickerin1m@weather.com,Male,138.152.143.219 +60,Tripp,Gillimgham,tgillimgham1n@theatlantic.com,Male,148.145.155.19 +61,Darren,Magor,dmagor1o@bloglovin.com,Male,227.71.133.46 +62,Teodoor,Ward,tward1p@apache.org,Male,62.84.254.138 +63,Hasty,Ackwood,hackwood1q@hugedomains.com,Male,63.160.157.31 +64,Amos,Rhodef,arhodef1r@ning.com,Male,7.145.52.40 +65,Didi,Slyne,dslyne1s@dot.gov,Female,194.227.222.207 +66,Burnard,Lonsdale,blonsdale1t@ovh.net,Male,114.23.46.249 +67,Elle,Breckon,ebreckon1u@networksolutions.com,Female,215.39.63.230 +68,Hymie,Combes,hcombes1v@state.gov,Male,58.172.205.214 +69,Erich,Edwardes,eedwardes1w@fc2.com,Male,32.41.132.248 +70,Lenard,Treble,ltreble1x@vk.com,Male,146.121.32.20 +71,Joycelin,Bateson,jbateson1y@ft.com,Female,138.30.142.214 +72,Cazzie,Hanton,chanton1z@cafepress.com,Male,5.188.135.54 +73,Tiebout,Middler,tmiddler20@abc.net.au,Male,74.94.86.50 +74,Calida,Everleigh,ceverleigh21@creativecommons.org,Female,119.102.95.231 +75,Guendolen,Satterly,gsatterly22@tmall.com,Female,114.180.93.86 +76,Zacharias,Cowerd,zcowerd23@europa.eu,Male,210.21.96.237 +77,Ringo,Eudall,reudall24@facebook.com,Male,19.4.243.120 +78,Ingar,Biesterfeld,ibiesterfeld25@google.co.jp,Male,135.243.177.162 +79,Kearney,Bronger,kbronger26@edublogs.org,Male,120.155.169.100 +80,Olag,Avramovsky,oavramovsky27@thetimes.co.uk,Male,35.0.255.72 +81,Biron,Mensler,bmensler28@tmall.com,Male,178.46.63.9 +82,Arthur,Bengtsen,abengtsen29@aol.com,Male,22.135.227.107 +83,Salomo,Tissington,stissington2a@creativecommons.org,Male,228.25.207.28 +84,Maxy,Rosenzwig,mrosenzwig2b@devhub.com,Male,150.183.139.166 +85,Free,Parkes,fparkes2c@ustream.tv,Male,6.6.183.253 +86,Kara,Eastcourt,keastcourt2d@g.co,Female,129.47.157.131 +87,Rayshell,Randle,rrandle2e@friendfeed.com,Female,174.122.243.220 +88,Rafael,Adamek,radamek2f@taobao.com,Male,95.170.165.95 +89,Robinet,Tossell,rtossell2g@chronoengine.com,Female,158.32.170.103 +90,Sallie,Capron,scapron2h@angelfire.com,Female,1.151.28.144 +91,Virgilio,Gooly,vgooly2i@google.it,Male,191.120.174.238 +92,Danya,Vowell,dvowell2j@vkontakte.ru,Female,72.244.171.245 +93,Jdavie,Jedrzejewski,jjedrzejewski2k@phpbb.com,Male,4.77.207.41 +94,Angelico,Houldin,ahouldin2l@examiner.com,Male,235.156.142.82 +95,Minnie,Aizik,maizik2m@google.ca,Female,116.30.50.72 +96,Britt,Haseldine,bhaseldine2n@ezinearticles.com,Female,219.133.19.198 +97,Madge,Kibblewhite,mkibblewhite2o@economist.com,Female,69.152.149.188 +98,Therese,Najera,tnajera2p@theguardian.com,Female,41.237.230.45 +99,North,Jorge,njorge2q@naver.com,Male,212.97.68.88 +100,Roseann,Lukash,rlukash2r@slashdot.org,Female,160.70.76.202 +101,Rosanne,Eakins,reakins2s@etsy.com,Female,0.144.220.101 +102,Esther,Verryan,everryan2t@macromedia.com,Female,129.55.64.228 +103,Darnell,Shepherd,dshepherd2u@statcounter.com,Male,68.132.182.27 +104,Ophelia,Schruurs,oschruurs2v@techcrunch.com,Female,249.31.245.168 +105,Colin,Gerdts,cgerdts2w@mysql.com,Male,81.153.170.233 +106,Mervin,Berrington,mberrington2x@github.io,Male,180.251.36.10 +107,Berty,Ruprich,bruprich2y@51.la,Female,153.79.8.123 +108,Hagan,Coatsworth,hcoatsworth2z@pcworld.com,Male,121.66.10.210 +109,Yorke,Roo,yroo30@nymag.com,Male,184.35.6.96 +110,Ardys,Aimson,aaimson31@soup.io,Female,135.220.157.192 +111,Sandro,Hugnet,shugnet32@360.cn,Male,76.13.244.45 +112,Ogden,Riglar,origlar33@google.ru,Male,208.90.60.84 +113,Garek,Josupeit,gjosupeit34@umn.edu,Male,51.208.229.137 +114,Corey,Fobidge,cfobidge35@scribd.com,Male,220.232.99.84 +115,Sophia,Bradburne,sbradburne36@topsy.com,Female,111.21.162.38 +116,Evelyn,Badcock,ebadcock37@cnbc.com,Male,197.148.234.167 +117,Smitty,Suscens,ssuscens38@jigsy.com,Male,233.125.141.177 +118,Glenden,Smoth,gsmoth39@desdev.cn,Male,177.172.20.162 +119,Tabby,Tole,ttole3a@artisteer.com,Male,181.219.104.8 +120,Cassie,Dogg,cdogg3b@com.com,Male,94.128.218.189 +121,Dunn,Simoens,dsimoens3c@springer.com,Male,34.58.68.102 +122,Bonnie,Huller,bhuller3d@gravatar.com,Female,170.66.150.232 +123,Ernaline,Kencott,ekencott3e@gmpg.org,Female,20.243.5.118 +124,Christabel,Founds,cfounds3f@google.com.au,Female,148.109.36.124 +125,Tessa,Lopes,tlopes3g@archive.org,Female,148.7.143.176 +126,Maybelle,Goldis,mgoldis3h@mayoclinic.com,Female,109.229.73.108 +127,Kore,Murtell,kmurtell3i@phpbb.com,Female,195.207.211.249 +128,Iorgos,Sheerman,isheerman3j@reference.com,Male,22.21.98.106 +129,Allison,Huyge,ahuyge3k@sciencedaily.com,Female,243.85.208.197 +130,Elston,Kops,ekops3l@topsy.com,Male,3.158.49.0 +131,Johnathon,Krugmann,jkrugmann3m@bandcamp.com,Male,45.31.55.211 +132,Melva,Gledhall,mgledhall3n@wisc.edu,Female,47.82.79.93 +133,Hortensia,O'Dreain,hodreain3o@is.gd,Female,126.134.197.252 +134,Rockey,Paur,rpaur3p@bloomberg.com,Male,116.200.129.163 +135,Yoshiko,MacTerlagh,ymacterlagh3q@linkedin.com,Female,206.51.65.92 +136,Florri,Doohey,fdoohey3r@hud.gov,Female,25.41.105.138 +137,Maryanna,D'Agostini,mdagostini3s@naver.com,Female,23.34.118.41 +138,Carlie,Mogra,cmogra3t@globo.com,Male,18.231.184.119 +139,Milt,Paver,mpaver3u@salon.com,Male,131.132.160.138 +140,Augusto,Caughey,acaughey3v@thetimes.co.uk,Male,214.52.69.41 +141,Sherye,Shwenn,sshwenn3w@go.com,Female,97.147.101.235 +142,Sheba,Cleeton,scleeton3x@topsy.com,Female,20.59.143.119 +143,Minor,Chavey,mchavey3y@sogou.com,Male,114.132.190.194 +144,Brooke,Cowden,bcowden3z@guardian.co.uk,Male,241.92.212.86 +145,Korey,Ilyin,kilyin40@marketwatch.com,Male,133.204.179.77 +146,Meier,Silbert,msilbert41@latimes.com,Male,186.251.201.108 +147,Giuditta,Bainton,gbainton42@webmd.com,Female,105.97.124.80 +148,Marcus,Higgoe,mhiggoe43@cnn.com,Male,20.189.245.187 +149,Brnaba,Hember,bhember44@unicef.org,Male,163.145.196.20 +150,Gerri,Beausang,gbeausang45@usnews.com,Male,15.109.37.68 +151,Donella,Huggin,dhuggin46@ca.gov,Female,120.125.216.195 +152,Tracey,Kenen,tkenen47@comsenz.com,Female,140.170.24.151 +153,Zorana,Durram,zdurram48@sogou.com,Female,180.142.213.73 +154,Harp,Merryweather,hmerryweather49@drupal.org,Male,172.235.94.155 +155,Abdul,Tagg,atagg4a@tiny.cc,Male,225.129.8.43 +156,Augustine,Doucette,adoucette4b@blog.com,Male,220.111.209.2 +157,Durante,Jedrzejewicz,djedrzejewicz4c@cnn.com,Male,138.32.14.203 +158,Hanna,Topping,htopping4d@imageshack.us,Female,159.184.226.163 +159,Esmaria,Francescozzi,efrancescozzi4e@geocities.com,Female,140.142.13.46 +160,Diena,Darcey,ddarcey4f@rakuten.co.jp,Female,58.172.29.133 +161,Edna,Aitkenhead,eaitkenhead4g@hibu.com,Female,161.205.220.65 +162,Norman,Giffin,ngiffin4h@biblegateway.com,Male,250.147.72.47 +163,Hillier,Lemerle,hlemerle4i@illinois.edu,Male,169.238.17.48 +164,Gerri,McGuffog,gmcguffog4j@tinypic.com,Male,18.33.72.14 +165,Catharine,Izakoff,cizakoff4k@pbs.org,Female,23.198.173.9 +166,Rhys,Rupprecht,rrupprecht4l@tumblr.com,Male,138.217.106.8 +167,Carina,Glewe,cglewe4m@shinystat.com,Female,0.253.104.96 +168,Mikael,Rush,mrush4n@domainmarket.com,Male,86.234.222.208 +169,Jareb,Addis,jaddis4o@soundcloud.com,Male,166.238.226.190 +170,Devora,Sampey,dsampey4p@icio.us,Female,194.178.107.169 +171,Rad,Fullard,rfullard4q@51.la,Male,91.144.120.29 +172,Hermine,Basindale,hbasindale4r@whitehouse.gov,Female,233.55.222.213 +173,Kingsley,Connolly,kconnolly4s@typepad.com,Male,164.138.66.87 +174,Moise,Galier,mgalier4t@arstechnica.com,Male,6.103.0.138 +175,Oralee,Samwayes,osamwayes4u@cyberchimps.com,Female,234.65.52.44 +176,Hyacinth,Vasyukhnov,hvasyukhnov4v@icio.us,Female,150.97.166.62 +177,Toddy,Perrigo,tperrigo4w@wsj.com,Male,238.138.222.17 +178,Lilla,Sarsons,lsarsons4x@blogspot.com,Female,84.120.240.244 +179,Ivett,Bateman,ibateman4y@sbwire.com,Female,41.104.115.169 +180,Esther,Siehard,esiehard4z@huffingtonpost.com,Female,85.234.188.48 +181,Zack,Clemmensen,zclemmensen50@engadget.com,Male,160.201.14.28 +182,Hortense,Ditts,hditts51@hhs.gov,Female,116.99.217.98 +183,Dodie,Jovicic,djovicic52@ning.com,Female,183.208.51.62 +184,Katey,Heifer,kheifer53@patch.com,Female,59.27.208.61 +185,Lesli,Brent,lbrent54@businessinsider.com,Female,122.183.181.26 +186,Winny,Cobley,wcobley55@usatoday.com,Male,82.32.24.195 +187,Rori,Klugman,rklugman56@blogger.com,Female,14.73.170.3 +188,Gabrila,Epple,gepple57@telegraph.co.uk,Female,22.247.240.130 +189,Enrique,Olney,eolney58@ftc.gov,Male,22.107.65.148 +190,Adolpho,Norquoy,anorquoy59@flavors.me,Male,38.27.103.185 +191,Putnam,Sussans,psussans5a@purevolume.com,Male,126.243.178.48 +192,Candy,Pyffe,cpyffe5b@hhs.gov,Female,187.7.236.110 +193,Cindee,Manjin,cmanjin5c@pinterest.com,Female,137.164.181.152 +194,Marie-jeanne,Burchett,mburchett5d@weibo.com,Female,219.41.124.224 +195,Dana,Josuweit,djosuweit5e@linkedin.com,Male,220.76.148.247 +196,Shoshana,Lewcock,slewcock5f@jigsy.com,Female,64.248.93.180 +197,Garald,Kincade,gkincade5g@huffingtonpost.com,Male,58.202.163.96 +198,Consuela,Vecard,cvecard5h@flavors.me,Female,173.140.232.254 +199,Aleen,Rickhuss,arickhuss5i@ft.com,Female,130.163.128.193 +200,Rutger,Sebrens,rsebrens5j@mysql.com,Male,20.7.110.151 +201,Addi,Rollingson,arollingson5k@uol.com.br,Female,148.125.220.186 +202,Bertina,Stratton,bstratton5l@imdb.com,Female,190.7.85.233 +203,Maxy,Neads,mneads5m@seesaa.net,Female,72.73.187.123 +204,Rolfe,Trobe,rtrobe5n@scientificamerican.com,Male,41.25.146.151 +205,Bobette,Morrish,bmorrish5o@ihg.com,Female,85.133.226.94 +206,Jemima,Inglesant,jinglesant5p@cafepress.com,Female,211.64.179.113 +207,Raimundo,Cornbell,rcornbell5q@usgs.gov,Male,154.236.88.201 +208,Hendrick,Colvie,hcolvie5r@yellowbook.com,Male,18.38.255.136 +209,Jennine,Ohlsen,johlsen5s@deliciousdays.com,Female,195.232.213.8 +210,Allie,Pietrowski,apietrowski5t@merriam-webster.com,Male,239.237.28.9 +211,Averyl,Kenrack,akenrack5u@yolasite.com,Female,215.80.13.53 +212,Kristen,Jaine,kjaine5v@biblegateway.com,Female,157.173.154.207 +213,Dori,Johnsey,djohnsey5w@technorati.com,Female,46.161.216.201 +214,Erhard,Firks,efirks5x@miibeian.gov.cn,Male,132.179.121.213 +215,Rebekah,McDunlevy,rmcdunlevy5y@msn.com,Female,103.190.175.180 +216,Tybie,Doret,tdoret5z@blogger.com,Female,181.245.255.76 +217,Gratia,Dulson,gdulson60@shareasale.com,Female,191.208.187.20 +218,Ferrell,Spittle,fspittle61@blog.com,Male,147.249.103.183 +219,Eadie,O'Duane,eoduane62@about.com,Female,85.145.246.32 +220,Allyn,Josling,ajosling63@blogs.com,Male,218.10.53.1 +221,Lindy,Laxton,llaxton64@dropbox.com,Male,32.123.63.230 +222,Gunther,O'Toole,gotoole65@npr.org,Male,153.241.89.75 +223,Odette,Lamperti,olamperti66@jugem.jp,Female,184.238.129.139 +224,Doyle,Culter,dculter67@shop-pro.jp,Male,3.153.96.124 +225,Rock,Quainton,rquainton68@soup.io,Male,129.68.192.246 +226,Georgy,Thurling,gthurling69@jugem.jp,Male,217.149.20.231 +227,Eben,Snowball,esnowball6a@naver.com,Male,123.36.29.197 +228,Frederica,Patrie,fpatrie6b@g.co,Female,220.169.232.41 +229,Raynor,Gershom,rgershom6c@furl.net,Male,114.141.132.140 +230,Hewie,Cerman,hcerman6d@patch.com,Male,102.218.210.127 +231,Earlie,Caen,ecaen6e@mozilla.com,Male,172.68.166.72 +232,Joseito,Muriel,jmuriel6f@home.pl,Male,2.91.37.43 +233,Paolo,Nunson,pnunson6g@imdb.com,Male,132.247.196.226 +234,Ertha,Pantling,epantling6h@livejournal.com,Female,244.56.234.152 +235,Tommie,Edgeller,tedgeller6i@google.es,Male,173.54.91.241 +236,Porty,Thain,pthain6j@house.gov,Male,247.114.229.82 +237,Tabbie,Wyllcocks,twyllcocks6k@globo.com,Male,121.250.147.32 +238,Ase,Coult,acoult6l@gravatar.com,Male,100.64.126.254 +239,Bobbye,St. Queintain,bstqueintain6m@fotki.com,Female,243.238.136.20 +240,Lolita,Cellier,lcellier6n@360.cn,Female,134.183.31.210 +241,Brand,Bitchener,bbitchener6o@howstuffworks.com,Male,104.94.121.237 +242,Hermy,Philippe,hphilippe6p@amazon.com,Male,106.7.182.227 +243,Bogart,Winship,bwinship6q@yale.edu,Male,197.240.51.87 +244,Frederic,Maceur,fmaceur6r@51.la,Male,24.168.144.235 +245,Norry,Baron,nbaron6s@usnews.com,Female,224.82.184.236 +246,Bunni,Brilon,bbrilon6t@uol.com.br,Female,197.236.16.163 +247,Nelie,Lawdham,nlawdham6u@nature.com,Female,124.230.221.10 +248,Erika,Glentz,eglentz6v@wikimedia.org,Female,18.12.72.158 +249,Heddi,Roydon,hroydon6w@freewebs.com,Female,188.239.60.80 +250,Othilia,McGahern,omcgahern6x@seattletimes.com,Female,149.105.41.29 +251,Erinn,Cosin,ecosin6y@free.fr,Female,203.106.34.247 +252,Arch,Gaunson,agaunson6z@sohu.com,Male,170.107.25.99 +253,Harmony,Trice,htrice70@netscape.com,Female,142.10.247.0 +254,Deirdre,Frisel,dfrisel71@bizjournals.com,Female,217.249.90.153 +255,Pierre,Lambe,plambe72@usgs.gov,Male,137.163.219.108 +256,Wernher,Marchi,wmarchi73@ucoz.com,Male,163.132.19.172 +257,Adora,Briatt,abriatt74@gov.uk,Female,184.131.184.35 +258,Molli,Feeney,mfeeney75@timesonline.co.uk,Female,99.155.225.195 +259,Miguela,Renackowna,mrenackowna76@npr.org,Female,90.9.229.18 +260,Stefano,Petrie,spetrie77@booking.com,Male,33.241.170.27 +261,Gustavo,Girvin,ggirvin78@vistaprint.com,Male,144.215.252.147 +262,Kalle,Waudby,kwaudby79@auda.org.au,Male,224.123.148.67 +263,Kippy,Stanford,kstanford7a@theguardian.com,Female,228.152.242.13 +264,Leupold,Tesmond,ltesmond7b@go.com,Male,44.226.167.211 +265,Wallis,Cuttings,wcuttings7c@omniture.com,Male,107.69.208.230 +266,Sue,Mattaus,smattaus7d@altervista.org,Female,95.143.213.130 +267,Alberta,Rubke,arubke7e@odnoklassniki.ru,Female,252.43.119.52 +268,Frederica,Chillcot,fchillcot7f@domainmarket.com,Female,68.104.166.224 +269,Margi,Mersh,mmersh7g@mashable.com,Female,40.131.124.5 +270,Worthy,Toth,wtoth7h@zdnet.com,Male,22.60.117.87 +271,Barbaraanne,McGonigal,bmcgonigal7i@ask.com,Female,92.58.121.29 +272,Corina,Sandiford,csandiford7j@t-online.de,Female,64.239.92.245 +273,Erich,Oliveras,eoliveras7k@reference.com,Male,141.78.67.141 +274,Nico,Coltart,ncoltart7l@theatlantic.com,Male,83.201.29.187 +275,Otis,Basek,obasek7m@skyrock.com,Male,59.47.187.122 +276,Jordanna,Sperling,jsperling7n@google.es,Female,177.176.82.15 +277,Tara,Wright,twright7o@mlb.com,Female,93.219.128.165 +278,Rosana,Varah,rvarah7p@un.org,Female,157.61.45.177 +279,Jourdain,Wareham,jwareham7q@twitpic.com,Male,15.21.62.234 +280,Mabelle,Tondeur,mtondeur7r@slate.com,Female,121.39.193.41 +281,Jesse,Atcherley,jatcherley7s@nih.gov,Female,248.142.221.205 +282,Xenia,Christescu,xchristescu7t@eventbrite.com,Female,137.117.175.169 +283,Gayelord,Lauks,glauks7u@seesaa.net,Male,164.50.99.221 +284,Neill,Bidewell,nbidewell7v@slideshare.net,Male,33.251.63.53 +285,Gilberte,Ranken,granken7w@privacy.gov.au,Female,228.106.22.185 +286,Selby,Jessope,sjessope7x@npr.org,Male,79.53.165.99 +287,Madelin,Barbe,mbarbe7y@skyrock.com,Female,161.111.86.3 +288,Concordia,Maric,cmaric7z@europa.eu,Female,217.151.11.45 +289,Lu,Deane,ldeane80@miibeian.gov.cn,Female,247.248.204.228 +290,Bryanty,Holdron,bholdron81@hubpages.com,Male,125.182.41.116 +291,Rex,Beadnall,rbeadnall82@surveymonkey.com,Male,196.28.18.63 +292,Debby,Kubista,dkubista83@biglobe.ne.jp,Female,114.142.99.106 +293,Wilton,Crilley,wcrilley84@netlog.com,Male,174.119.3.198 +294,Timmy,Allone,tallone85@spiegel.de,Female,192.178.144.78 +295,Debi,Clemenzi,dclemenzi86@umich.edu,Female,63.213.106.121 +296,Amalia,Leavy,aleavy87@census.gov,Female,22.169.3.33 +297,Garwin,Ivakhnov,givakhnov88@ask.com,Male,15.114.40.69 +298,Adriaens,Ovey,aovey89@marriott.com,Female,56.233.216.233 +299,Karrie,McBeith,kmcbeith8a@indiegogo.com,Female,141.240.3.58 +300,Hermy,Torri,htorri8b@indiatimes.com,Male,76.230.182.58 +301,Ham,Luttgert,hluttgert8c@hibu.com,Male,250.191.180.29 +302,Cullin,Turland,cturland8d@tamu.edu,Male,186.49.204.220 +303,Leontyne,Langfitt,llangfitt8e@unblog.fr,Female,252.228.192.45 +304,Zarah,Andrault,zandrault8f@imdb.com,Female,227.116.115.114 +305,Ed,McDougal,emcdougal8g@shutterfly.com,Male,189.195.13.182 +306,Gertrudis,Skerritt,gskerritt8h@ocn.ne.jp,Female,97.38.167.63 +307,Iago,Devey,idevey8i@i2i.jp,Male,54.179.168.187 +308,Paton,Wallenger,pwallenger8j@time.com,Male,45.146.37.13 +309,Wyatan,Hackwell,whackwell8k@networksolutions.com,Male,159.62.23.47 +310,Harcourt,McCrackem,hmccrackem8l@blogger.com,Male,142.254.102.176 +311,Eli,Frosdick,efrosdick8m@altervista.org,Male,82.74.131.233 +312,Rusty,Scoles,rscoles8n@google.es,Male,79.119.230.86 +313,Jorrie,Stallebrass,jstallebrass8o@nps.gov,Female,200.198.207.34 +314,Anneliese,Meak,ameak8p@cornell.edu,Female,146.237.249.136 +315,Gustaf,Standbrooke,gstandbrooke8q@thetimes.co.uk,Male,252.70.183.170 +316,Agneta,Rawlingson,arawlingson8r@shareasale.com,Female,240.233.252.242 +317,Elmo,Haysar,ehaysar8s@altervista.org,Male,201.71.229.121 +318,Alyosha,Taber,ataber8t@sogou.com,Male,138.119.96.155 +319,Josee,Feather,jfeather8u@technorati.com,Female,36.117.56.158 +320,Petronia,Simoens,psimoens8v@unicef.org,Female,187.107.102.136 +321,Aguie,Jurczik,ajurczik8w@furl.net,Male,42.150.52.40 +322,Lem,Dearn,ldearn8x@hhs.gov,Male,119.191.0.48 +323,Lyle,Gregoire,lgregoire8y@scientificamerican.com,Male,209.203.143.75 +324,Donelle,Pluthero,dpluthero8z@delicious.com,Female,194.209.138.80 +325,Chrotoem,Carnihan,ccarnihan90@noaa.gov,Male,150.98.192.234 +326,Ginelle,Zoanetti,gzoanetti91@technorati.com,Female,5.234.224.33 +327,Karolina,Strognell,kstrognell92@pbs.org,Female,215.100.74.183 +328,Remington,Thornton,rthornton93@trellian.com,Male,38.134.164.164 +329,Mufi,Kubica,mkubica94@seesaa.net,Female,171.147.43.216 +330,Wake,Nazareth,wnazareth95@xinhuanet.com,Male,214.225.21.242 +331,Artemas,Vian,avian96@cbsnews.com,Male,149.210.200.160 +332,Tiffanie,Osmond,tosmond97@intel.com,Female,246.31.229.52 +333,Torrence,Southcombe,tsouthcombe98@who.int,Male,188.175.113.91 +334,Giraud,Dickings,gdickings99@nymag.com,Male,237.246.8.104 +335,Steffi,Harpin,sharpin9a@usatoday.com,Female,167.135.138.75 +336,Hilliard,Weafer,hweafer9b@usnews.com,Male,118.210.220.36 +337,Effie,Diano,ediano9c@oakley.com,Female,117.88.132.224 +338,Upton,Edgerton,uedgerton9d@cnbc.com,Male,22.220.220.223 +339,Evania,Caskey,ecaskey9e@archive.org,Female,125.139.31.105 +340,Dorolisa,Layne,dlayne9f@devhub.com,Female,88.193.152.9 +341,Risa,Dessant,rdessant9g@alexa.com,Female,106.126.111.128 +342,Shelia,Czajkowska,sczajkowska9h@chronoengine.com,Female,86.54.174.133 +343,Diandra,McGrudder,dmcgrudder9i@yellowbook.com,Female,139.97.240.154 +344,Leah,Osboldstone,losboldstone9j@live.com,Female,157.72.42.233 +345,Elayne,Britner,ebritner9k@blinklist.com,Female,10.14.21.76 +346,Patric,Chern,pchern9l@cmu.edu,Male,58.143.175.229 +347,Dexter,Jugging,djugging9m@abc.net.au,Male,189.140.151.72 +348,Ammamaria,Zanassi,azanassi9n@unc.edu,Female,9.182.79.76 +349,Kylynn,Lowing,klowing9o@google.co.uk,Female,137.86.144.106 +350,Robbie,Guslon,rguslon9p@delicious.com,Female,111.10.130.61 +351,Gal,Terzo,gterzo9q@nyu.edu,Male,191.106.18.132 +352,Marjy,Radmer,mradmer9r@marketwatch.com,Female,245.107.126.109 +353,Dorisa,Higgonet,dhiggonet9s@hc360.com,Female,147.107.113.179 +354,Hoebart,Ofener,hofener9t@flavors.me,Male,139.226.14.181 +355,Melisse,Bust,mbust9u@homestead.com,Female,65.251.228.185 +356,Booth,Poveleye,bpoveleye9v@hexun.com,Male,152.94.165.123 +357,Tyson,Kerner,tkerner9w@guardian.co.uk,Male,149.5.23.38 +358,Lorelle,Bridger,lbridger9x@youtube.com,Female,112.147.1.229 +359,Lura,Slemming,lslemming9y@geocities.jp,Female,189.255.60.127 +360,Biddie,Trueman,btrueman9z@123-reg.co.uk,Female,20.27.47.254 +361,Iosep,Moneti,imonetia0@tumblr.com,Male,150.140.44.230 +362,Beulah,Gatesman,bgatesmana1@technorati.com,Female,190.31.97.149 +363,Donaugh,Bazire,dbazirea2@google.cn,Male,37.158.171.136 +364,La verne,Scough,lscougha3@wp.com,Female,169.7.252.52 +365,Brod,Tolcharde,btolchardea4@gov.uk,Male,127.17.226.177 +366,Christina,Leveritt,cleveritta5@mail.ru,Female,114.75.17.101 +367,Hillery,Spieck,hspiecka6@instagram.com,Male,200.114.107.25 +368,Maurise,Shalcras,mshalcrasa7@reddit.com,Male,18.167.224.172 +369,Cort,Flaherty,cflahertya8@jugem.jp,Male,32.108.162.184 +370,Rik,Manston,rmanstona9@ed.gov,Male,59.66.86.31 +371,Kattie,Gatrill,kgatrillaa@privacy.gov.au,Female,13.105.163.78 +372,Ambrose,Ayton,aaytonab@latimes.com,Male,169.198.119.181 +373,Flss,Janssen,fjanssenac@histats.com,Female,142.48.197.45 +374,Ronica,Mattei,rmatteiad@lulu.com,Female,114.121.99.89 +375,Fitzgerald,Lanmeid,flanmeidae@craigslist.org,Male,18.188.81.90 +376,Khalil,Skrines,kskrinesaf@independent.co.uk,Male,237.246.49.60 +377,Sebastien,Menezes,smenezesag@sakura.ne.jp,Male,169.107.200.188 +378,Ree,Isley,risleyah@virginia.edu,Female,113.43.113.216 +379,Marsh,Mullard,mmullardai@infoseek.co.jp,Male,61.135.11.141 +380,Flory,Bultitude,fbultitudeaj@spiegel.de,Male,209.244.181.219 +381,Harley,Traite,htraiteak@bbc.co.uk,Female,215.180.186.2 +382,Angie,Covelle,acovelleal@deviantart.com,Male,141.192.14.15 +383,Kitti,Alfonsini,kalfonsiniam@squarespace.com,Female,191.17.36.121 +384,Joella,Rawstron,jrawstronan@wordpress.com,Female,162.193.8.254 +385,Lurlene,Earley,learleyao@123-reg.co.uk,Female,234.160.79.131 +386,Elana,Pattesall,epattesallap@lycos.com,Female,30.142.197.80 +387,Samaria,McKleod,smckleodaq@networksolutions.com,Female,47.164.18.34 +388,Pamella,Armin,parminar@soup.io,Female,118.188.213.192 +389,Dorella,Barthorpe,dbarthorpeas@dedecms.com,Female,162.247.162.214 +390,Bancroft,Wakley,bwakleyat@aol.com,Male,212.245.154.92 +391,Ange,Aylmer,aaylmerau@amazon.de,Male,96.194.115.113 +392,Zerk,Edmeades,zedmeadesav@boston.com,Male,213.126.50.112 +393,Dione,Monard,dmonardaw@ca.gov,Female,203.197.6.45 +394,Rancell,Corneljes,rcorneljesax@chronoengine.com,Male,232.214.8.172 +395,Daron,Leech,dleechay@xing.com,Male,115.5.5.54 +396,Becka,Marrison,bmarrisonaz@e-recht24.de,Female,91.149.9.5 +397,Joey,Fawcett,jfawcettb0@loc.gov,Female,188.101.127.126 +398,Jo ann,Shrimpling,jshrimplingb1@scribd.com,Female,73.181.129.52 +399,Stuart,Uren,surenb2@cnet.com,Male,99.101.249.35 +400,Edouard,Sturdey,esturdeyb3@blogtalkradio.com,Male,109.78.77.56 +401,Kristine,Farman,kfarmanb4@utexas.edu,Female,150.183.231.106 +402,Carleton,Tulip,ctulipb5@elegantthemes.com,Male,82.26.40.205 +403,Bil,Geertsen,bgeertsenb6@tumblr.com,Male,118.165.246.204 +404,Louella,Chinery,lchineryb7@chicagotribune.com,Female,95.166.76.30 +405,Bryn,Pattini,bpattinib8@youtu.be,Male,1.112.211.57 +406,Ashton,Noen,anoenb9@hexun.com,Male,250.212.31.46 +407,Flo,Barukh,fbarukhba@seesaa.net,Female,196.149.44.18 +408,Orville,Gilliam,ogilliambb@msn.com,Male,238.250.246.133 +409,Dane,Chauvey,dchauveybc@wikia.com,Male,220.15.255.58 +410,Carmela,Hakonsson,chakonssonbd@sun.com,Female,172.20.73.255 +411,Theo,Waugh,twaughbe@mozilla.com,Female,191.27.66.190 +412,Audry,Allabarton,aallabartonbf@xing.com,Female,210.70.223.239 +413,Marietta,Cutchey,mcutcheybg@edublogs.org,Male,231.23.65.153 +414,Noni,Roizn,nroiznbh@wix.com,Female,110.199.176.134 +415,Kora,Duddan,kduddanbi@skype.com,Female,163.73.108.5 +416,Ophelia,Feldmark,ofeldmarkbj@yelp.com,Female,188.191.12.142 +417,Brendis,Manning,bmanningbk@gov.uk,Male,47.103.74.192 +418,Emeline,Crank,ecrankbl@cnet.com,Female,50.70.249.186 +419,Olenolin,Ingarfill,oingarfillbm@edublogs.org,Male,168.136.21.37 +420,Elicia,Verrill,everrillbn@bloomberg.com,Female,61.37.98.192 +421,Karlee,Venables,kvenablesbo@dyndns.org,Female,99.8.174.181 +422,Evelin,Aldrich,ealdrichbp@taobao.com,Male,186.113.8.86 +423,Torey,Shrawley,tshrawleybq@miitbeian.gov.cn,Male,50.28.124.222 +424,Jocelyn,Hue,jhuebr@addtoany.com,Female,21.56.92.98 +425,Wilek,Daffey,wdaffeybs@yellowbook.com,Male,78.198.212.103 +426,Mendy,Shovelton,mshoveltonbt@salon.com,Male,218.74.17.242 +427,Guss,Sheldrake,gsheldrakebu@nhs.uk,Male,112.59.116.224 +428,Louisette,Tolley,ltolleybv@g.co,Female,202.7.220.183 +429,Tabby,Horry,thorrybw@uol.com.br,Female,29.219.112.158 +430,Cassandre,Furness,cfurnessbx@foxnews.com,Female,58.124.135.31 +431,Natty,McAvinchey,nmcavincheyby@icio.us,Male,171.76.116.249 +432,Emelia,Shurrock,eshurrockbz@gmpg.org,Female,217.143.162.81 +433,Doretta,Alliot,dalliotc0@barnesandnoble.com,Female,58.122.154.64 +434,Ewell,Frogley,efrogleyc1@soup.io,Male,180.164.235.210 +435,Winonah,McCrea,wmccreac2@answers.com,Female,27.52.155.230 +436,Jewelle,Creaven,jcreavenc3@google.cn,Female,69.24.154.249 +437,Bat,Goranov,bgoranovc4@wikipedia.org,Male,91.224.151.29 +438,Mitchael,Fellgatt,mfellgattc5@instagram.com,Male,77.26.213.243 +439,Elva,Cabel,ecabelc6@bloglovin.com,Female,36.148.62.124 +440,Dollie,Huntriss,dhuntrissc7@opensource.org,Female,110.233.191.12 +441,Kriste,Hamber,khamberc8@reverbnation.com,Female,129.214.228.57 +442,Janot,Lochrie,jlochriec9@php.net,Female,146.116.230.144 +443,Elisabetta,Mellor,emellorca@angelfire.com,Female,59.60.231.49 +444,Rockwell,Brahan,rbrahancb@about.com,Male,239.225.115.73 +445,Colline,Dullingham,cdullinghamcc@sina.com.cn,Female,127.241.188.227 +446,Anjanette,O'Collopy,aocollopycd@nba.com,Female,55.87.202.240 +447,Coleen,Ludwikiewicz,cludwikiewiczce@google.com.br,Female,49.13.254.84 +448,Ambrosi,Begin,abegincf@paginegialle.it,Male,15.125.189.217 +449,Lew,Sagerson,lsagersoncg@mozilla.com,Male,47.235.103.47 +450,Robinson,Hearnah,rhearnahch@japanpost.jp,Male,14.179.238.137 +451,Meyer,Spatoni,mspatonici@csmonitor.com,Male,186.196.242.90 +452,Terrel,Crowley,tcrowleycj@npr.org,Male,218.146.87.159 +453,Winnie,Beattie,wbeattieck@pinterest.com,Male,178.192.46.126 +454,Livvy,Kershow,lkershowcl@1688.com,Female,206.167.16.138 +455,Anstice,Shepard,ashepardcm@dell.com,Female,193.251.218.1 +456,Corey,Boller,cbollercn@gravatar.com,Male,20.190.174.69 +457,Sapphire,Esgate,sesgateco@bigcartel.com,Female,72.203.194.228 +458,Katherina,Loody,kloodycp@umn.edu,Female,210.114.159.150 +459,Lezley,Ditter,ldittercq@comsenz.com,Male,182.175.119.27 +460,Karee,Jewise,kjewisecr@youku.com,Female,181.72.163.53 +461,Reade,Heasly,rheaslycs@dyndns.org,Male,111.255.96.23 +462,Carin,Lexa,clexact@apache.org,Female,47.156.88.181 +463,Yorke,Passler,ypasslercu@vimeo.com,Male,114.80.61.32 +464,Jeannie,Trodden,jtroddencv@hud.gov,Female,9.34.207.59 +465,Hyman,Gleader,hgleadercw@reuters.com,Male,180.110.216.125 +466,Odo,Dullingham,odullinghamcx@altervista.org,Male,41.238.252.187 +467,Ahmad,Skarin,askarincy@elpais.com,Male,25.246.185.113 +468,Ibby,Picheford,ipichefordcz@acquirethisname.com,Female,15.15.217.60 +469,Gannon,Dunderdale,gdunderdaled0@prlog.org,Male,230.187.147.6 +470,Gayelord,Baptist,gbaptistd1@oaic.gov.au,Male,231.228.215.121 +471,Tilly,Caslett,tcaslettd2@networksolutions.com,Female,57.156.166.188 +472,Raimondo,Hallard,rhallardd3@seesaa.net,Male,252.123.165.32 +473,Robina,Iddison,riddisond4@who.int,Female,175.4.200.106 +474,Kassey,Geddes,kgeddesd5@privacy.gov.au,Female,175.71.132.102 +475,Thane,Salerno,tsalernod6@cyberchimps.com,Male,169.238.70.119 +476,Gib,Milverton,gmilvertond7@free.fr,Male,240.89.144.238 +477,Clare,Hogbin,chogbind8@dion.ne.jp,Male,109.87.249.69 +478,Monte,Cahillane,mcahillaned9@dailymail.co.uk,Male,13.100.100.8 +479,Babita,Hawtrey,bhawtreyda@wix.com,Female,94.81.114.4 +480,Horatius,Shovel,hshoveldb@huffingtonpost.com,Male,62.133.39.49 +481,Vanda,Mullin,vmullindc@prweb.com,Female,91.226.82.241 +482,Gottfried,Sarsfield,gsarsfielddd@theglobeandmail.com,Male,73.241.39.210 +483,Georgetta,Fitzer,gfitzerde@jigsy.com,Female,97.62.187.41 +484,Carce,Purkiss,cpurkissdf@google.pl,Male,239.32.126.71 +485,Norine,Ornelas,nornelasdg@icq.com,Female,60.132.176.16 +486,Lorens,Dalyell,ldalyelldh@about.me,Male,241.249.4.32 +487,Elsey,Sute,esutedi@themeforest.net,Female,72.38.231.225 +488,Dasha,Skellion,dskelliondj@google.fr,Female,51.129.20.177 +489,Zita,Hirsch,zhirschdk@mozilla.com,Female,180.92.124.124 +490,Minna,Ughelli,mughellidl@odnoklassniki.ru,Female,82.20.35.233 +491,Giffer,Van Bruggen,gvanbruggendm@msn.com,Male,153.52.4.24 +492,Anastasia,Raymond,araymonddn@time.com,Female,233.77.66.64 +493,Steven,Breens,sbreensdo@illinois.edu,Male,13.76.215.132 +494,Weylin,Drage,wdragedp@vimeo.com,Male,68.219.198.85 +495,Darleen,Mursell,dmurselldq@narod.ru,Female,213.30.144.136 +496,Faustine,Gilson,fgilsondr@prweb.com,Female,45.124.220.120 +497,Leonerd,Fordy,lfordyds@technorati.com,Male,96.89.243.1 +498,Fabe,Bruffell,fbruffelldt@sakura.ne.jp,Male,175.175.211.95 +499,Louie,Daglish,ldaglishdu@paypal.com,Male,58.82.108.92 +500,Farris,Dominiak,fdominiakdv@usgs.gov,Male,189.166.35.5 +501,Llywellyn,Pudding,lpuddingdw@mail.ru,Male,92.38.99.58 +502,Miller,Verdon,mverdondx@usgs.gov,Male,179.211.127.155 +503,Claiborne,Barnhart,cbarnhartdy@stanford.edu,Male,111.153.68.39 +504,Bastian,Brisbane,bbrisbanedz@pbs.org,Male,126.188.56.243 +505,Jone,Britten,jbrittene0@theguardian.com,Male,147.192.45.100 +506,Turner,Jacobowicz,tjacobowicze1@ehow.com,Male,56.60.148.64 +507,Leroi,Pirelli,lpirellie2@edublogs.org,Male,35.74.76.217 +508,Adela,Cawsby,acawsbye3@nih.gov,Female,31.13.152.182 +509,Bartlett,Capell,bcapelle4@hexun.com,Male,95.52.187.184 +510,Joseito,Brisley,jbrisleye5@prnewswire.com,Male,153.105.0.131 +511,Corilla,Slayford,cslayforde6@google.es,Female,79.29.49.45 +512,Trisha,Arrigo,tarrigoe7@zdnet.com,Female,179.112.108.109 +513,Chane,Henaughan,chenaughane8@blogs.com,Male,222.190.45.240 +514,Amandi,Mathivat,amathivate9@netvibes.com,Female,223.211.226.11 +515,Ase,Eagland,aeaglandea@histats.com,Male,24.8.12.175 +516,Aldon,Snipe,asnipeeb@google.ca,Male,185.63.54.5 +517,Laura,Been,lbeenec@parallels.com,Female,116.244.0.23 +518,Nero,Fairchild,nfairchilded@nhs.uk,Male,34.125.184.7 +519,Brandice,Winkell,bwinkellee@mysql.com,Female,43.88.102.230 +520,Trixie,De Ambrosi,tdeambrosief@cbslocal.com,Female,225.196.5.10 +521,Meredith,Emma,memmaeg@hao123.com,Female,27.23.167.47 +522,Ernst,Stanborough,estanborougheh@ucla.edu,Male,140.174.27.225 +523,Jody,Swadon,jswadonei@newsvine.com,Male,246.14.93.233 +524,Korney,Yallowley,kyallowleyej@infoseek.co.jp,Female,128.249.218.126 +525,Barth,Jarley,bjarleyek@netvibes.com,Male,138.47.114.206 +526,Dione,Rolingson,drolingsonel@purevolume.com,Female,137.125.83.223 +527,Lyndell,Wheatley,lwheatleyem@slashdot.org,Female,35.165.45.197 +528,Emalia,Batrim,ebatrimen@wikipedia.org,Female,205.182.83.84 +529,Chrisse,Rae,craeeo@google.com,Male,37.109.131.54 +530,Sandra,Bussetti,sbussettiep@free.fr,Female,111.14.33.212 +531,Oates,De La Cote,odelacoteeq@bing.com,Male,185.106.178.10 +532,Cathie,Cammis,ccammiser@w3.org,Female,179.70.255.247 +533,Ned,Osman,nosmanes@deliciousdays.com,Male,46.189.166.13 +534,Broddy,Vasyutichev,bvasyutichevet@nbcnews.com,Male,104.242.32.146 +535,Sisely,Attril,sattrileu@sitemeter.com,Female,127.188.209.140 +536,Hamlen,Ewings,hewingsev@photobucket.com,Male,103.221.140.77 +537,Rayner,Goly,rgolyew@facebook.com,Male,8.10.109.34 +538,Chrotoem,Peal,cpealex@nydailynews.com,Male,206.0.137.115 +539,Fraze,Delmonti,fdelmontiey@domainmarket.com,Male,127.42.42.242 +540,Reggis,Lewzey,rlewzeyez@over-blog.com,Male,237.49.52.93 +541,Nicolea,Arington,naringtonf0@usnews.com,Female,73.14.82.104 +542,Raffarty,Grote,rgrotef1@indiegogo.com,Male,77.169.19.177 +543,Zacherie,Upham,zuphamf2@yale.edu,Male,219.192.56.247 +544,Ulises,Fingleton,ufingletonf3@epa.gov,Male,166.135.91.75 +545,Wanda,Jeffress,wjeffressf4@comcast.net,Female,201.90.188.38 +546,Yule,Cartmail,ycartmailf5@etsy.com,Male,101.253.156.221 +547,Jaymie,Widdup,jwiddupf6@list-manage.com,Male,46.173.94.75 +548,Jan,Killough,jkilloughf7@icio.us,Male,72.245.81.216 +549,Emilee,Purrier,epurrierf8@businessweek.com,Female,143.195.159.166 +550,Rawley,Saywell,rsaywellf9@google.com.br,Male,117.115.148.141 +551,Noreen,Yarnold,nyarnoldfa@gnu.org,Female,88.86.173.37 +552,Lockwood,Cosley,lcosleyfb@telegraph.co.uk,Male,113.54.70.3 +553,Sharity,Akram,sakramfc@sogou.com,Female,5.162.10.24 +554,Antoine,Petchey,apetcheyfd@networksolutions.com,Male,15.108.177.167 +555,Siffre,Leas,sleasfe@alexa.com,Male,223.88.61.199 +556,Garrek,Dugan,gduganff@parallels.com,Male,97.247.8.79 +557,Yelena,Revett,yrevettfg@weibo.com,Female,29.17.229.199 +558,Meta,Prene,mprenefh@hibu.com,Female,96.143.33.118 +559,Marielle,Suarez,msuarezfi@yellowpages.com,Female,253.117.6.46 +560,Constancy,Drohun,cdrohunfj@addthis.com,Female,81.145.233.7 +561,Rebekah,Isacq,risacqfk@alexa.com,Female,244.5.168.241 +562,Ellwood,Farrans,efarransfl@cyberchimps.com,Male,81.139.192.64 +563,Lucius,Putten,lputtenfm@meetup.com,Male,166.134.24.120 +564,Sherwin,Palmar,spalmarfn@ted.com,Male,1.144.70.25 +565,Oralla,McVitie,omcvitiefo@seesaa.net,Female,152.237.88.29 +566,Stacey,Kitt,skittfp@apache.org,Female,11.210.144.28 +567,Evanne,Robelin,erobelinfq@wired.com,Female,92.163.232.63 +568,Perry,Denerley,pdenerleyfr@arstechnica.com,Male,240.65.121.244 +569,Stewart,Albrooke,salbrookefs@feedburner.com,Male,199.120.213.204 +570,Melamie,Geering,mgeeringft@fastcompany.com,Female,249.45.150.80 +571,Nikolas,Errigo,nerrigofu@ow.ly,Male,231.5.97.88 +572,Hendrik,Bruntjen,hbruntjenfv@theatlantic.com,Male,193.198.251.8 +573,Keven,Runnett,krunnettfw@free.fr,Male,174.205.254.250 +574,Francoise,Dowell,fdowellfx@360.cn,Female,65.28.78.61 +575,Alisun,Vanne,avannefy@economist.com,Female,94.202.252.106 +576,Rea,Cauldfield,rcauldfieldfz@symantec.com,Female,109.168.44.165 +577,Genny,Bloomer,gbloomerg0@163.com,Female,51.110.159.200 +578,Waylen,Kennedy,wkennedyg1@netscape.com,Male,249.213.42.246 +579,Miles,MacIlraith,mmacilraithg2@paginegialle.it,Male,198.102.222.103 +580,Janek,Olyet,jolyetg3@nature.com,Male,4.61.51.126 +581,Wyn,Hinkes,whinkesg4@hud.gov,Male,161.184.62.235 +582,Mariana,Twell,mtwellg5@arstechnica.com,Female,166.227.171.231 +583,Yetty,Gillmore,ygillmoreg6@multiply.com,Female,200.151.218.63 +584,Michele,Robbel,mrobbelg7@youku.com,Male,178.166.208.218 +585,Hilda,Pidgen,hpidgeng8@statcounter.com,Female,32.199.237.25 +586,Blanche,Teck,bteckg9@about.me,Female,78.146.179.125 +587,Lek,Halt,lhaltga@yale.edu,Male,162.231.254.241 +588,Angelle,Larwell,alarwellgb@vk.com,Female,203.215.102.174 +589,Calvin,Flewett,cflewettgc@nih.gov,Male,215.75.205.148 +590,Murial,Crowdson,mcrowdsongd@google.co.jp,Female,49.252.208.109 +591,Madella,Ivanuschka,mivanuschkage@mysql.com,Female,170.24.246.85 +592,Lorens,Mostyn,lmostyngf@liveinternet.ru,Male,62.59.149.90 +593,Samuele,Fowlie,sfowliegg@digg.com,Male,141.84.124.206 +594,Doll,Durdle,ddurdlegh@cisco.com,Female,50.207.47.81 +595,Alfie,Bezzant,abezzantgi@ucoz.com,Male,119.27.255.230 +596,Kakalina,Jessep,kjessepgj@angelfire.com,Female,58.97.231.227 +597,Dinny,Faull,dfaullgk@hp.com,Female,171.60.254.195 +598,Allis,Buxsey,abuxseygl@apple.com,Female,121.143.58.66 +599,Maggy,Briars,mbriarsgm@github.io,Female,203.127.93.134 +600,Candis,Peal,cpealgn@surveymonkey.com,Female,20.114.27.15 +601,Jody,De Ruggiero,jderuggierogo@clickbank.net,Female,3.198.182.89 +602,Padriac,Simester,psimestergp@miitbeian.gov.cn,Male,91.245.182.134 +603,Stanwood,Huckel,shuckelgq@drupal.org,Male,233.106.164.1 +604,Rosabel,Magarrell,rmagarrellgr@bing.com,Female,73.128.21.86 +605,Silvano,McKim,smckimgs@wikia.com,Male,2.130.218.160 +606,Lin,Guerreau,lguerreaugt@samsung.com,Female,74.166.120.151 +607,Charil,Gatecliff,cgatecliffgu@myspace.com,Female,86.71.35.201 +608,Jake,Stood,jstoodgv@discuz.net,Male,193.144.240.92 +609,Charil,Kneale,cknealegw@dell.com,Female,183.34.172.215 +610,Glen,Dirkin,gdirkingx@simplemachines.org,Female,206.76.229.103 +611,Trey,Warsop,twarsopgy@microsoft.com,Male,154.170.22.153 +612,Dru,Butte,dbuttegz@yelp.com,Male,78.175.116.121 +613,Danyette,Antognozzii,dantognozziih0@cafepress.com,Female,182.233.80.35 +614,Norbie,Farnish,nfarnishh1@amazon.com,Male,182.80.54.109 +615,Giulia,Olohan,golohanh2@sfgate.com,Female,164.74.142.186 +616,Clemmie,Hixley,chixleyh3@liveinternet.ru,Female,222.145.197.69 +617,Kristian,Penning,kpenningh4@topsy.com,Male,16.100.146.4 +618,Lynne,Fessions,lfessionsh5@buzzfeed.com,Female,126.152.40.70 +619,Freddi,Mayor,fmayorh6@t.co,Female,238.241.67.62 +620,Archibold,Grinikhinov,agrinikhinovh7@smugmug.com,Male,236.217.81.15 +621,Darn,Lipp,dlipph8@mediafire.com,Male,12.78.206.122 +622,Tish,Reyne,treyneh9@yelp.com,Female,6.184.21.159 +623,Barbi,Heathcoat,bheathcoatha@simplemachines.org,Female,68.58.119.16 +624,Elset,De Mattei,edematteihb@engadget.com,Female,227.113.241.48 +625,Shalna,Bydaway,sbydawayhc@census.gov,Female,233.124.1.80 +626,Laurens,McGeaney,lmcgeaneyhd@ed.gov,Male,141.202.22.158 +627,Glory,Haley,ghaleyhe@bloomberg.com,Female,81.92.92.218 +628,Jermayne,Andree,jandreehf@opera.com,Male,209.177.19.250 +629,Jacquenette,Clemintoni,jclemintonihg@list-manage.com,Female,238.217.63.137 +630,Matthus,MacKowle,mmackowlehh@pcworld.com,Male,95.120.52.59 +631,Ronni,Mishow,rmishowhi@tumblr.com,Female,226.0.240.163 +632,Nataniel,Ravel,nravelhj@simplemachines.org,Male,18.106.206.140 +633,Liesa,Lively,llivelyhk@example.com,Female,34.112.42.13 +634,Rob,Keelin,rkeelinhl@yale.edu,Male,1.50.201.60 +635,Marj,Giottoi,mgiottoihm@nbcnews.com,Female,134.233.30.49 +636,My,Coldman,mcoldmanhn@unicef.org,Male,4.195.217.165 +637,Fields,Issacoff,fissacoffho@sakura.ne.jp,Male,26.120.7.171 +638,Harris,Stilwell,hstilwellhp@issuu.com,Male,8.228.226.79 +639,Toma,Trusler,ttruslerhq@miibeian.gov.cn,Female,205.192.108.193 +640,Guillemette,Salzberger,gsalzbergerhr@businessweek.com,Female,236.33.225.248 +641,Sol,Southern,ssouthernhs@discuz.net,Male,180.79.207.102 +642,Zonnya,Janway,zjanwayht@tinyurl.com,Female,183.29.111.39 +643,Finlay,Ilyinski,filyinskihu@msu.edu,Male,163.146.215.249 +644,Zilvia,McKernan,zmckernanhv@ed.gov,Female,175.120.227.241 +645,Lyn,MacCook,lmaccookhw@xrea.com,Male,251.29.217.85 +646,Tomasina,Roglieri,troglierihx@shareasale.com,Female,54.217.236.125 +647,Lyndy,Grimes,lgrimeshy@oakley.com,Female,176.10.128.2 +648,Adrian,Dorrins,adorrinshz@wikispaces.com,Male,212.123.49.163 +649,Barbara-anne,Christophersen,bchristopherseni0@nifty.com,Female,38.171.124.239 +650,Thatch,Blackie,tblackiei1@about.me,Male,217.153.106.124 +651,Guido,Ribbens,gribbensi2@indiegogo.com,Male,31.230.145.28 +652,Valentine,Levesque,vlevesquei3@fastcompany.com,Female,69.243.195.145 +653,Shannan,Pinor,spinori4@salon.com,Male,212.194.152.87 +654,Almeda,Christaeas,achristaeasi5@house.gov,Female,41.210.0.253 +655,Obed,Acres,oacresi6@amazon.co.jp,Male,214.237.172.71 +656,Forest,Esmond,fesmondi7@shinystat.com,Male,124.151.181.215 +657,Tedmund,Stoffer,tstofferi8@altervista.org,Male,125.131.214.250 +658,Cammy,Doniso,cdonisoi9@dedecms.com,Female,123.255.105.2 +659,Lidia,Spink,lspinkia@discuz.net,Female,51.240.111.227 +660,Arlee,Philippsohn,aphilippsohnib@live.com,Female,92.181.51.252 +661,Lisa,Mulliss,lmullissic@npr.org,Female,221.113.142.109 +662,Tiffie,Daye,tdayeid@wp.com,Female,0.191.183.204 +663,Valaria,Ondrich,vondrichie@nba.com,Female,174.175.59.85 +664,Sherlocke,Blanshard,sblanshardif@ft.com,Male,93.213.222.43 +665,Natty,Riddel,nriddelig@webmd.com,Female,219.26.234.192 +666,Trina,Friett,tfriettih@princeton.edu,Female,112.37.91.166 +667,Dew,Thrower,dthrowerii@ca.gov,Male,251.214.217.0 +668,Rooney,Wonfar,rwonfarij@telegraph.co.uk,Male,240.45.104.81 +669,Claus,McGavigan,cmcgaviganik@mapquest.com,Male,188.30.153.46 +670,Tedra,Bilam,tbilamil@amazonaws.com,Female,94.50.58.113 +671,Alexine,Teare,ateareim@netvibes.com,Female,65.28.96.128 +672,Korie,Kingzet,kkingzetin@youtu.be,Female,181.251.109.169 +673,Quintus,Scare,qscareio@homestead.com,Male,196.229.255.105 +674,Reinald,Andriolli,randriolliip@tripod.com,Male,141.35.146.0 +675,Sanders,Pinnere,spinnereiq@t.co,Male,90.63.85.128 +676,Jobie,Good,jgoodir@networkadvertising.org,Female,187.249.69.70 +677,Sibley,Gleader,sgleaderis@instagram.com,Female,131.243.239.31 +678,Maureene,Rotchell,mrotchellit@usa.gov,Female,92.161.174.24 +679,Sapphire,Bertomier,sbertomieriu@sciencedirect.com,Female,208.181.84.129 +680,Ignaz,Tack,itackiv@skyrock.com,Male,127.250.1.89 +681,Casandra,Olivetta,colivettaiw@elegantthemes.com,Female,158.218.230.146 +682,Mikol,Futty,mfuttyix@yolasite.com,Male,13.225.103.150 +683,Murielle,Cellone,mcelloneiy@photobucket.com,Female,102.134.77.151 +684,Angelika,Olford,aolfordiz@sfgate.com,Female,245.211.218.214 +685,Page,Slarke,pslarkej0@imageshack.us,Female,63.40.89.7 +686,Cicely,Pateman,cpatemanj1@facebook.com,Female,115.76.10.199 +687,Cecilio,Flett,cflettj2@yandex.ru,Male,11.114.213.218 +688,Ramon,Phythian,rphythianj3@quantcast.com,Male,88.97.101.46 +689,Karlen,Getley,kgetleyj4@nytimes.com,Female,41.230.126.245 +690,Elfrieda,Oby,eobyj5@google.pl,Female,73.132.146.196 +691,Myrwyn,Fincham,mfinchamj6@macromedia.com,Male,90.42.241.74 +692,Brynn,Beaumont,bbeaumontj7@php.net,Female,201.187.126.71 +693,Reeva,Gino,rginoj8@homestead.com,Female,135.205.170.21 +694,Emerson,Scopham,escophamj9@amazon.de,Male,87.173.161.24 +695,Anabella,Franceschielli,afranceschiellija@cpanel.net,Female,237.150.94.49 +696,Arvin,Cordell,acordelljb@hubpages.com,Male,214.197.61.230 +697,Saree,Malloy,smalloyjc@cyberchimps.com,Female,145.64.214.152 +698,Tomkin,Krystof,tkrystofjd@buzzfeed.com,Male,30.115.211.55 +699,Ulrick,Haney,uhaneyje@home.pl,Male,26.25.143.18 +700,Maire,Ferrettino,mferrettinojf@cloudflare.com,Female,188.196.227.89 +701,Ezequiel,MacKeeg,emackeegjg@imdb.com,Male,193.213.121.231 +702,Ranee,Moorman,rmoormanjh@lulu.com,Female,214.159.203.197 +703,Everett,Schuchmacher,eschuchmacherji@weebly.com,Male,110.171.213.219 +704,Kiley,Olver,kolverjj@themeforest.net,Male,125.238.29.251 +705,Derwin,Pantlin,dpantlinjk@yellowbook.com,Male,141.146.202.138 +706,Kerry,Ritmeier,kritmeierjl@virginia.edu,Female,174.140.167.167 +707,Mariya,Sibly,msiblyjm@loc.gov,Female,187.243.66.53 +708,Bride,Murdie,bmurdiejn@wsj.com,Female,146.185.144.50 +709,Page,Sneezum,psneezumjo@prlog.org,Male,141.11.98.207 +710,Luca,Treace,ltreacejp@china.com.cn,Male,103.244.92.149 +711,Halsey,Kunat,hkunatjq@go.com,Male,15.11.133.144 +712,Annadiane,Corr,acorrjr@howstuffworks.com,Female,240.145.174.220 +713,Aloysia,Dodgson,adodgsonjs@ocn.ne.jp,Female,58.105.120.173 +714,Appolonia,Maiklem,amaiklemjt@ed.gov,Female,94.204.182.144 +715,Jackie,O'Clery,jocleryju@theglobeandmail.com,Male,80.176.193.52 +716,Holly-anne,Spracklin,hspracklinjv@printfriendly.com,Female,51.133.168.41 +717,Addia,Thor,athorjw@guardian.co.uk,Female,199.30.38.142 +718,Lucho,Blemen,lblemenjx@creativecommons.org,Male,146.213.119.232 +719,Sherwynd,Geale,sgealejy@timesonline.co.uk,Male,166.223.98.40 +720,Cesare,Barkus,cbarkusjz@admin.ch,Male,7.29.131.54 +721,Crissie,Sherewood,csherewoodk0@usatoday.com,Female,190.179.6.248 +722,Julia,Bridgman,jbridgmank1@surveymonkey.com,Female,154.70.251.109 +723,Fanchon,Kefford,fkeffordk2@google.it,Female,189.105.95.143 +724,Towney,Guerriero,tguerrierok3@netscape.com,Male,178.200.183.195 +725,Guillemette,Mintoff,gmintoffk4@auda.org.au,Female,188.156.36.187 +726,Natalina,Mariot,nmariotk5@epa.gov,Female,44.19.50.225 +727,Kimberli,Elfleet,kelfleetk6@latimes.com,Female,9.154.168.182 +728,Bing,Pitkin,bpitkink7@wordpress.org,Male,0.96.238.140 +729,Lulu,Spandley,lspandleyk8@ft.com,Female,249.15.27.80 +730,Sigfried,Getley,sgetleyk9@google.co.jp,Male,18.114.191.173 +731,Aldous,Barratt,abarrattka@si.edu,Male,110.22.152.197 +732,Gannie,Bonome,gbonomekb@mapy.cz,Male,130.230.201.28 +733,Mortimer,Gobeaux,mgobeauxkc@photobucket.com,Male,253.99.83.78 +734,Eimile,Lafferty,elaffertykd@homestead.com,Female,165.0.18.195 +735,Creight,Dryburgh,cdryburghke@hao123.com,Male,250.213.10.12 +736,Brittany,Reilly,breillykf@jalbum.net,Female,10.68.132.212 +737,Adolph,MacDaid,amacdaidkg@discovery.com,Male,117.100.88.116 +738,Annabel,Matthisson,amatthissonkh@cpanel.net,Female,164.178.223.2 +739,Neddie,Fabry,nfabryki@nbcnews.com,Male,230.111.45.123 +740,Ardeen,Gurery,agurerykj@un.org,Female,83.163.3.183 +741,Granville,Broseman,gbrosemankk@cornell.edu,Male,137.153.71.88 +742,Sydney,Filippone,sfilipponekl@theatlantic.com,Female,68.146.143.17 +743,Ozzy,Cullen,ocullenkm@stanford.edu,Male,250.134.171.153 +744,Lilias,Oneile,loneilekn@w3.org,Female,251.26.106.34 +745,Rouvin,Borgesio,rborgesioko@dion.ne.jp,Male,197.20.218.21 +746,Mitchell,Doulden,mdouldenkp@cdbaby.com,Male,29.173.35.108 +747,Krista,De Michele,kdemichelekq@paginegialle.it,Female,122.148.5.36 +748,Marys,Jillings,mjillingskr@diigo.com,Female,254.249.145.100 +749,Kenyon,Klauber,kklauberks@i2i.jp,Male,109.4.189.98 +750,James,O'Corrane,jocorranekt@umn.edu,Male,40.180.214.74 +751,Kelbee,Hutcheon,khutcheonku@huffingtonpost.com,Male,155.80.155.235 +752,Egbert,Armatage,earmatagekv@statcounter.com,Male,121.138.241.138 +753,Parnell,Presshaugh,ppresshaughkw@a8.net,Male,115.210.93.210 +754,Kasper,Carley,kcarleykx@hatena.ne.jp,Male,239.211.122.193 +755,Demeter,Hovey,dhoveyky@wix.com,Female,198.92.17.197 +756,Jarid,Guisby,jguisbykz@paginegialle.it,Male,83.110.71.162 +757,Tuck,Baistow,tbaistowl0@php.net,Male,168.27.214.117 +758,Reese,Sparkwill,rsparkwilll1@go.com,Male,245.39.96.248 +759,Avie,Crimpe,acrimpel2@ovh.net,Female,247.32.54.190 +760,Minnnie,Sellan,msellanl3@tinypic.com,Female,82.77.141.97 +761,Kenton,Prantoni,kprantonil4@prlog.org,Male,120.57.88.59 +762,Maurits,Longland,mlonglandl5@miibeian.gov.cn,Male,245.183.12.170 +763,Ginelle,Slarke,gslarkel6@paypal.com,Female,201.238.22.13 +764,Aila,Brugh,abrughl7@surveymonkey.com,Female,234.80.204.218 +765,Cirstoforo,Petruska,cpetruskal8@cbsnews.com,Male,151.29.107.38 +766,Catie,Kingett,ckingettl9@csmonitor.com,Female,248.237.179.11 +767,Tannie,Swannell,tswannellla@cnet.com,Male,27.53.207.17 +768,Kristos,Woodham,kwoodhamlb@drupal.org,Male,71.82.54.5 +769,Ches,Purseglove,cpurseglovelc@google.nl,Male,61.217.171.33 +770,Ezekiel,Goodlip,egoodlipld@netvibes.com,Male,250.157.183.121 +771,Heath,Coles,hcolesle@globo.com,Male,99.70.75.146 +772,Farah,Whitchurch,fwhitchurchlf@tripod.com,Female,152.194.70.234 +773,Gibbie,Longcake,glongcakelg@geocities.jp,Male,174.177.61.186 +774,Jayson,Waring,jwaringlh@stanford.edu,Male,194.76.168.228 +775,Rooney,Spofford,rspoffordli@purevolume.com,Male,226.217.10.11 +776,Martynne,Pauleit,mpauleitlj@ebay.co.uk,Female,4.23.163.101 +777,Cazzie,Franciottoi,cfranciottoilk@ehow.com,Male,174.3.3.12 +778,Kelby,Wayon,kwayonll@cbsnews.com,Male,225.32.112.20 +779,Verena,Grafton,vgraftonlm@edublogs.org,Female,135.28.208.74 +780,Dav,Di Pietro,ddipietroln@msn.com,Male,31.23.105.47 +781,Minette,Bannister,mbannisterlo@reuters.com,Female,118.188.113.204 +782,Virge,Tremmil,vtremmillp@adobe.com,Male,123.195.101.72 +783,Emlynn,Nice,enicelq@scientificamerican.com,Female,7.235.161.162 +784,Sharity,Moulden,smouldenlr@amazon.co.jp,Female,125.208.196.79 +785,Sibley,Salmon,ssalmonls@unblog.fr,Female,79.134.76.164 +786,Willey,Bilton,wbiltonlt@twitter.com,Male,54.154.6.157 +787,Konstanze,Forrington,kforringtonlu@netlog.com,Female,255.253.166.112 +788,Minerva,Carnier,mcarnierlv@paginegialle.it,Female,167.195.5.15 +789,Kathlin,Aslett,kaslettlw@vimeo.com,Female,176.26.20.15 +790,Fletcher,Kramer,fkramerlx@artisteer.com,Male,121.109.179.59 +791,Zorana,Pigny,zpignyly@free.fr,Female,86.81.96.224 +792,Normie,Dumbar,ndumbarlz@biblegateway.com,Male,110.42.110.219 +793,Minetta,Boulden,mbouldenm0@symantec.com,Female,58.84.94.170 +794,Thaine,Flippen,tflippenm1@pcworld.com,Male,210.0.70.59 +795,Kippar,Matthewson,kmatthewsonm2@linkedin.com,Male,121.248.102.171 +796,Elsa,Hurdwell,ehurdwellm3@addthis.com,Female,112.137.213.128 +797,Chickie,Bluschke,cbluschkem4@virginia.edu,Male,69.15.111.191 +798,Derick,Claus,dclausm5@typepad.com,Male,70.195.165.37 +799,Laurie,Torrecilla,ltorrecillam6@exblog.jp,Female,49.38.170.252 +800,Antoinette,Essame,aessamem7@amazon.co.uk,Female,129.99.198.185 +801,Mano,Patching,mpatchingm8@soup.io,Male,175.105.153.239 +802,Maritsa,O'Glessane,moglessanem9@reference.com,Female,231.82.216.32 +803,Giraud,Strattan,gstrattanma@hugedomains.com,Male,179.84.57.182 +804,Darda,Benson,dbensonmb@digg.com,Female,214.199.249.8 +805,Michaela,Few,mfewmc@wiley.com,Female,15.116.188.227 +806,Nye,Keggins,nkegginsmd@jugem.jp,Male,93.91.234.130 +807,Robinet,Woolstenholmes,rwoolstenholmesme@netvibes.com,Male,72.126.231.143 +808,Sandy,MacKegg,smackeggmf@unblog.fr,Male,120.89.134.163 +809,Jory,Argue,jarguemg@ocn.ne.jp,Male,219.15.103.91 +810,Nance,Birkmyr,nbirkmyrmh@umich.edu,Female,18.232.251.117 +811,Davey,Aylott,daylottmi@springer.com,Male,96.239.8.204 +812,Tabb,Breslane,tbreslanemj@wikimedia.org,Male,114.43.107.76 +813,Miof mela,Sautter,msauttermk@bing.com,Female,233.35.28.104 +814,Sibylle,Kenion,skenionml@bloglines.com,Female,97.142.64.81 +815,Mead,Guiness,mguinessmm@usgs.gov,Male,209.198.95.250 +816,Shelley,Garrold,sgarroldmn@xrea.com,Female,229.65.196.100 +817,Philippa,Greschke,pgreschkemo@printfriendly.com,Female,52.142.51.204 +818,Conrado,Quarles,cquarlesmp@cafepress.com,Male,145.53.164.223 +819,Fernando,Bastone,fbastonemq@google.com.hk,Male,244.58.155.220 +820,Catarina,Gateland,cgatelandmr@bigcartel.com,Female,111.136.172.211 +821,Hildagard,Strang,hstrangms@chronoengine.com,Female,59.79.229.90 +822,Yard,Leeburne,yleeburnemt@shinystat.com,Male,216.202.244.118 +823,Iago,Trevna,itrevnamu@tiny.cc,Male,52.114.181.101 +824,Kaiser,Tomaszczyk,ktomaszczykmv@wiley.com,Male,50.236.219.11 +825,Cornelius,Plaid,cplaidmw@simplemachines.org,Male,145.123.116.66 +826,Mathias,Kearney,mkearneymx@w3.org,Male,140.29.96.70 +827,Aile,Thor,athormy@shinystat.com,Female,28.52.59.129 +828,Alica,Medd,ameddmz@google.it,Female,195.210.251.253 +829,Jess,Varga,jvargan0@npr.org,Female,172.9.103.242 +830,Eloisa,Tonsley,etonsleyn1@umn.edu,Female,32.210.12.140 +831,Dewitt,Sinott,dsinottn2@china.com.cn,Male,133.255.194.137 +832,Wildon,Humpherston,whumpherstonn3@cam.ac.uk,Male,231.78.127.146 +833,Kat,Ajean,kajeann4@weather.com,Female,226.27.164.156 +834,Monica,Mamwell,mmamwelln5@aboutads.info,Female,63.80.239.65 +835,Ryley,Rawet,rrawetn6@de.vu,Male,112.153.171.45 +836,Preston,Raymen,praymenn7@blogs.com,Male,198.86.241.216 +837,Milicent,Wass,mwassn8@exblog.jp,Female,198.186.122.199 +838,Margo,Treace,mtreacen9@huffingtonpost.com,Female,140.254.126.100 +839,Sophey,Occleshaw,soccleshawna@dell.com,Female,230.248.241.137 +840,Olly,Newlove,onewlovenb@webmd.com,Male,70.31.76.32 +841,Indira,Nosworthy,inosworthync@paypal.com,Female,73.167.233.115 +842,Beckie,Rosenfelt,brosenfeltnd@soundcloud.com,Female,52.39.100.181 +843,Brigitte,Manville,bmanvillene@chron.com,Female,49.36.102.30 +844,Perri,Gilley,pgilleynf@artisteer.com,Female,241.236.76.3 +845,Erl,Fearns,efearnsng@mail.ru,Male,175.244.94.120 +846,Udell,Daysh,udayshnh@hp.com,Male,64.167.94.43 +847,Dru,Fayne,dfayneni@addthis.com,Female,231.90.186.81 +848,Chrissy,Adamski,cadamskinj@google.pl,Male,234.168.45.59 +849,Caryl,Wildbore,cwildborenk@nhs.uk,Male,88.10.183.196 +850,Bobinette,Kausche,bkauschenl@nifty.com,Female,159.154.34.191 +851,Edvard,Guidera,eguideranm@chron.com,Male,121.100.40.2 +852,Carlina,Bette,cbettenn@tripadvisor.com,Female,217.146.48.219 +853,Ario,Iredell,airedellno@statcounter.com,Male,181.148.2.186 +854,Sibby,Whitemarsh,swhitemarshnp@cisco.com,Female,199.102.179.153 +855,Edwin,Maldin,emaldinnq@google.ru,Male,244.250.212.141 +856,Aurelea,Frowde,afrowdenr@aol.com,Female,92.62.31.105 +857,Claire,Dunbobin,cdunbobinns@tripod.com,Male,191.118.197.181 +858,Linn,Battison,lbattisonnt@zdnet.com,Male,13.130.189.250 +859,Mathilde,Torre,mtorrenu@rakuten.co.jp,Female,124.20.176.81 +860,Tucker,Torrecilla,ttorrecillanv@oaic.gov.au,Male,121.225.220.9 +861,Lawrence,Lukins,llukinsnw@chron.com,Male,192.43.20.66 +862,Roma,Cornes,rcornesnx@deliciousdays.com,Male,27.103.218.199 +863,Yasmeen,Suddell,ysuddellny@eepurl.com,Female,91.46.129.14 +864,Erich,Trotton,etrottonnz@fema.gov,Male,72.120.135.139 +865,Matthieu,Dummer,mdummero0@bbc.co.uk,Male,54.237.165.11 +866,Law,Sonschein,lsonscheino1@newyorker.com,Male,84.162.50.121 +867,Hillery,Pickrill,hpickrillo2@redcross.org,Male,104.175.195.87 +868,Vida,Gerdts,vgerdtso3@usda.gov,Female,55.100.34.156 +869,Merci,Shills,mshillso4@paypal.com,Female,204.211.155.105 +870,Gerti,Roelofs,groelofso5@freewebs.com,Female,140.90.26.18 +871,Bron,Gowthorpe,bgowthorpeo6@seattletimes.com,Male,171.128.211.79 +872,Serge,Stoving,sstovingo7@slashdot.org,Male,31.56.241.173 +873,Con,MacQuarrie,cmacquarrieo8@godaddy.com,Female,63.175.232.195 +874,Eleanora,Westrey,ewestreyo9@sohu.com,Female,223.63.10.215 +875,Griffin,McGorman,gmcgormanoa@chronoengine.com,Male,215.143.213.66 +876,Corinna,Goodred,cgoodredob@livejournal.com,Female,133.194.126.111 +877,Wanda,De Blasio,wdeblasiooc@symantec.com,Female,236.31.80.2 +878,Rita,MacPake,rmacpakeod@wunderground.com,Female,184.130.125.175 +879,Brandise,Yakovich,byakovichoe@omniture.com,Female,61.134.36.47 +880,Garey,Daviot,gdaviotof@patch.com,Male,142.84.194.115 +881,Leonardo,Kedge,lkedgeog@google.de,Male,192.161.142.160 +882,Torry,Coppins,tcoppinsoh@rambler.ru,Male,28.95.151.107 +883,Fraser,Flaxon,fflaxonoi@xing.com,Male,57.246.162.70 +884,Leland,Mossdale,lmossdaleoj@google.it,Male,236.211.28.233 +885,Mariska,MacDonagh,mmacdonaghok@51.la,Female,197.110.233.153 +886,Ingmar,Shekle,ishekleol@flickr.com,Male,242.91.93.151 +887,Elvis,Flitcroft,eflitcroftom@cyberchimps.com,Male,137.145.97.253 +888,Dermot,Sherrin,dsherrinon@topsy.com,Male,205.128.33.101 +889,Mathian,Grabb,mgrabboo@epa.gov,Male,135.194.83.25 +890,Woodman,Sherbrooke,wsherbrookeop@globo.com,Male,222.3.71.153 +891,Gorden,Keward,gkewardoq@aol.com,Male,209.133.35.15 +892,Biddy,Gravenall,bgravenallor@springer.com,Female,122.224.72.180 +893,Joseph,Maguire,jmaguireos@globo.com,Male,90.242.201.67 +894,Noelani,Royan,nroyanot@a8.net,Female,4.0.51.159 +895,Doll,Ayree,dayreeou@gnu.org,Female,159.133.204.144 +896,Marshal,Shubotham,mshubothamov@sourceforge.net,Male,252.153.223.95 +897,Quentin,Rachuig,qrachuigow@eventbrite.com,Male,208.11.132.28 +898,Tara,Lukes,tlukesox@list-manage.com,Female,33.237.168.176 +899,Eugenie,Parmer,eparmeroy@dmoz.org,Female,78.180.113.197 +900,Alejandra,Bassill,abassilloz@moonfruit.com,Female,196.84.48.0 +901,Florri,Scutt,fscuttp0@nymag.com,Female,6.231.88.105 +902,Ives,Janowski,ijanowskip1@mysql.com,Male,45.193.221.59 +903,Nelson,Endrizzi,nendrizzip2@arizona.edu,Male,82.96.176.57 +904,Tammara,Stobie,tstobiep3@scientificamerican.com,Female,7.113.54.217 +905,Tillie,De Bruijne,tdebruijnep4@springer.com,Female,54.221.169.69 +906,Teodoor,McGann,tmcgannp5@ucsd.edu,Male,75.29.153.66 +907,Roi,Tyce,rtycep6@ca.gov,Male,221.246.118.20 +908,Boote,Course,bcoursep7@hc360.com,Male,24.65.57.24 +909,Stacy,Hinchshaw,shinchshawp8@miibeian.gov.cn,Female,137.50.201.39 +910,Leeland,Joplin,ljoplinp9@1688.com,Male,159.27.188.131 +911,Herold,Craker,hcrakerpa@sohu.com,Male,102.92.205.187 +912,Jaquelin,Challen,jchallenpb@yale.edu,Female,166.227.170.139 +913,Dominick,Gaukroger,dgaukrogerpc@ocn.ne.jp,Male,239.26.212.40 +914,Rossy,Sneden,rsnedenpd@businesswire.com,Male,61.152.226.184 +915,Debbi,Dunnico,ddunnicope@yellowpages.com,Female,93.90.9.177 +916,Scotti,Shirlaw,sshirlawpf@timesonline.co.uk,Male,14.228.210.173 +917,Tomaso,Beausang,tbeausangpg@shareasale.com,Male,119.207.160.240 +918,Carley,Mullender,cmullenderph@mac.com,Female,97.65.91.185 +919,Malissa,Josephi,mjosephipi@pcworld.com,Female,1.173.125.195 +920,Marilyn,McGinnis,mmcginnispj@zdnet.com,Female,113.172.199.213 +921,Lewie,Scrammage,lscrammagepk@time.com,Male,131.185.212.27 +922,Aimil,Corkhill,acorkhillpl@jugem.jp,Female,47.107.160.53 +923,Tim,Lindermann,tlindermannpm@dyndns.org,Male,157.65.112.43 +924,Floria,Nern,fnernpn@xing.com,Female,237.172.48.238 +925,Demetris,Gillion,dgillionpo@google.com,Male,217.174.45.102 +926,Eada,Gullan,egullanpp@sina.com.cn,Female,109.134.105.160 +927,Matty,Costerd,mcosterdpq@yandex.ru,Female,101.74.50.15 +928,Denna,Castanos,dcastanospr@samsung.com,Female,98.201.149.237 +929,Tull,Cutler,tcutlerps@google.es,Male,34.194.212.59 +930,Odessa,Tinkham,otinkhampt@hibu.com,Female,211.200.106.43 +931,Artemus,Hellyer,ahellyerpu@cnbc.com,Male,212.135.128.46 +932,Morgan,Eastcott,meastcottpv@cornell.edu,Male,87.58.152.214 +933,Farlie,Yeats,fyeatspw@hp.com,Male,4.62.62.234 +934,Dawn,Ratnage,dratnagepx@storify.com,Female,149.66.34.20 +935,Doll,Heelis,dheelispy@tinyurl.com,Female,156.45.114.41 +936,Jewelle,Prentice,jprenticepz@mayoclinic.com,Female,222.187.165.0 +937,Shalne,Pimlock,spimlockq0@github.io,Female,151.91.67.64 +938,Brendon,Stickland,bsticklandq1@squidoo.com,Male,1.152.82.100 +939,Poppy,Turban,pturbanq2@ed.gov,Female,187.177.214.192 +940,Agata,Halkyard,ahalkyardq3@virginia.edu,Female,186.59.181.228 +941,Alfonso,Chislett,achislettq4@bravesites.com,Male,21.211.66.166 +942,Averil,Orht,aorhtq5@tmall.com,Female,152.167.162.127 +943,Loree,Pierrepoint,lpierrepointq6@google.ca,Female,69.172.202.230 +944,Bette,Holbury,bholburyq7@cloudflare.com,Female,25.162.226.147 +945,Huey,Medgewick,hmedgewickq8@jigsy.com,Male,188.193.217.94 +946,Liane,Brecher,lbrecherq9@engadget.com,Female,56.43.8.124 +947,Heather,Issatt,hissattqa@craigslist.org,Female,178.151.248.176 +948,Emmaline,Yggo,eyggoqb@deliciousdays.com,Female,102.189.205.47 +949,Mindy,Winterborne,mwinterborneqc@mashable.com,Female,231.191.52.169 +950,Conny,Petran,cpetranqd@skype.com,Male,181.15.178.206 +951,Mala,Waples,mwaplesqe@webnode.com,Female,168.191.86.236 +952,Preston,Mathwen,pmathwenqf@netlog.com,Male,178.231.226.158 +953,Nanette,Curtayne,ncurtayneqg@ucsd.edu,Female,71.144.9.113 +954,Pascale,Santon,psantonqh@addthis.com,Male,107.255.154.96 +955,Bea,Slad,bsladqi@latimes.com,Female,224.69.201.249 +956,Fredra,Whitlaw,fwhitlawqj@typepad.com,Female,14.84.131.193 +957,Parke,Reditt,predittqk@prlog.org,Male,218.210.153.103 +958,Aldon,Oakly,aoaklyql@google.co.uk,Male,236.117.50.133 +959,Rowen,Pechell,rpechellqm@hhs.gov,Male,212.38.79.29 +960,Mile,Fryman,mfrymanqn@symantec.com,Male,120.173.73.3 +961,Mayne,Duquesnay,mduquesnayqo@indiatimes.com,Male,57.157.3.46 +962,Lela,Oulner,loulnerqp@uol.com.br,Female,58.218.91.178 +963,Roland,Handaside,rhandasideqq@networksolutions.com,Male,138.214.166.248 +964,Fletcher,Gercke,fgerckeqr@bizjournals.com,Male,163.91.177.70 +965,Goddart,Bastock,gbastockqs@people.com.cn,Male,29.32.146.155 +966,Blake,Hierro,bhierroqt@ft.com,Female,228.53.177.76 +967,Maryellen,Hassent,mhassentqu@youtu.be,Female,21.140.212.72 +968,Claiborn,Vanes,cvanesqv@github.com,Male,226.4.103.43 +969,Ardenia,Brindley,abrindleyqw@flickr.com,Female,210.198.88.61 +970,Marita,Rudyard,mrudyardqx@creativecommons.org,Female,116.207.14.123 +971,Catriona,Cade,ccadeqy@cocolog-nifty.com,Female,87.221.34.254 +972,Maddie,Kupis,mkupisqz@photobucket.com,Male,124.126.244.148 +973,Alister,Zoane,azoaner0@dailymotion.com,Male,91.144.22.151 +974,Stevena,Whysall,swhysallr1@mediafire.com,Female,25.98.41.94 +975,Adolph,Warbys,awarbysr2@1und1.de,Male,226.243.70.38 +976,Katerina,Lent,klentr3@hugedomains.com,Female,10.120.136.68 +977,Cassaundra,Earengey,cearengeyr4@stanford.edu,Female,45.80.145.71 +978,Willis,Flacke,wflacker5@hibu.com,Male,115.225.62.151 +979,Witty,Greetham,wgreethamr6@bigcartel.com,Male,130.228.100.148 +980,Libby,Cottam,lcottamr7@merriam-webster.com,Female,112.114.81.33 +981,Benedikta,Ingerman,bingermanr8@umich.edu,Female,240.56.61.165 +982,Rubetta,Phipard-Shears,rphipardshearsr9@webmd.com,Female,7.119.3.224 +983,Brew,Vannah,bvannahra@state.gov,Male,218.158.56.217 +984,Donal,Splevins,dsplevinsrb@reference.com,Male,129.165.217.212 +985,Cassy,Hathway,chathwayrc@webnode.com,Female,179.18.252.16 +986,Rainer,Beaudry,rbeaudryrd@yahoo.co.jp,Male,140.134.47.119 +987,Hynda,McKeon,hmckeonre@apache.org,Female,125.240.87.28 +988,Jemmie,Laidlaw,jlaidlawrf@studiopress.com,Female,12.161.231.123 +989,Cthrine,McRobb,cmcrobbrg@stanford.edu,Female,52.221.245.25 +990,Hillary,Hitzke,hhitzkerh@feedburner.com,Female,208.214.204.114 +991,Ardyce,Fould,afouldri@comcast.net,Female,216.141.2.59 +992,Dorris,Drake,ddrakerj@intel.com,Female,3.218.102.252 +993,Sharity,Crippen,scrippenrk@themeforest.net,Female,12.121.92.255 +994,Tandie,Kahn,tkahnrl@cam.ac.uk,Female,49.12.239.114 +995,Kermy,Braban,kbrabanrm@uiuc.edu,Male,109.139.220.68 +996,Caritta,Diegan,cdieganrn@ox.ac.uk,Female,36.111.73.134 +997,Dorthy,Thys,dthysro@live.com,Female,43.161.52.24 +998,Bathsheba,Billington,bbillingtonrp@arstechnica.com,Female,42.251.195.185 +999,Ruprecht,Giovannetti,rgiovannettirq@google.fr,Male,40.67.154.106 +1000,Mikaela,Birtwhistle,mbirtwhistlerr@comsenz.com,Female,243.3.36.198 \ No newline at end of file diff --git a/infrastructure/tari_util/Cargo.toml b/infrastructure/tari_util/Cargo.toml index 3f9e0730b8..1f4a433104 100644 --- a/infrastructure/tari_util/Cargo.toml +++ b/infrastructure/tari_util/Cargo.toml @@ -6,14 +6,15 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.0.2" +version = "0.0.5" edition = "2018" [dependencies] derive-error = "0.0.4" clear_on_drop = "0.2.3" chrono = {version = "0.4.6", optional = true} -rmp-serde = "0.13.7" +#rmp-serde = "0.13.7" +bincode = "1.1.4" base64 = "0.10.1" serde_json = "1.0" serde = {version = "1.0.89", features = ["derive"] } diff --git a/infrastructure/tari_util/src/ciphers/chacha20.rs b/infrastructure/tari_util/src/ciphers/chacha20.rs index 931f93aac3..7316d39456 100644 --- a/infrastructure/tari_util/src/ciphers/chacha20.rs +++ b/infrastructure/tari_util/src/ciphers/chacha20.rs @@ -138,7 +138,7 @@ impl ChaCha20 { impl Cipher for ChaCha20 where D: ByteArray { - fn seal(plain_text: &D, key: &Vec, nonce: &Vec) -> Result, CipherError> { + fn seal(plain_text: &D, key: &[u8], nonce: &[u8]) -> Result, CipherError> { // Validation if key.len() != 32 { return Err(CipherError::KeyLengthError); @@ -146,15 +146,15 @@ where D: ByteArray if nonce.len() != 12 { return Err(CipherError::NonceLengthError); } - if plain_text.as_bytes().len() == 0 { + if plain_text.as_bytes().is_empty() { return Err(CipherError::NoDataError); } let mut sized_key = [0; 32]; - sized_key.copy_from_slice(key.as_slice()); + sized_key.copy_from_slice(key); let mut sized_nonce = [0; 12]; - sized_nonce.copy_from_slice(nonce.as_slice()); + sized_nonce.copy_from_slice(nonce); let cipher_text = ChaCha20::encode_with_nonce(plain_text.as_bytes(), &sized_key, &sized_nonce); // Clear copied private data @@ -164,7 +164,7 @@ where D: ByteArray Ok(cipher_text) } - fn open(cipher_text: &Vec, key: &Vec, nonce: &Vec) -> Result { + fn open(cipher_text: &[u8], key: &[u8], nonce: &[u8]) -> Result { // Validation if key.len() != 32 { return Err(CipherError::KeyLengthError); @@ -172,15 +172,15 @@ where D: ByteArray if nonce.len() != 12 { return Err(CipherError::NonceLengthError); } - if cipher_text.len() == 0 { + if cipher_text.is_empty() { return Err(CipherError::NoDataError); } let mut sized_key = [0; 32]; - sized_key.copy_from_slice(key.as_slice()); + sized_key.copy_from_slice(key); let mut sized_nonce = [0; 12]; - sized_nonce.copy_from_slice(nonce.as_slice()); + sized_nonce.copy_from_slice(nonce); let plain_text = ChaCha20::decode_with_nonce(cipher_text, &sized_key, &sized_nonce); // Clear copied private data @@ -190,17 +190,17 @@ where D: ByteArray Ok(D::from_vec(&plain_text)?) } - fn seal_with_integral_nonce(plain_text: &D, key: &Vec) -> Result, CipherError> { + fn seal_with_integral_nonce(plain_text: &D, key: &[u8]) -> Result, CipherError> { // Validation if key.len() != 32 { return Err(CipherError::KeyLengthError); } - if plain_text.as_bytes().len() == 0 { + if plain_text.as_bytes().is_empty() { return Err(CipherError::NoDataError); } let mut sized_key = [0; 32]; - sized_key.copy_from_slice(key.as_slice()); + sized_key.copy_from_slice(key); let mut rng = OsRng::new().unwrap(); let mut nonce = [0u8; 12]; @@ -217,7 +217,7 @@ where D: ByteArray Ok(nonce_with_cipher_text) } - fn open_with_integral_nonce(cipher_text: &Vec, key: &Vec) -> Result { + fn open_with_integral_nonce(cipher_text: &[u8], key: &[u8]) -> Result { // Validation if key.len() != 32 { return Err(CipherError::KeyLengthError); @@ -230,10 +230,10 @@ where D: ByteArray } let mut sized_key = [0; 32]; - sized_key.copy_from_slice(key.as_slice()); + sized_key.copy_from_slice(key); let mut nonce = [0u8; 12]; - nonce.copy_from_slice(&cipher_text.clone()[0..12]); + nonce.copy_from_slice(&cipher_text[0..12]); let plain_text = ChaCha20::decode_with_nonce(&cipher_text[12..], &sized_key, &nonce); // Clear copied private data diff --git a/infrastructure/tari_util/src/ciphers/cipher.rs b/infrastructure/tari_util/src/ciphers/cipher.rs index ee52384c3e..9a877819c5 100644 --- a/infrastructure/tari_util/src/ciphers/cipher.rs +++ b/infrastructure/tari_util/src/ciphers/cipher.rs @@ -40,14 +40,14 @@ pub trait Cipher where D: ByteArray { /// Encrypt using a cipher and provided key and nonce - fn seal(plain_text: &D, key: &Vec, nonce: &Vec) -> Result, CipherError>; + fn seal(plain_text: &D, key: &[u8], nonce: &[u8]) -> Result, CipherError>; /// Decrypt using a cipher and provided key and nonce - fn open(cipher_text: &Vec, key: &Vec, nonce: &Vec) -> Result; + fn open(cipher_text: &[u8], key: &[u8], nonce: &[u8]) -> Result; /// Encrypt using a cipher and provided key, the nonce will be generate internally and appended to the cipher text - fn seal_with_integral_nonce(plain_text: &D, key: &Vec) -> Result, CipherError>; + fn seal_with_integral_nonce(plain_text: &D, key: &[u8]) -> Result, CipherError>; /// Decrypt using a cipher and provided key. The integral nonce will be read from the cipher text - fn open_with_integral_nonce(cipher_text: &Vec, key: &Vec) -> Result; + fn open_with_integral_nonce(cipher_text: &[u8], key: &[u8]) -> Result; } diff --git a/infrastructure/tari_util/src/hex.rs b/infrastructure/tari_util/src/hex.rs index be3ac6878e..592600075a 100644 --- a/infrastructure/tari_util/src/hex.rs +++ b/infrastructure/tari_util/src/hex.rs @@ -1,4 +1,5 @@ use derive_error::Error; +use serde::Serializer; use std::{ fmt::{LowerHex, Write}, num::ParseIntError, @@ -67,6 +68,14 @@ pub fn from_hex(hex_str: &str) -> Result, HexError> { Ok(result) } +pub fn serialize_to_hex(t: &T, ser: S) -> Result +where + S: Serializer, + T: Hex, +{ + ser.serialize_str(&t.to_hex()) +} + #[cfg(test)] mod test { use super::*; diff --git a/infrastructure/tari_util/src/lib.rs b/infrastructure/tari_util/src/lib.rs index 7d1faa15e5..686abcb006 100644 --- a/infrastructure/tari_util/src/lib.rs +++ b/infrastructure/tari_util/src/lib.rs @@ -28,6 +28,7 @@ pub mod fixed_set; pub mod hash; pub mod hex; pub mod message_format; +pub mod thread_join; pub use self::extend_bytes::ExtendBytes; diff --git a/infrastructure/tari_util/src/message_format.rs b/infrastructure/tari_util/src/message_format.rs index 9f2b48a00b..d2d81dd4ec 100644 --- a/infrastructure/tari_util/src/message_format.rs +++ b/infrastructure/tari_util/src/message_format.rs @@ -22,16 +22,17 @@ use base64; use derive_error::Error; -use rmp_serde; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json; #[derive(Debug, Error)] pub enum MessageFormatError { // An error occurred serialising an object into binary - BinarySerializeError(rmp_serde::encode::Error), + #[error(no_from, no_std)] + BinarySerializeError, // An error occurred deserialising binary data into an object - BinaryDeserializeError(rmp_serde::decode::Error), + #[error(no_from, no_std)] + BinaryDeserializeError, // An error occurred de-/serialising an object from/into JSON JSONError(serde_json::error::Error), // An error occurred deserialising an object from Base64 @@ -48,14 +49,11 @@ pub trait MessageFormat: Sized { fn from_base64(msg: &str) -> Result; } -impl<'a, T> MessageFormat for T -where T: Deserialize<'a> + Serialize +impl<'de, T> MessageFormat for T +where T: DeserializeOwned + Serialize { fn to_binary(&self) -> Result, MessageFormatError> { - let mut buf = Vec::new(); - self.serialize(&mut rmp_serde::Serializer::new(&mut buf)) - .map_err(MessageFormatError::BinarySerializeError)?; - Ok(buf.to_vec()) + bincode::serialize(self).map_err(|_| MessageFormatError::BinarySerializeError) } fn to_json(&self) -> Result { @@ -68,8 +66,7 @@ where T: Deserialize<'a> + Serialize } fn from_binary(msg: &[u8]) -> Result { - let mut de = rmp_serde::Deserializer::new(msg); - Deserialize::deserialize(&mut de).map_err(MessageFormatError::BinaryDeserializeError) + bincode::deserialize(msg).map_err(|_| MessageFormatError::BinaryDeserializeError) } fn from_json(msg: &str) -> Result { @@ -87,9 +84,7 @@ where T: Deserialize<'a> + Serialize mod test { use super::*; use base64::DecodeError as Base64Error; - use rmp_serde::decode::Error as RMPError; use serde::{Deserialize, Serialize}; - use std::io::ErrorKind; #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] struct TestMessage { @@ -116,7 +111,10 @@ mod test { fn binary_simple() { let val = TestMessage::new("twenty", 20); let msg = val.to_binary().unwrap(); - assert_eq!(msg, b"\x93\xA6\x74\x77\x65\x6E\x74\x79\x14\xC0"); + assert_eq!( + msg, + b"\x06\x00\x00\x00\x00\x00\x00\x00\x74\x77\x65\x6e\x74\x79\x14\x00\x00\x00\x00\x00\x00\x00\x00" + ); let val2 = TestMessage::from_binary(&msg).unwrap(); assert_eq!(val, val2); } @@ -125,7 +123,7 @@ mod test { fn base64_simple() { let val = TestMessage::new("twenty", 20); let msg = val.to_base64().unwrap(); - assert_eq!(msg, "k6Z0d2VudHkUwA=="); + assert_eq!(msg, "BgAAAAAAAAB0d2VudHkUAAAAAAAAAAA="); let val2 = TestMessage::from_base64(&msg).unwrap(); assert_eq!(val, val2); } @@ -153,12 +151,15 @@ mod test { ); let msg_base64 = val.to_base64().unwrap(); - assert_eq!(msg_base64, "k6h0b21vcnJvdzKTpXRvZGF5ZMA="); + assert_eq!( + msg_base64, + "CAAAAAAAAAB0b21vcnJvdzIAAAAAAAAAAQUAAAAAAAAAdG9kYXlkAAAAAAAAAAA=" + ); let msg_bin = val.to_binary().unwrap(); assert_eq!( msg_bin, - b"\x93\xA8\x74\x6F\x6D\x6F\x72\x72\x6F\x77\x32\x93\xA5\x74\x6F\x64\x61\x79\x64\xC0" + b"\x08\x00\x00\x00\x00\x00\x00\x00\x74\x6f\x6d\x6f\x72\x72\x6f\x77\x32\x00\x00\x00\x00\x00\x00\x00\x01\x05\x00\x00\x00\x00\x00\x00\x00\x74\x6f\x64\x61\x79\x64\x00\x00\x00\x00\x00\x00\x00\x00".to_vec() ); let val2 = TestMessage::from_json(&msg_json).unwrap(); @@ -197,9 +198,7 @@ mod test { let err = TestMessage::from_base64("j6h0b21vcnJvdzKTpXRvZGF5ZMA=").err().unwrap(); match err { - MessageFormatError::BinaryDeserializeError(RMPError::Syntax(s)) => { - assert_eq!(s, "invalid type: sequence, expected field identifier"); - }, + MessageFormatError::BinaryDeserializeError => {}, _ => panic!("Base64 conversion should fail"), }; } @@ -208,9 +207,7 @@ mod test { fn fail_binary() { let err = TestMessage::from_binary(b"").err().unwrap(); match err { - MessageFormatError::BinaryDeserializeError(RMPError::InvalidMarkerRead(e)) => { - assert_eq!(e.kind(), ErrorKind::UnexpectedEof, "Unexpected error type: {:?}", e); - }, + MessageFormatError::BinaryDeserializeError => {}, _ => { panic!("Base64 conversion should fail"); }, diff --git a/infrastructure/tari_util/src/thread_join/error.rs b/infrastructure/tari_util/src/thread_join/error.rs new file mode 100644 index 0000000000..0b49aef4db --- /dev/null +++ b/infrastructure/tari_util/src/thread_join/error.rs @@ -0,0 +1,33 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use derive_error::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum ThreadError { + /// An error occurred attempting to join the thread + JoinError, + /// The timeout period allocated to the thread joining process has been exceeded + TimeoutReached, + /// The channel has disconnected between the host and the join thread + ChannelDisconnected, +} diff --git a/infrastructure/comms/src/connection/p2p/connection.rs b/infrastructure/tari_util/src/thread_join/mod.rs similarity index 93% rename from infrastructure/comms/src/connection/p2p/connection.rs rename to infrastructure/tari_util/src/thread_join/mod.rs index 3d550c3ff4..47120eb6eb 100644 --- a/infrastructure/comms/src/connection/p2p/connection.rs +++ b/infrastructure/tari_util/src/thread_join/mod.rs @@ -20,8 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::connection::Connection; +pub mod error; +pub mod thread_join; -pub struct PeerConnection {} - -impl Connection for PeerConnection {} +pub use self::{error::ThreadError, thread_join::ThreadJoinWithTimeout}; diff --git a/infrastructure/tari_util/src/thread_join/thread_join.rs b/infrastructure/tari_util/src/thread_join/thread_join.rs new file mode 100644 index 0000000000..4f73c64f85 --- /dev/null +++ b/infrastructure/tari_util/src/thread_join/thread_join.rs @@ -0,0 +1,104 @@ +// Copyright 2019 The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::thread_join::ThreadError; +use std::{ + sync::mpsc::{sync_channel, RecvTimeoutError, SyncSender}, + thread::{self, JoinHandle}, + time::Duration, +}; + +#[derive(Debug)] +pub enum StatusMessage { + /// Successfully joined the thread + Ok, + /// An error occurred attempting to join the thread + Error, +} + +/// Spawn a single thread that will attempt to join the specified thread +fn spawn_join_thread(thread_handle: JoinHandle, status_sync_sender: SyncSender) +where T: 'static { + thread::spawn(move || match thread_handle.join() { + Ok(_) => status_sync_sender.send(StatusMessage::Ok).unwrap(), + Err(_) => status_sync_sender.send(StatusMessage::Error).unwrap(), + }); +} + +/// Perform a thread join with a timeout on the JoinHandle, it has a configurable timeout +fn timeout_join(thread_handle: JoinHandle, timeout_in_ms: Duration) -> Result<(), ThreadError> +where T: 'static { + let (status_sync_sender, status_receiver) = sync_channel(5); + spawn_join_thread(thread_handle, status_sync_sender); + + // Check for status messages + match status_receiver.recv_timeout(timeout_in_ms) { + Ok(status_msg) => match status_msg { + StatusMessage::Ok => Ok(()), + StatusMessage::Error => Err(ThreadError::JoinError), + }, + Err(RecvTimeoutError::Timeout) => Err(ThreadError::TimeoutReached), + Err(RecvTimeoutError::Disconnected) => Err(ThreadError::ChannelDisconnected), + } +} + +pub trait ThreadJoinWithTimeout { + /// Attempt to join the current thread with a configurable timeout + fn timeout_join(self, timeout_in_ms: Duration) -> Result<(), ThreadError>; +} + +/// Extend JoinHandle to have member functions that enable join with a timeout +impl ThreadJoinWithTimeout for JoinHandle +where T: 'static +{ + fn timeout_join(self, timeout_in_ms: Duration) -> Result<(), ThreadError> { + timeout_join(self, timeout_in_ms) + } +} + +#[cfg(test)] +mod test { + use crate::thread_join::ThreadJoinWithTimeout; + use std::{thread, time::Duration}; + + #[test] + fn test_normal_thread_join() { + // Create a blocking thread + let thread_handle = thread::spawn(move || { + thread::sleep(Duration::from_millis(50)); + }); + + let join_timeout_in_ms = Duration::from_millis(100); + assert!(thread_handle.timeout_join(join_timeout_in_ms).is_ok()); + } + + #[test] + fn test_thread_join_with_timeout() { + // Create a blocking thread + let thread_handle = thread::spawn(move || { + thread::sleep(Duration::from_millis(50)); + }); + + let join_timeout_in_ms = Duration::from_millis(25); + assert!(thread_handle.timeout_join(join_timeout_in_ms).is_err()); + } +} diff --git a/meta/assets/rustdoc-include-js-header.html b/meta/assets/rustdoc-include-js-header.html new file mode 100644 index 0000000000..dd21f60951 --- /dev/null +++ b/meta/assets/rustdoc-include-js-header.html @@ -0,0 +1,12 @@ + + + + + + + diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000000..c4fd127496 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly-2019-08-21 diff --git a/rustfmt.toml b/rustfmt.toml index 659768fefd..67fcc6ff7f 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -19,7 +19,8 @@ space_before_colon = false struct_lit_single_line = true use_field_init_shorthand = true use_try_shorthand = true -format_doc_comments = true +unstable_features = true +format_code_in_doc_comments = true where_single_line = true wrap_comments = true overflow_delimited_expr = true diff --git a/scripts/code_coverage.sh b/scripts/code_coverage.sh index 818fe0a491..0e601f6470 100755 --- a/scripts/code_coverage.sh +++ b/scripts/code_coverage.sh @@ -10,7 +10,7 @@ member_crate_name=$1 member_source_dir=$2 source_root_dir="tari" build_dir="target/debug/" -report_dir="report/" +report_dir="report/$member_crate_name" echo "Check if in correct directory": path=$(pwd) @@ -64,21 +64,23 @@ else fi # Make clean directories for Build and Report mkdir $build_dir -mkdir $report_dir +mkdir -p $report_dir -echo "Build project.." +echo "Setup project.." export CARGO_INCREMENTAL=0 -export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads" -cargo +nightly build --verbose $CARGO_OPTIONS +export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Clink-dead-code -Copt-level=0 -Coverflow-checks=off -Zno-landing-pads" +echo "Build project.." +cargo +nightly build $CARGO_OPTIONS echo "Perform project Tests.." cargo_filename="Cargo.toml" -cargo +nightly test --verbose $CARGO_OPTIONS --manifest-path="$member_source_dir$cargo_filename" +cargo +nightly test $CARGO_OPTIONS --manifest-path="$member_source_dir$cargo_filename" echo "Acquire all build and test files for coverage check.." ccov_filename="ccov.zip" ccov_path="$report_dir$ccov_filename" -zip -0 $ccov_path `find $build_dir \( -name "$member_crate_name*.gc*" \) -print`; + +zip $ccov_path `find $build_dir \( -name "$member_crate_name*.gc*" \) -print`; echo "Perform grcov code coverage.." lcov_filename="lcov.info" @@ -91,4 +93,4 @@ genhtml -o $report_dir --show-details --highlight --title $member_crate_name --l echo "Launch report in browser.." index_str="index.html" -open "$report_dir$index_str" +open "$report_dir/$index_str" diff --git a/scripts/code_coverage_blockchain.sh b/scripts/code_coverage_blockchain.sh deleted file mode 100755 index c27736b7fb..0000000000 --- a/scripts/code_coverage_blockchain.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# Check if in script directory -path=$(pwd) -primary_dir=$(basename $path) -if [ "$primary_dir" != "scripts" ]; then - cd scripts -fi - -./code_coverage.sh "blockchain" "base_layer/blockchain/" diff --git a/scripts/code_coverage_comms.sh b/scripts/code_coverage_comms.sh index b81cabec44..df79ac95bb 100755 --- a/scripts/code_coverage_comms.sh +++ b/scripts/code_coverage_comms.sh @@ -7,4 +7,4 @@ if [ "$primary_dir" != "scripts" ]; then cd scripts fi -./code_coverage.sh "comms" "infrastructure/comms/" +./code_coverage.sh "tari_comms" "comms/" diff --git a/scripts/code_coverage_crypto.sh b/scripts/code_coverage_crypto.sh index 94fc4b4476..ca19ffb8d6 100755 --- a/scripts/code_coverage_crypto.sh +++ b/scripts/code_coverage_crypto.sh @@ -7,4 +7,4 @@ if [ "$primary_dir" != "scripts" ]; then cd scripts fi -./code_coverage.sh "crypto" "infrastructure/crypto/" +./code_coverage.sh "tari_crypto" "infrastructure/crypto/" diff --git a/scripts/code_coverage_storage.sh b/scripts/code_coverage_storage.sh index 3a83c918a5..3721c23b58 100755 --- a/scripts/code_coverage_storage.sh +++ b/scripts/code_coverage_storage.sh @@ -7,4 +7,4 @@ if [ "$primary_dir" != "scripts" ]; then cd scripts fi -./code_coverage.sh "storage" "infrastructure/storage/" +./code_coverage.sh "tari_storage" "infrastructure/storage/" diff --git a/scripts/publish_crates.sh b/scripts/publish_crates.sh index 49c2ca281b..a03ed07ede 100755 --- a/scripts/publish_crates.sh +++ b/scripts/publish_crates.sh @@ -1,5 +1,19 @@ #!/usr/bin/env bash -packages=${@:-'infrastructure/tari_util infrastructure/derive infrastructure/crypto infrastructure/merklemountainrange base_layer/core'} +packages=${@:-' +infrastructure/crypto +infrastructure/derive +infrastructure/storage +infrastructure/tari_util +base_layer/core +base_layer/keymanager +base_layer/mining +base_layer/mmr +base_layer/p2p +base_layer/service_framework +base_layer/wallet +common +comms +'} p_arr=($packages) function build_package { diff --git a/scripts/update_crate_metadata.sh b/scripts/update_crate_metadata.sh new file mode 100755 index 0000000000..d278a97511 --- /dev/null +++ b/scripts/update_crate_metadata.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +VERSION=$1 +if [ "x$VERSION" == "x" ]; then + echo "USAGE: update_crate_metadata version" + exit 1 +fi + +function update_versions { + packages=${@:-' + infrastructure/crypto + infrastructure/derive + infrastructure/storage + infrastructure/tari_util + base_layer/core + base_layer/keymanager + base_layer/mining + base_layer/mmr + base_layer/p2p + base_layer/service_framework + base_layer/wallet + common + comms +'} + + p_arr=($packages) + for p in "${p_arr[@]}"; do + echo "Updating $path/$p version" + update_version ./${p}/Cargo.toml $VERSION + done +} + +function update_version { + CARGO=$1 + VERSION=$2 + SCRIPT='s/^version = "[0-9]\.[0-9]\.[0-9]"$/version = "'"$VERSION"'"/g' + sed -i.bak -e "$SCRIPT" "$CARGO" + rm $CARGO.bak +} + + + +update_versions ${p_arr[@]}