diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 00000000..8ed932ab --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,74 @@ +# This builds `cargo doc` and uploads it to the repo's GitHub Pages. + +name: Doc + +on: + push: + branches: [ "main" ] # Only deploy if `main` changes. + workflow_dispatch: + +env: + # Show colored output in CI. + CARGO_TERM_COLOR: always + # Generate an index page. + RUSTDOCFLAGS: '--cfg docsrs --show-type-layout --enable-index-page -Zunstable-options' + +jobs: + # Build documentation. + build: + # FIXME: how to build and merge Windows + macOS docs + # with Linux's? Similar to the OS toggle on docs.rs. + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + # Nightly required for some `cargo doc` settings. + toolchain: nightly + + - name: Cache + uses: actions/cache@v4 + with: + # Don't cache actual doc files, just build files. + # This is so that removed crates don't show up. + path: target/debug + key: doc + + # Packages other than `Boost` used by `Monero` are listed here. + # https://github.com/monero-project/monero/blob/c444a7e002036e834bfb4c68f04a121ce1af5825/.github/workflows/build.yml#L71 + + - name: Install dependencies (Linux) + run: sudo apt install -y libboost-dev + + - name: Documentation + run: cargo +nightly doc --workspace --all-features + + - name: Upload documentation + uses: actions/upload-pages-artifact@v3 + with: + path: target/doc/ + + # Deployment job. + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + + # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages + permissions: + contents: read + pages: write + id-token: write + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c9c1f03..1b66a58e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -216,9 +216,9 @@ The description of pull requests should generally follow the template laid out i If your pull request is long and/or has sections that need clarifying, consider leaving a review on your own PR with comments explaining the changes. ## 5. Documentation -Cuprate's crates (libraries) have inline documentation. +Cuprate's crates (libraries) have inline documentation, they are published from the `main` branch at https://doc.cuprate.org. -These can be built and viewed using the `cargo` tool. For example, to build and view a specific crate's documentation, run the following command at the repository's root: +Documentation can be built and viewed using the `cargo` tool. For example, to build and view a specific crate's documentation, run the following command at the repository's root: ```bash cargo doc --open --package $CRATE ``` diff --git a/Cargo.lock b/Cargo.lock index 5ee9d870..32a5cbd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -110,12 +121,28 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base58-monero" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978e81a45367d2409ecd33369a45dda2e9a3ca516153ec194de1fbda4b9fb79d" +dependencies = [ + "thiserror", + "tiny-keccak", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bincode" version = "1.3.3" @@ -247,6 +274,9 @@ name = "bytes" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +dependencies = [ + "serde", +] [[package]] name = "cc" @@ -316,6 +346,15 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -397,6 +436,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -404,7 +449,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "subtle", - "zeroize", ] [[package]] @@ -463,8 +507,8 @@ dependencies = [ "hex", "hex-literal", "monero-serai", - "paste", "pretty_assertions", + "proptest", "rayon", "serde", "tempfile", @@ -483,17 +527,21 @@ dependencies = [ "cuprate-test-utils", "cuprate-types", "curve25519-dalek", + "dalek-ff-group", "futures", "hex", "hex-literal", "monero-serai", + "multiexp", "proptest", "proptest-derive", + "rand", "randomx-rs", "rayon", "thiserror", "thread_local", "tokio", + "tokio-test", "tokio-util", "tower", "tracing", @@ -507,9 +555,11 @@ dependencies = [ "cuprate-cryptonight", "cuprate-helper", "curve25519-dalek", + "dalek-ff-group", "hex", "hex-literal", "monero-serai", + "multiexp", "proptest", "proptest-derive", "rand", @@ -552,6 +602,7 @@ dependencies = [ "cfg-if", "heed", "page_size", + "paste", "redb", "serde", "tempfile", @@ -595,6 +646,8 @@ name = "cuprate-fixed-bytes" version = "0.1.0" dependencies = [ "bytes", + "serde", + "serde_json", "thiserror", ] @@ -642,6 +695,7 @@ dependencies = [ name = "cuprate-p2p" version = "0.1.0" dependencies = [ + "borsh", "bytes", "cuprate-address-book", "cuprate-async-buffer", @@ -650,6 +704,7 @@ dependencies = [ "cuprate-p2p-core", "cuprate-pruning", "cuprate-test-utils", + "cuprate-types", "cuprate-wire", "dashmap", "futures", @@ -682,9 +737,11 @@ dependencies = [ "cuprate-wire", "futures", "hex", + "hex-literal", "thiserror", "tokio", "tokio-stream", + "tokio-test", "tokio-util", "tower", "tracing", @@ -708,8 +765,13 @@ name = "cuprate-rpc-types" version = "0.0.0" dependencies = [ "cuprate-epee-encoding", + "cuprate-fixed-bytes", + "cuprate-json-rpc", + "cuprate-test-utils", + "cuprate-types", "monero-serai", "paste", + "pretty_assertions", "serde", "serde_json", ] @@ -728,9 +790,8 @@ dependencies = [ "futures", "hex", "hex-literal", - "monero-rpc", "monero-serai", - "monero-simple-request-rpc", + "paste", "pretty_assertions", "serde", "serde_json", @@ -747,7 +808,9 @@ version = "0.0.0" name = "cuprate-types" version = "0.0.0" dependencies = [ - "borsh", + "bytes", + "cuprate-epee-encoding", + "cuprate-fixed-bytes", "curve25519-dalek", "monero-serai", "serde", @@ -762,6 +825,7 @@ dependencies = [ "cuprate-epee-encoding", "cuprate-fixed-bytes", "cuprate-levin", + "cuprate-types", "hex", "thiserror", ] @@ -798,7 +862,7 @@ dependencies = [ [[package]] name = "dalek-ff-group" version = "0.4.1" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" +source = "git+https://github.com/Cuprate/serai.git?rev=d27d934#d27d93480aa8a849d84214ad4c71d83ce6fea0c1" dependencies = [ "crypto-bigint", "curve25519-dalek", @@ -917,6 +981,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.1.0" @@ -943,7 +1028,7 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "flexible-transcript" version = "0.3.2" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" +source = "git+https://github.com/Cuprate/serai.git?rev=d27d934#d27d93480aa8a849d84214ad4c71d83ce6fea0c1" dependencies = [ "blake2", "digest", @@ -1177,6 +1262,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.1.0" @@ -1213,9 +1307,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" [[package]] name = "hyper" @@ -1586,163 +1680,63 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "monero-address" -version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" -dependencies = [ - "curve25519-dalek", - "monero-io", - "monero-primitives", - "std-shims", - "thiserror", - "zeroize", -] - -[[package]] -name = "monero-borromean" -version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" -dependencies = [ - "curve25519-dalek", - "monero-generators", - "monero-io", - "monero-primitives", - "std-shims", - "zeroize", -] - -[[package]] -name = "monero-bulletproofs" -version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" -dependencies = [ - "curve25519-dalek", - "monero-generators", - "monero-io", - "monero-primitives", - "rand_core", - "std-shims", - "thiserror", - "zeroize", -] - -[[package]] -name = "monero-clsag" -version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" -dependencies = [ - "curve25519-dalek", - "dalek-ff-group", - "flexible-transcript", - "group", - "monero-generators", - "monero-io", - "monero-primitives", - "rand_chacha", - "rand_core", - "std-shims", - "subtle", - "thiserror", - "zeroize", -] - [[package]] name = "monero-generators" version = "0.4.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" +source = "git+https://github.com/Cuprate/serai.git?rev=d27d934#d27d93480aa8a849d84214ad4c71d83ce6fea0c1" dependencies = [ "curve25519-dalek", "dalek-ff-group", "group", - "monero-io", "sha3", "std-shims", "subtle", ] -[[package]] -name = "monero-io" -version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" -dependencies = [ - "curve25519-dalek", - "std-shims", -] - -[[package]] -name = "monero-mlsag" -version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" -dependencies = [ - "curve25519-dalek", - "monero-generators", - "monero-io", - "monero-primitives", - "std-shims", - "thiserror", - "zeroize", -] - -[[package]] -name = "monero-primitives" -version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" -dependencies = [ - "curve25519-dalek", - "monero-generators", - "monero-io", - "sha3", - "std-shims", - "zeroize", -] - -[[package]] -name = "monero-rpc" -version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" -dependencies = [ - "async-trait", - "curve25519-dalek", - "hex", - "monero-address", - "monero-serai", - "serde", - "serde_json", - "std-shims", - "thiserror", - "zeroize", -] - [[package]] name = "monero-serai" version = "0.1.4-alpha" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" +source = "git+https://github.com/Cuprate/serai.git?rev=d27d934#d27d93480aa8a849d84214ad4c71d83ce6fea0c1" dependencies = [ + "async-lock", + "async-trait", + "base58-monero", "curve25519-dalek", + "dalek-ff-group", + "digest_auth", + "flexible-transcript", + "group", + "hex", "hex-literal", - "monero-borromean", - "monero-bulletproofs", - "monero-clsag", "monero-generators", - "monero-io", - "monero-mlsag", - "monero-primitives", + "multiexp", + "pbkdf2", + "rand", + "rand_chacha", + "rand_core", + "rand_distr", + "serde", + "serde_json", + "sha3", + "simple-request", "std-shims", + "subtle", + "thiserror", + "tokio", "zeroize", ] [[package]] -name = "monero-simple-request-rpc" -version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" +name = "multiexp" +version = "0.4.0" +source = "git+https://github.com/Cuprate/serai.git?rev=d27d934#d27d93480aa8a849d84214ad4c71d83ce6fea0c1" dependencies = [ - "async-trait", - "digest_auth", - "hex", - "monero-rpc", - "simple-request", - "tokio", + "ff", + "group", + "rand_core", + "rustversion", + "std-shims", + "zeroize", ] [[package]] @@ -1802,6 +1796,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1825,12 +1825,35 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2383,7 +2406,7 @@ dependencies = [ [[package]] name = "simple-request" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" +source = "git+https://github.com/Cuprate/serai.git?rev=d27d934#d27d93480aa8a849d84214ad4c71d83ce6fea0c1" dependencies = [ "http-body-util", "hyper", @@ -2439,7 +2462,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "std-shims" version = "0.1.1" -source = "git+https://github.com/Cuprate/serai.git?rev=880565c#880565cb819e8b52883151d3b109713975561078" +source = "git+https://github.com/Cuprate/serai.git?rev=d27d934#d27d93480aa8a849d84214ad4c71d83ce6fea0c1" dependencies = [ "hashbrown 0.14.5", "spin", @@ -2553,6 +2576,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -3220,9 +3252,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", @@ -3231,9 +3263,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0d8bea07..dc265a28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,204 @@ tokio-test = { version = "0.4.4" } # regex = { version = "1.10.2" } # Regular expressions | https://github.com/rust-lang/regex # ryu = { version = "1.0.15" } # Fast float to string formatting | https://github.com/dtolnay/ryu -# Maybe one day. -# disk = { version = "*" } # (De)serialization to/from disk with various file formats | https://github.com/hinto-janai/disk -# readable = { version = "*" } # Stack-based string formatting utilities | https://github.com/hinto-janai/readable -# json-rpc = { git = "https://github.com/hinto-janai/json-rpc" } # JSON-RPC 2.0 types +# Lints: cold, warm, hot: +[workspace.lints.clippy] +# Cold +borrow_as_ptr = "deny" +case_sensitive_file_extension_comparisons = "deny" +cast_lossless = "deny" +cast_ptr_alignment = "deny" +checked_conversions = "deny" +cloned_instead_of_copied = "deny" +doc_link_with_quotes = "deny" +empty_enum = "deny" +enum_glob_use = "deny" +expl_impl_clone_on_copy = "deny" +explicit_into_iter_loop = "deny" +filter_map_next = "deny" +flat_map_option = "deny" +from_iter_instead_of_collect = "deny" +if_not_else = "deny" +ignored_unit_patterns = "deny" +inconsistent_struct_constructor = "deny" +index_refutable_slice = "deny" +inefficient_to_string = "deny" +invalid_upcast_comparisons = "deny" +iter_filter_is_ok = "deny" +iter_filter_is_some = "deny" +implicit_clone = "deny" +manual_c_str_literals = "deny" +manual_instant_elapsed = "deny" +manual_is_variant_and = "deny" +manual_let_else = "deny" +manual_ok_or = "deny" +manual_string_new = "deny" +map_unwrap_or = "deny" +match_bool = "deny" +match_same_arms = "deny" +match_wildcard_for_single_variants = "deny" +mismatching_type_param_order = "deny" +mut_mut = "deny" +needless_bitwise_bool = "deny" +needless_continue = "deny" +needless_for_each = "deny" +needless_raw_string_hashes = "deny" +no_effect_underscore_binding = "deny" +no_mangle_with_rust_abi = "deny" +option_as_ref_cloned = "deny" +option_option = "deny" +ptr_as_ptr = "deny" +ptr_cast_constness = "deny" +pub_underscore_fields = "deny" +redundant_closure_for_method_calls = "deny" +ref_as_ptr = "deny" +ref_option_ref = "deny" +same_functions_in_if_condition = "deny" +semicolon_if_nothing_returned = "deny" +trivially_copy_pass_by_ref = "deny" +uninlined_format_args = "deny" +unnecessary_join = "deny" +unnested_or_patterns = "deny" +unused_async = "deny" +unused_self = "deny" +used_underscore_binding = "deny" +zero_sized_map_values = "deny" +as_ptr_cast_mut = "deny" +clear_with_drain = "deny" +collection_is_never_read = "deny" +debug_assert_with_mut_call = "deny" +derive_partial_eq_without_eq = "deny" +empty_line_after_doc_comments = "deny" +empty_line_after_outer_attr = "deny" +equatable_if_let = "deny" +iter_on_empty_collections = "deny" +iter_on_single_items = "deny" +iter_with_drain = "deny" +needless_collect = "deny" +needless_pass_by_ref_mut = "deny" +negative_feature_names = "deny" +non_send_fields_in_send_ty = "deny" +nonstandard_macro_braces = "deny" +path_buf_push_overwrite = "deny" +read_zero_byte_vec = "deny" +redundant_clone = "deny" +redundant_feature_names = "deny" +trailing_empty_array = "deny" +trait_duplication_in_bounds = "deny" +type_repetition_in_bounds = "deny" +uninhabited_references = "deny" +unnecessary_struct_initialization = "deny" +unused_peekable = "deny" +unused_rounding = "deny" +use_self = "deny" +useless_let_if_seq = "deny" +wildcard_dependencies = "deny" +unseparated_literal_suffix = "deny" +unnecessary_safety_doc = "deny" +unnecessary_safety_comment = "deny" +unnecessary_self_imports = "deny" +tests_outside_test_module = "deny" +string_to_string = "deny" +rest_pat_in_fully_bound_structs = "deny" +redundant_type_annotations = "deny" +infinite_loop = "deny" + +# Warm +cast_possible_truncation = "deny" +cast_possible_wrap = "deny" +cast_precision_loss = "deny" +cast_sign_loss = "deny" +copy_iterator = "deny" +doc_markdown = "deny" +explicit_deref_methods = "deny" +explicit_iter_loop = "deny" +float_cmp = "deny" +fn_params_excessive_bools = "deny" +into_iter_without_iter = "deny" +iter_without_into_iter = "deny" +iter_not_returning_iterator = "deny" +large_digit_groups = "deny" +large_types_passed_by_value = "deny" +manual_assert = "deny" +maybe_infinite_iter = "deny" +missing_fields_in_debug = "deny" +needless_pass_by_value = "deny" +range_minus_one = "deny" +range_plus_one = "deny" +redundant_else = "deny" +ref_binding_to_reference = "deny" +return_self_not_must_use = "deny" +single_match_else = "deny" +string_add_assign = "deny" +transmute_ptr_to_ptr = "deny" +unchecked_duration_subtraction = "deny" +unnecessary_box_returns = "deny" +unnecessary_wraps = "deny" +branches_sharing_code = "deny" +fallible_impl_from = "deny" +missing_const_for_fn = "deny" +significant_drop_in_scrutinee = "deny" +significant_drop_tightening = "deny" +try_err = "deny" +lossy_float_literal = "deny" +let_underscore_must_use = "deny" +iter_over_hash_type = "deny" +impl_trait_in_params = "deny" +get_unwrap = "deny" +error_impl_error = "deny" +empty_structs_with_brackets = "deny" +empty_enum_variants_with_brackets = "deny" +empty_drop = "deny" +clone_on_ref_ptr = "deny" + +# Hot +# inline_always = "deny" +# large_futures = "deny" +# large_stack_arrays = "deny" +# linkedlist = "deny" +# missing_errors_doc = "deny" +# missing_panics_doc = "deny" +# should_panic_without_expect = "deny" +# similar_names = "deny" +# too_many_lines = "deny" +# unreadable_literal = "deny" +# wildcard_imports = "deny" +# allow_attributes_without_reason = "deny" +# missing_assert_message = "deny" +# missing_docs_in_private_items = "deny" +# undocumented_unsafe_blocks = "deny" +# multiple_unsafe_ops_per_block = "deny" +# single_char_lifetime_names = "deny" +# wildcard_enum_match_arm = "deny" + +[workspace.lints.rust] +# Cold +absolute_paths_not_starting_with_crate = "deny" +explicit_outlives_requirements = "deny" +keyword_idents_2018 = "deny" +keyword_idents_2024 = "deny" +missing_abi = "deny" +non_ascii_idents = "deny" +non_local_definitions = "deny" +single_use_lifetimes = "deny" +trivial_casts = "deny" +trivial_numeric_casts = "deny" +unsafe_op_in_unsafe_fn = "deny" +unused_crate_dependencies = "deny" +unused_import_braces = "deny" +unused_lifetimes = "deny" +unused_macro_rules = "deny" +ambiguous_glob_imports = "deny" +unused_unsafe = "deny" + +# Warm +let_underscore_drop = "deny" +unreachable_pub = "deny" +unused_qualifications = "deny" +variant_size_differences = "deny" + +# Hot +# unused_results = "deny" +# non_exhaustive_omitted_patterns = "deny" +# missing_docs = "deny" +# missing_copy_implementations = "deny" \ No newline at end of file diff --git a/README.md b/README.md index 100900d7..a9050d51 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Cuprate maintains various documentation books: | [Monero's protocol book](https://monero-book.cuprate.org) | Documents the Monero protocol | | [Cuprate's user book](https://user.cuprate.org) | Practical user-guide for using `cuprated` | -For crate (library) documentation, see the `Documentation` section in [`CONTRIBUTING.md`](CONTRIBUTING.md). +For crate (library) documentation, see: https://doc.cuprate.org. This site holds documentation for Cuprate's crates and all dependencies. All Cuprate crates start with `cuprate_`, for example: [`cuprate_database`](https://doc.cuprate.org/cuprate_database). ## Contributing diff --git a/books/architecture/README.md b/books/architecture/README.md index e4878432..88e86ebe 100644 --- a/books/architecture/README.md +++ b/books/architecture/README.md @@ -1,4 +1,4 @@ -## Cuprate's architecture (implementation) book +## Cuprate's architecture book This book documents Cuprate's architecture and implementation. See: diff --git a/books/architecture/book.toml b/books/architecture/book.toml index 76724aa4..996f7fe0 100644 --- a/books/architecture/book.toml +++ b/books/architecture/book.toml @@ -1,19 +1,17 @@ [book] -authors = ["hinto-janai"] +authors = ["Cuprate Contributors"] language = "en" multilingual = false src = "src" title = "Cuprate Architecture" git-repository-url = "https://github.com/Cuprate/architecture-book" -# TODO: fix after importing real files. -# -# [preprocessor.last-changed] -# command = "mdbook-last-changed" -# renderer = ["html"] -# -# [output.html] -# default-theme = "ayu" -# preferred-dark-theme = "ayu" -# git-repository-url = "https://github.com/hinto-janai/cuprate-architecture" -# additional-css = ["last-changed.css"] +[preprocessor.last-changed] +command = "mdbook-last-changed" +renderer = ["html"] + +[output.html] +default-theme = "ayu" +preferred-dark-theme = "ayu" +git-repository-url = "https://github.com/Cuprate/architecture-book" +additional-css = ["last-changed.css"] diff --git a/books/architecture/last-changed.css b/books/architecture/last-changed.css new file mode 100644 index 00000000..a9abae56 --- /dev/null +++ b/books/architecture/last-changed.css @@ -0,0 +1,7 @@ +footer { + font-size: 0.8em; + text-align: center; + border-top: 1px solid; + margin-top: 4%; + padding: 5px 0; +} \ No newline at end of file diff --git a/books/architecture/src/SUMMARY.md b/books/architecture/src/SUMMARY.md index 2b8615c9..3a8b3519 100644 --- a/books/architecture/src/SUMMARY.md +++ b/books/architecture/src/SUMMARY.md @@ -1,3 +1,124 @@ # Summary -- [TODO](todo.md) +[Cuprate Architecture](cuprate-architecture.md) +[🟡 Foreword](foreword.md) + +--- + +- [🟠 Intro](intro/intro.md) + - [🟡 Who this book is for](intro/who-this-book-is-for.md) + - [🔴 Required knowledge](intro/required-knowledge.md) + - [🔴 How to use this book](intro/how-to-use-this-book.md) + +--- + +- [⚪️ Bird's eye view](birds-eye-view/intro.md) + - [⚪️ Map](birds-eye-view/map.md) + - [⚪️ Components](birds-eye-view/components.md) + +--- + +- [⚪️ Formats, protocols, types](formats-protocols-types/intro.md) + - [⚪️ monero_serai](formats-protocols-types/monero-serai.md) + - [⚪️ cuprate_types](formats-protocols-types/cuprate-types.md) + - [⚪️ cuprate_helper](formats-protocols-types/cuprate-helper.md) + - [⚪️ Epee](formats-protocols-types/epee.md) + - [⚪️ Levin](formats-protocols-types/levin.md) + +--- + +- [⚪️ Storage](storage/intro.md) + - [⚪️ Database abstraction](storage/database-abstraction.md) + - [⚪️ Blockchain](storage/blockchain.md) + - [⚪️ Transaction pool](storage/transaction-pool.md) + - [⚪️ Pruning](storage/pruning.md) + +--- + +- [🔴 RPC](rpc/intro.md) + - [⚪️ Types](rpc/types/intro.md) + - [⚪️ JSON](rpc/types/json.md) + - [⚪️ Binary](rpc/types/binary.md) + - [⚪️ Other](rpc/types/other.md) + - [⚪️ Interface](rpc/interface.md) + - [⚪️ Router](rpc/router.md) + - [⚪️ Handler](rpc/handler.md) + - [⚪️ Methods](rpc/methods/intro.md) + +--- + +- [⚪️ ZMQ](zmq/intro.md) + - [⚪️ TODO](zmq/todo.md) + +--- + +- [⚪️ Consensus](consensus/intro.md) + - [⚪️ Verifier](consensus/verifier.md) + - [⚪️ TODO](consensus/todo.md) + +--- + +- [⚪️ Networking](networking/intro.md) + - [⚪️ P2P](networking/p2p.md) + - [⚪️ Dandelion++](networking/dandelion.md) + - [⚪️ Proxy](networking/proxy.md) + - [⚪️ Tor](networking/tor.md) + - [⚪️ i2p](networking/i2p.md) + - [⚪️ IPv4/IPv6](networking/ipv4-ipv6.md) + +--- + +- [🔴 Instrumentation](instrumentation/intro.md) + - [⚪️ Logging](instrumentation/logging.md) + - [⚪️ Data collection](instrumentation/data-collection.md) + +--- + +- [⚪️ Binary](binary/intro.md) + - [⚪️ CLI](binary/cli.md) + - [⚪️ Config](binary/config.md) + - [⚪️ Logging](binary/logging.md) + +--- + +- [⚪️ Resource model](resource-model/intro.md) + - [⚪️ File system](resource-model/file-system.md) + - [⚪️ Sockets](resource-model/sockets.md) + - [⚪️ Memory](resource-model/memory.md) + - [🟡 Concurrency and parallelism](resource-model/concurrency-and-parallelism/intro.md) + - [⚪️ Map](resource-model/concurrency-and-parallelism/map.md) + - [⚪️ The RPC server](resource-model/concurrency-and-parallelism/the-rpc-server.md) + - [⚪️ The database](resource-model/concurrency-and-parallelism/the-database.md) + - [⚪️ The block downloader](resource-model/concurrency-and-parallelism/the-block-downloader.md) + - [⚪️ The verifier](resource-model/concurrency-and-parallelism/the-verifier.md) + - [⚪️ Thread exit](resource-model/concurrency-and-parallelism/thread-exit.md) + +--- + +- [⚪️ External Monero libraries](external-monero-libraries/intro.md) + - [⚪️ Cryptonight](external-monero-libraries/cryptonight.md) + - [🔴 RandomX](external-monero-libraries/randomx.md) + - [🔴 monero_serai](external-monero-libraries/monero_serai.md) + +--- + +- [⚪️ Benchmarking](benchmarking/intro.md) + - [⚪️ Criterion](benchmarking/criterion.md) + - [⚪️ Harness](benchmarking/harness.md) +- [⚪️ Testing](testing/intro.md) + - [⚪️ Monero data](testing/monero-data.md) + - [⚪️ RPC client](testing/rpc-client.md) + - [⚪️ Spawning `monerod`](testing/spawning-monerod.md) +- [⚪️ Known issues and tradeoffs](known-issues-and-tradeoffs/intro.md) + - [⚪️ Networking](known-issues-and-tradeoffs/networking.md) + - [⚪️ RPC](known-issues-and-tradeoffs/rpc.md) + - [⚪️ Storage](known-issues-and-tradeoffs/storage.md) + +--- + +- [⚪️ Appendix](appendix/intro.md) + - [🟢 Crates](appendix/crates.md) + - [🔴 Contributing](appendix/contributing.md) + - [🔴 Build targets](appendix/build-targets.md) + - [🔴 Protocol book](appendix/protocol-book.md) + - [⚪️ User book](appendix/user-book.md) \ No newline at end of file diff --git a/books/architecture/src/appendix/build-targets.md b/books/architecture/src/appendix/build-targets.md new file mode 100644 index 00000000..495a3d6a --- /dev/null +++ b/books/architecture/src/appendix/build-targets.md @@ -0,0 +1,7 @@ +# Build targets +- x86 +- ARM64 +- Windows +- Linux +- macOS +- FreeBSD(?) diff --git a/books/architecture/src/appendix/contributing.md b/books/architecture/src/appendix/contributing.md new file mode 100644 index 00000000..675937a2 --- /dev/null +++ b/books/architecture/src/appendix/contributing.md @@ -0,0 +1,2 @@ +# Contributing + \ No newline at end of file diff --git a/books/architecture/src/appendix/crates.md b/books/architecture/src/appendix/crates.md new file mode 100644 index 00000000..224e678b --- /dev/null +++ b/books/architecture/src/appendix/crates.md @@ -0,0 +1,61 @@ +# Crates +This is an index of all of Cuprate's in-house crates it uses and maintains. + +They are categorized into groups. + +Crate documentation for each crate can be found by clicking the crate name or by visiting . Documentation can also be built manually by running this at the root of the `cuprate` repository: +```bash +cargo doc --package $CRATE +``` +For example, this will generate and open `cuprate-blockchain` documentation: +```bash +cargo doc --open --package cuprate-blockchain +``` + +## Consensus +| Crate | In-tree path | Purpose | +|-------|--------------|---------| +| [`cuprate-consensus`](https://doc.cuprate.org/cuprate_consensus) | [`consensus/`](https://github.com/Cuprate/cuprate/tree/main/consensus) | TODO +| [`cuprate-consensus-rules`](https://doc.cuprate.org/cuprate_consensus_rules) | [`consensus/rules/`](https://github.com/Cuprate/cuprate/tree/main/consensus-rules) | TODO +| [`cuprate-fast-sync`](https://doc.cuprate.org/cuprate_fast_sync) | [`consensus/fast-sync/`](https://github.com/Cuprate/cuprate/tree/main/consensus/fast-sync) | Fast block synchronization + +## Networking +| Crate | In-tree path | Purpose | +|-------|--------------|---------| +| [`cuprate-epee-encoding`](https://doc.cuprate.org/cuprate_epee_encoding) | [`net/epee-encoding/`](https://github.com/Cuprate/cuprate/tree/main/net/epee-encoding) | Epee (de)serialization +| [`cuprate-fixed-bytes`](https://doc.cuprate.org/cuprate_fixed_bytes) | [`net/fixed-bytes/`](https://github.com/Cuprate/cuprate/tree/main/net/fixed-bytes) | Fixed byte containers backed by `byte::Byte` +| [`cuprate-levin`](https://doc.cuprate.org/cuprate_levin) | [`net/levin/`](https://github.com/Cuprate/cuprate/tree/main/net/levin) | Levin bucket protocol implementation +| [`cuprate-wire`](https://doc.cuprate.org/cuprate_wire) | [`net/wire/`](https://github.com/Cuprate/cuprate/tree/main/net/wire) | TODO + +## P2P +| Crate | In-tree path | Purpose | +|-------|--------------|---------| +| [`cuprate-address-book`](https://doc.cuprate.org/cuprate_address_book) | [`p2p/address-book/`](https://github.com/Cuprate/cuprate/tree/main/p2p/address-book) | TODO +| [`cuprate-async-buffer`](https://doc.cuprate.org/cuprate_async_buffer) | [`p2p/async-buffer/`](https://github.com/Cuprate/cuprate/tree/main/p2p/async-buffer) | A bounded SPSC, FIFO, asynchronous buffer that supports arbitrary weights for values +| [`cuprate-dandelion-tower`](https://doc.cuprate.org/cuprate_dandelion_tower) | [`p2p/dandelion-tower/`](https://github.com/Cuprate/cuprate/tree/main/p2p/dandelion-tower) | TODO +| [`cuprate-p2p`](https://doc.cuprate.org/cuprate_p2p) | [`p2p/p2p/`](https://github.com/Cuprate/cuprate/tree/main/p2p/p2p) | TODO +| [`cuprate-p2p-core`](https://doc.cuprate.org/cuprate_p2p_core) | [`p2p/p2p-core/`](https://github.com/Cuprate/cuprate/tree/main/p2p/p2p-core) | TODO + +## Storage +| Crate | In-tree path | Purpose | +|-------|--------------|---------| +| [`cuprate-blockchain`](https://doc.cuprate.org/cuprate_blockchain) | [`storage/blockchain/`](https://github.com/Cuprate/cuprate/tree/main/storage/blockchain) | Blockchain database built on-top of `cuprate-database` & `cuprate-database-service` +| [`cuprate-database`](https://doc.cuprate.org/cuprate_database) | [`storage/database/`](https://github.com/Cuprate/cuprate/tree/main/storage/database) | Pure database abstraction +| [`cuprate-database-service`](https://doc.cuprate.org/cuprate_database_service) | [`storage/database-service/`](https://github.com/Cuprate/cuprate/tree/main/storage/database-service) | `tower::Service` + thread-pool abstraction built on-top of `cuprate-database` +| [`cuprate-txpool`](https://doc.cuprate.org/cuprate_txpool) | [`storage/txpool/`](https://github.com/Cuprate/cuprate/tree/main/storage/txpool) | Transaction pool database built on-top of `cuprate-database` & `cuprate-database-service` + +## RPC +| Crate | In-tree path | Purpose | +|-------|--------------|---------| +| [`cuprate-json-rpc`](https://doc.cuprate.org/cuprate_json_rpc) | [`rpc/json-rpc/`](https://github.com/Cuprate/cuprate/tree/main/rpc/json-rpc) | JSON-RPC 2.0 implementation +| [`cuprate-rpc-types`](https://doc.cuprate.org/cuprate_rpc_types) | [`rpc/types/`](https://github.com/Cuprate/cuprate/tree/main/rpc/types) | Monero RPC types and traits +| [`cuprate-rpc-interface`](https://doc.cuprate.org/cuprate_rpc_interface) | [`rpc/interface/`](https://github.com/Cuprate/cuprate/tree/main/rpc/interface) | RPC interface & routing + +## 1-off crates +| Crate | In-tree path | Purpose | +|-------|--------------|---------| +| [`cuprate-cryptonight`](https://doc.cuprate.org/cuprate_cryptonight) | [`cryptonight/`](https://github.com/Cuprate/cuprate/tree/main/cryptonight) | CryptoNight hash functions +| [`cuprate-pruning`](https://doc.cuprate.org/cuprate_pruning) | [`pruning/`](https://github.com/Cuprate/cuprate/tree/main/pruning) | Monero pruning logic/types +| [`cuprate-helper`](https://doc.cuprate.org/cuprate_helper) | [`helper/`](https://github.com/Cuprate/cuprate/tree/main/helper) | Kitchen-sink helper crate for Cuprate +| [`cuprate-test-utils`](https://doc.cuprate.org/cuprate_test_utils) | [`test-utils/`](https://github.com/Cuprate/cuprate/tree/main/test-utils) | Testing utilities for Cuprate +| [`cuprate-types`](https://doc.cuprate.org/cuprate_types) | [`types/`](https://github.com/Cuprate/cuprate/tree/main/types) | Shared types across Cuprate diff --git a/books/architecture/src/appendix/intro.md b/books/architecture/src/appendix/intro.md new file mode 100644 index 00000000..fad5ae45 --- /dev/null +++ b/books/architecture/src/appendix/intro.md @@ -0,0 +1 @@ +# Appendix diff --git a/books/architecture/src/appendix/protocol-book.md b/books/architecture/src/appendix/protocol-book.md new file mode 100644 index 00000000..a855b732 --- /dev/null +++ b/books/architecture/src/appendix/protocol-book.md @@ -0,0 +1,2 @@ +# Protocol book + \ No newline at end of file diff --git a/books/architecture/src/appendix/user-book.md b/books/architecture/src/appendix/user-book.md new file mode 100644 index 00000000..0f124765 --- /dev/null +++ b/books/architecture/src/appendix/user-book.md @@ -0,0 +1 @@ +# ⚪️ User book diff --git a/books/architecture/src/benchmarking/criterion.md b/books/architecture/src/benchmarking/criterion.md new file mode 100644 index 00000000..e9d61e65 --- /dev/null +++ b/books/architecture/src/benchmarking/criterion.md @@ -0,0 +1 @@ +# ⚪️ Criterion diff --git a/books/architecture/src/benchmarking/harness.md b/books/architecture/src/benchmarking/harness.md new file mode 100644 index 00000000..6f82b523 --- /dev/null +++ b/books/architecture/src/benchmarking/harness.md @@ -0,0 +1 @@ +# ⚪️ Harness diff --git a/books/architecture/src/benchmarking/intro.md b/books/architecture/src/benchmarking/intro.md new file mode 100644 index 00000000..f043a0ba --- /dev/null +++ b/books/architecture/src/benchmarking/intro.md @@ -0,0 +1 @@ +# ⚪️ Benchmarking diff --git a/books/architecture/src/binary/cli.md b/books/architecture/src/binary/cli.md new file mode 100644 index 00000000..1c515f47 --- /dev/null +++ b/books/architecture/src/binary/cli.md @@ -0,0 +1 @@ +# ⚪️ CLI diff --git a/books/architecture/src/binary/config.md b/books/architecture/src/binary/config.md new file mode 100644 index 00000000..c9582d09 --- /dev/null +++ b/books/architecture/src/binary/config.md @@ -0,0 +1 @@ +# ⚪️ Config diff --git a/books/architecture/src/binary/intro.md b/books/architecture/src/binary/intro.md new file mode 100644 index 00000000..dea12faf --- /dev/null +++ b/books/architecture/src/binary/intro.md @@ -0,0 +1 @@ +# ⚪️ Binary diff --git a/books/architecture/src/binary/logging.md b/books/architecture/src/binary/logging.md new file mode 100644 index 00000000..c7c88a3d --- /dev/null +++ b/books/architecture/src/binary/logging.md @@ -0,0 +1 @@ +# ⚪️ Logging diff --git a/books/architecture/src/birds-eye-view/components.md b/books/architecture/src/birds-eye-view/components.md new file mode 100644 index 00000000..19a17e2f --- /dev/null +++ b/books/architecture/src/birds-eye-view/components.md @@ -0,0 +1 @@ +# ⚪️ Components diff --git a/books/architecture/src/birds-eye-view/intro.md b/books/architecture/src/birds-eye-view/intro.md new file mode 100644 index 00000000..5ee2eb33 --- /dev/null +++ b/books/architecture/src/birds-eye-view/intro.md @@ -0,0 +1 @@ +# ⚪️ Bird's eye view diff --git a/books/architecture/src/birds-eye-view/map.md b/books/architecture/src/birds-eye-view/map.md new file mode 100644 index 00000000..1bde9943 --- /dev/null +++ b/books/architecture/src/birds-eye-view/map.md @@ -0,0 +1 @@ +# ⚪️ Map diff --git a/books/architecture/src/consensus/intro.md b/books/architecture/src/consensus/intro.md new file mode 100644 index 00000000..32013b62 --- /dev/null +++ b/books/architecture/src/consensus/intro.md @@ -0,0 +1 @@ +# ⚪️ Consensus diff --git a/books/architecture/src/consensus/todo.md b/books/architecture/src/consensus/todo.md new file mode 100644 index 00000000..460d4457 --- /dev/null +++ b/books/architecture/src/consensus/todo.md @@ -0,0 +1 @@ +# ⚪️ TODO diff --git a/books/architecture/src/consensus/verifier.md b/books/architecture/src/consensus/verifier.md new file mode 100644 index 00000000..128a3b0f --- /dev/null +++ b/books/architecture/src/consensus/verifier.md @@ -0,0 +1 @@ +# ⚪️ Verifier diff --git a/books/architecture/src/cuprate-architecture.md b/books/architecture/src/cuprate-architecture.md new file mode 100644 index 00000000..3c6c073e --- /dev/null +++ b/books/architecture/src/cuprate-architecture.md @@ -0,0 +1,22 @@ +# Cuprate Architecture +WIP + +[Cuprate](https://github.com/Cuprate/cuprate)'s architecture book. + +Sections are notated with colors indicating how complete they are: + +| Color | Meaning | +|-------|---------| +| ⚪️ | Empty +| 🔴 | Severely lacking information +| 🟠 | Lacking some information +| 🟡 | Almost ready +| 🟢 | OK + +--- + +Continue to the next chapter by clicking the right `>` button, or by selecting it on the left side. + +All chapters are viewable by clicking the top-left `☰` button. + +The entire book can searched by clicking the top-left 🔍 button. \ No newline at end of file diff --git a/books/architecture/src/external-monero-libraries/cryptonight.md b/books/architecture/src/external-monero-libraries/cryptonight.md new file mode 100644 index 00000000..80647b0e --- /dev/null +++ b/books/architecture/src/external-monero-libraries/cryptonight.md @@ -0,0 +1 @@ +# ⚪️ Cryptonight diff --git a/books/architecture/src/external-monero-libraries/intro.md b/books/architecture/src/external-monero-libraries/intro.md new file mode 100644 index 00000000..440a3440 --- /dev/null +++ b/books/architecture/src/external-monero-libraries/intro.md @@ -0,0 +1 @@ +# ⚪️ External Monero libraries diff --git a/books/architecture/src/external-monero-libraries/monero_serai.md b/books/architecture/src/external-monero-libraries/monero_serai.md new file mode 100644 index 00000000..f1567b1a --- /dev/null +++ b/books/architecture/src/external-monero-libraries/monero_serai.md @@ -0,0 +1,2 @@ +# monero_serai + diff --git a/books/architecture/src/external-monero-libraries/randomx.md b/books/architecture/src/external-monero-libraries/randomx.md new file mode 100644 index 00000000..77051519 --- /dev/null +++ b/books/architecture/src/external-monero-libraries/randomx.md @@ -0,0 +1,2 @@ +# RandomX + \ No newline at end of file diff --git a/books/architecture/src/foreword.md b/books/architecture/src/foreword.md new file mode 100644 index 00000000..c85f18b2 --- /dev/null +++ b/books/architecture/src/foreword.md @@ -0,0 +1,36 @@ +# Foreword +Monero[^1] is a large software project, coming in at 329k lines of C++, C, headers, and make files.[^2] It is directly responsible for 2.6 billion dollars worth of value.[^3] It has had over 400 contributors, more if counting unnamed contributions.[^4] It has over 10,000 node operators and a large active userbase.[^5] + +The project wasn't always this big, but somewhere in the midst of contributors coming and going, various features being added, bugs being fixed, and celebrated cryptography being implemented - there was an aspect that was lost by the project that it could not easily gain again: **maintainability**. + +Within large and complicated software projects, there is an important transfer of knowledge that must occur for long-term survival. Much like an organism that must eventually pass the torch onto the next generation, projects must do the same for future contributors. + +However, newcomers often lack experience, past contributors might not be around, and current maintainers may be too busy. For whatever reason, this transfer of knowledge is not always smooth. + +There is a solution to this problem: **documentation**. + +The activity of writing the what, where, why, and how of the solutions to technical problems can be done in an author's lonesome. + +The activity of reading these ideas can be done by future readers at any time without permission. + +These readers may be new prospective contributors, it may be the current maintainers, it may be researchers, it may be users of various scale. Whoever it may be, documentation acts as the link between the past and present; a bottle of wisdom thrown into the river of time for future participants to open. + +This book is the manifestation of this will, for Cuprate[^6], an alternative Monero node. It documents Cuprate's implementation from head-to-toe such that in the case of a contributor's untimely disappearance, the project can continue. + +People come and go, documentation is forever. + +— hinto-janai + +--- + +[^1]: [`monero-project/monero`](https://github.com/monero-project/monero) + +[^2]: `git ls-files | grep "\.cpp$\|\.h$\|\.c$\|CMake" | xargs cat | wc -l` on [`cc73fe7`](https://github.com/monero-project/monero/tree/cc73fe71162d564ffda8e549b79a350bca53c454) + +[^3]: 2024-05-24: $143.55 USD * 18,151,608 XMR = $2,605,663,258 + +[^4]: `git log --all --pretty="%an" | sort -u | wc -l` on [`cc73fe7`](https://github.com/monero-project/monero/tree/cc73fe71162d564ffda8e549b79a350bca53c454) + +[^5]: + +[^6]: \ No newline at end of file diff --git a/books/architecture/src/formats-protocols-types/cuprate-helper.md b/books/architecture/src/formats-protocols-types/cuprate-helper.md new file mode 100644 index 00000000..62278297 --- /dev/null +++ b/books/architecture/src/formats-protocols-types/cuprate-helper.md @@ -0,0 +1 @@ +# ⚪️ cuprate_helper diff --git a/books/architecture/src/formats-protocols-types/cuprate-types.md b/books/architecture/src/formats-protocols-types/cuprate-types.md new file mode 100644 index 00000000..1069ce5b --- /dev/null +++ b/books/architecture/src/formats-protocols-types/cuprate-types.md @@ -0,0 +1 @@ +# ⚪️ cuprate_types diff --git a/books/architecture/src/formats-protocols-types/epee.md b/books/architecture/src/formats-protocols-types/epee.md new file mode 100644 index 00000000..4c0b17ed --- /dev/null +++ b/books/architecture/src/formats-protocols-types/epee.md @@ -0,0 +1 @@ +# ⚪️ Epee diff --git a/books/architecture/src/formats-protocols-types/intro.md b/books/architecture/src/formats-protocols-types/intro.md new file mode 100644 index 00000000..77052fd5 --- /dev/null +++ b/books/architecture/src/formats-protocols-types/intro.md @@ -0,0 +1 @@ +# ⚪️ Formats, protocols, types diff --git a/books/architecture/src/formats-protocols-types/levin.md b/books/architecture/src/formats-protocols-types/levin.md new file mode 100644 index 00000000..72a88cc1 --- /dev/null +++ b/books/architecture/src/formats-protocols-types/levin.md @@ -0,0 +1 @@ +# ⚪️ Levin diff --git a/books/architecture/src/formats-protocols-types/monero-serai.md b/books/architecture/src/formats-protocols-types/monero-serai.md new file mode 100644 index 00000000..139af8ee --- /dev/null +++ b/books/architecture/src/formats-protocols-types/monero-serai.md @@ -0,0 +1 @@ +# ⚪️ monero_serai diff --git a/books/architecture/src/instrumentation/data-collection.md b/books/architecture/src/instrumentation/data-collection.md new file mode 100644 index 00000000..7ea3d9fc --- /dev/null +++ b/books/architecture/src/instrumentation/data-collection.md @@ -0,0 +1 @@ +# ⚪️ Data collection diff --git a/books/architecture/src/instrumentation/intro.md b/books/architecture/src/instrumentation/intro.md new file mode 100644 index 00000000..33640dd2 --- /dev/null +++ b/books/architecture/src/instrumentation/intro.md @@ -0,0 +1,2 @@ +# Instrumentation +Cuprate is built with [instrumentation](https://en.wikipedia.org/wiki/Instrumentation) in mind. \ No newline at end of file diff --git a/books/architecture/src/instrumentation/logging.md b/books/architecture/src/instrumentation/logging.md new file mode 100644 index 00000000..c7c88a3d --- /dev/null +++ b/books/architecture/src/instrumentation/logging.md @@ -0,0 +1 @@ +# ⚪️ Logging diff --git a/books/architecture/src/intro/how-to-use-this-book.md b/books/architecture/src/intro/how-to-use-this-book.md new file mode 100644 index 00000000..7664e04d --- /dev/null +++ b/books/architecture/src/intro/how-to-use-this-book.md @@ -0,0 +1,5 @@ +# How to use this book + +## Maintainers +## Contributors +## Researchers \ No newline at end of file diff --git a/books/architecture/src/intro/intro.md b/books/architecture/src/intro/intro.md new file mode 100644 index 00000000..db2603ca --- /dev/null +++ b/books/architecture/src/intro/intro.md @@ -0,0 +1,15 @@ +# Intro +[Cuprate](https://github.com/Cuprate/cuprate) is an alternative [Monero](https://getmonero.org) node implementation. + +This book describes Cuprate's architecture, ranging from small things like database pruning to larger meta-components like the networking stack. + +A brief overview of some aspects covered within this book: +- Component designs +- Implementation details +- File location and purpose +- Design decisions and tradeoffs +- Things in relation to `monerod` +- Dependency usage + +## Source code +The source files for this book can be found on at: . \ No newline at end of file diff --git a/books/architecture/src/intro/required-knowledge.md b/books/architecture/src/intro/required-knowledge.md new file mode 100644 index 00000000..3262f059 --- /dev/null +++ b/books/architecture/src/intro/required-knowledge.md @@ -0,0 +1,28 @@ +# Required knowledge + +## General +- Rust +- Monero +- System design + +## Components +### Storage +- Embedded databases +- LMDB +- redb + +### RPC +- `axum` +- `tower` +- `async` +- JSON-RPC 2.0 +- Epee + +### Networking +- `tower` +- `tokio` +- `async` +- Levin + +### Instrumentation +- `tracing` diff --git a/books/architecture/src/intro/who-this-book-is-for.md b/books/architecture/src/intro/who-this-book-is-for.md new file mode 100644 index 00000000..4b5be2b5 --- /dev/null +++ b/books/architecture/src/intro/who-this-book-is-for.md @@ -0,0 +1,31 @@ +# Who this book is for + +## Maintainers +As mentioned in [`Foreword`](../foreword.md), the group of people that benefit from this book's value the most by far are the current and future Cuprate maintainers. + +Cuprate's system design is documented in this book such that if you were ever to build it again from scratch, you would have an excellent guide on how to do such, and also where improvements could be made. + +Practically, what that means for maintainers is that it acts as _the_ reference. During maintenance, it is quite valuable to have a book that contains condensed knowledge on the behavior of components, or how certain code works, or why it was built a certain way. + +## Contributors +Contributors also have access to the inner-workings of Cuprate via this book, which helps when making larger contributions. + +Design decisions and implementation details notated in this book helps answer questions such as: +- Why is it done this way? +- Why can it _not_ be done this way? +- Were other methods attempted? + +Cuprate's testing and benchmarking suites, unknown to new contributors, are also documented within this book. + +## Researchers +This book contains the why, where, and how of the _implementation_ of formal research. + +Although it is an informal specification, this book still acts as a more accessible overview of Cuprate compared to examining the codebase itself. + +## Operators & users +This book is not a practical guide for using Cuprate itself. + +For configuration, data collection (also important for researchers), and other practical usage, see [Cuprate's user book](https://user.cuprate.org). + +## Observers +Anyone curious enough is free to learn the inner-workings of Cuprate via this book, and maybe even contribute someday. \ No newline at end of file diff --git a/books/architecture/src/known-issues-and-tradeoffs/intro.md b/books/architecture/src/known-issues-and-tradeoffs/intro.md new file mode 100644 index 00000000..20ab7b5c --- /dev/null +++ b/books/architecture/src/known-issues-and-tradeoffs/intro.md @@ -0,0 +1 @@ +# ⚪️ Known issues and tradeoffs diff --git a/books/architecture/src/known-issues-and-tradeoffs/networking.md b/books/architecture/src/known-issues-and-tradeoffs/networking.md new file mode 100644 index 00000000..20487cba --- /dev/null +++ b/books/architecture/src/known-issues-and-tradeoffs/networking.md @@ -0,0 +1 @@ +# ⚪️ Networking diff --git a/books/architecture/src/known-issues-and-tradeoffs/rpc.md b/books/architecture/src/known-issues-and-tradeoffs/rpc.md new file mode 100644 index 00000000..3337f379 --- /dev/null +++ b/books/architecture/src/known-issues-and-tradeoffs/rpc.md @@ -0,0 +1 @@ +# ⚪️ RPC diff --git a/books/architecture/src/known-issues-and-tradeoffs/storage.md b/books/architecture/src/known-issues-and-tradeoffs/storage.md new file mode 100644 index 00000000..214cf15d --- /dev/null +++ b/books/architecture/src/known-issues-and-tradeoffs/storage.md @@ -0,0 +1 @@ +# ⚪️ Storage diff --git a/books/architecture/src/networking/dandelion.md b/books/architecture/src/networking/dandelion.md new file mode 100644 index 00000000..30916b7b --- /dev/null +++ b/books/architecture/src/networking/dandelion.md @@ -0,0 +1 @@ +# ⚪️ Dandelion++ diff --git a/books/architecture/src/networking/i2p.md b/books/architecture/src/networking/i2p.md new file mode 100644 index 00000000..986ab2a9 --- /dev/null +++ b/books/architecture/src/networking/i2p.md @@ -0,0 +1 @@ +# ⚪️ i2p diff --git a/books/architecture/src/networking/intro.md b/books/architecture/src/networking/intro.md new file mode 100644 index 00000000..20487cba --- /dev/null +++ b/books/architecture/src/networking/intro.md @@ -0,0 +1 @@ +# ⚪️ Networking diff --git a/books/architecture/src/networking/ipv4-ipv6.md b/books/architecture/src/networking/ipv4-ipv6.md new file mode 100644 index 00000000..07339b48 --- /dev/null +++ b/books/architecture/src/networking/ipv4-ipv6.md @@ -0,0 +1 @@ +# ⚪️ IPv4/IPv6 diff --git a/books/architecture/src/networking/p2p.md b/books/architecture/src/networking/p2p.md new file mode 100644 index 00000000..11b20155 --- /dev/null +++ b/books/architecture/src/networking/p2p.md @@ -0,0 +1 @@ +# ⚪️ P2P diff --git a/books/architecture/src/networking/proxy.md b/books/architecture/src/networking/proxy.md new file mode 100644 index 00000000..bd9e5f73 --- /dev/null +++ b/books/architecture/src/networking/proxy.md @@ -0,0 +1 @@ +# ⚪️ Proxy diff --git a/books/architecture/src/networking/tor.md b/books/architecture/src/networking/tor.md new file mode 100644 index 00000000..cc0a809b --- /dev/null +++ b/books/architecture/src/networking/tor.md @@ -0,0 +1 @@ +# ⚪️ Tor diff --git a/books/architecture/src/resource-model/concurrency-and-parallelism/intro.md b/books/architecture/src/resource-model/concurrency-and-parallelism/intro.md new file mode 100644 index 00000000..2cca1808 --- /dev/null +++ b/books/architecture/src/resource-model/concurrency-and-parallelism/intro.md @@ -0,0 +1,32 @@ +# Concurrency and parallelism +It is incumbent upon software like Cuprate to take advantage of today's highly parallel hardware as much as practically possible. + +With that said, programs must setup guardrails when operating in a concurrent and parallel manner, [for correctness and safety](https://en.wikipedia.org/wiki/Concurrency_(computer_science)). + +There are "synchronization primitives" that help with this, common ones being: +- [Locks](https://en.wikipedia.org/wiki/Lock_(computer_science)) +- [Channels](https://en.wikipedia.org/wiki/Channel_(programming)) +- [Atomics](https://en.wikipedia.org/wiki/Linearizability#Primitive_atomic_instructions) + +These tools are relatively easy to use in isolation, but trickier to do so when considering the entire system. It is not uncommon for _the_ bottleneck to be the [poor orchastration](https://en.wikipedia.org/wiki/Starvation_(computer_science)) of these primitives. + +## Analogy +A common analogy for a parallel system is an intersection. + +Like a parallel computer system, an intersection contains: +1. **Parallelism:** multiple individual units that want to move around (cars, pedestrians, etc) +1. **Synchronization primitives:** traffic lights, car lights, walk signals + +In theory, the amount of "work" the units can do is only limited by the speed of the units themselves, but in practice, the slow cascading reaction speeds between all units, the frequent hiccups that can occur, and the synchronization primitives themselves become bottlenecks far before the maximum speed of any unit is reached. + +A car that hogs the middle of the intersection on the wrong light is akin to a system thread holding onto a lock longer than it should be - it degrades total system output. + +Unlike humans however, computer systems at least have the potential to move at lightning speeds, but only if the above synchronization primitives are used correctly. + +## Goal +To aid the long-term maintenance of highly concurrent and parallel code, this section documents: +1. All system threads spawned and maintained +1. All major sections where synchronization primitives are used +1. The asynchronous behavior of some components + +and how these compose together efficiently in Cuprate. \ No newline at end of file diff --git a/books/architecture/src/resource-model/concurrency-and-parallelism/map.md b/books/architecture/src/resource-model/concurrency-and-parallelism/map.md new file mode 100644 index 00000000..1bde9943 --- /dev/null +++ b/books/architecture/src/resource-model/concurrency-and-parallelism/map.md @@ -0,0 +1 @@ +# ⚪️ Map diff --git a/books/architecture/src/resource-model/concurrency-and-parallelism/the-block-downloader.md b/books/architecture/src/resource-model/concurrency-and-parallelism/the-block-downloader.md new file mode 100644 index 00000000..c0dccba4 --- /dev/null +++ b/books/architecture/src/resource-model/concurrency-and-parallelism/the-block-downloader.md @@ -0,0 +1 @@ +# ⚪️ The block downloader diff --git a/books/architecture/src/resource-model/concurrency-and-parallelism/the-database.md b/books/architecture/src/resource-model/concurrency-and-parallelism/the-database.md new file mode 100644 index 00000000..32e4b622 --- /dev/null +++ b/books/architecture/src/resource-model/concurrency-and-parallelism/the-database.md @@ -0,0 +1 @@ +# ⚪️ The database diff --git a/books/architecture/src/resource-model/concurrency-and-parallelism/the-rpc-server.md b/books/architecture/src/resource-model/concurrency-and-parallelism/the-rpc-server.md new file mode 100644 index 00000000..cd654cf6 --- /dev/null +++ b/books/architecture/src/resource-model/concurrency-and-parallelism/the-rpc-server.md @@ -0,0 +1 @@ +# ⚪️ The RPC server diff --git a/books/architecture/src/resource-model/concurrency-and-parallelism/the-verifier.md b/books/architecture/src/resource-model/concurrency-and-parallelism/the-verifier.md new file mode 100644 index 00000000..eeaedc61 --- /dev/null +++ b/books/architecture/src/resource-model/concurrency-and-parallelism/the-verifier.md @@ -0,0 +1 @@ +# ⚪️ The verifier diff --git a/books/architecture/src/resource-model/concurrency-and-parallelism/thread-exit.md b/books/architecture/src/resource-model/concurrency-and-parallelism/thread-exit.md new file mode 100644 index 00000000..39259753 --- /dev/null +++ b/books/architecture/src/resource-model/concurrency-and-parallelism/thread-exit.md @@ -0,0 +1 @@ +# ⚪️ Thread exit diff --git a/books/architecture/src/resource-model/file-system.md b/books/architecture/src/resource-model/file-system.md new file mode 100644 index 00000000..b67ca07d --- /dev/null +++ b/books/architecture/src/resource-model/file-system.md @@ -0,0 +1 @@ +# ⚪️ File system diff --git a/books/architecture/src/resource-model/intro.md b/books/architecture/src/resource-model/intro.md new file mode 100644 index 00000000..28d1dd61 --- /dev/null +++ b/books/architecture/src/resource-model/intro.md @@ -0,0 +1 @@ +# ⚪️ Resource model diff --git a/books/architecture/src/resource-model/memory.md b/books/architecture/src/resource-model/memory.md new file mode 100644 index 00000000..e3624b5f --- /dev/null +++ b/books/architecture/src/resource-model/memory.md @@ -0,0 +1 @@ +# ⚪️ Memory diff --git a/books/architecture/src/resource-model/sockets.md b/books/architecture/src/resource-model/sockets.md new file mode 100644 index 00000000..0d590ca4 --- /dev/null +++ b/books/architecture/src/resource-model/sockets.md @@ -0,0 +1 @@ +# ⚪️ Sockets diff --git a/books/architecture/src/rpc/handler.md b/books/architecture/src/rpc/handler.md new file mode 100644 index 00000000..fffa45f6 --- /dev/null +++ b/books/architecture/src/rpc/handler.md @@ -0,0 +1 @@ +# ⚪️ Handler diff --git a/books/architecture/src/rpc/interface.md b/books/architecture/src/rpc/interface.md new file mode 100644 index 00000000..541b7449 --- /dev/null +++ b/books/architecture/src/rpc/interface.md @@ -0,0 +1 @@ +# ⚪️ Interface diff --git a/books/architecture/src/rpc/intro.md b/books/architecture/src/rpc/intro.md new file mode 100644 index 00000000..dcfc82b4 --- /dev/null +++ b/books/architecture/src/rpc/intro.md @@ -0,0 +1,3 @@ +# RPC +- +- \ No newline at end of file diff --git a/books/architecture/src/rpc/methods/intro.md b/books/architecture/src/rpc/methods/intro.md new file mode 100644 index 00000000..d4a3a15b --- /dev/null +++ b/books/architecture/src/rpc/methods/intro.md @@ -0,0 +1 @@ +# ⚪️ Methods diff --git a/books/architecture/src/rpc/router.md b/books/architecture/src/rpc/router.md new file mode 100644 index 00000000..1827dd31 --- /dev/null +++ b/books/architecture/src/rpc/router.md @@ -0,0 +1 @@ +# ⚪️ Router diff --git a/books/architecture/src/rpc/types/binary.md b/books/architecture/src/rpc/types/binary.md new file mode 100644 index 00000000..dea12faf --- /dev/null +++ b/books/architecture/src/rpc/types/binary.md @@ -0,0 +1 @@ +# ⚪️ Binary diff --git a/books/architecture/src/rpc/types/intro.md b/books/architecture/src/rpc/types/intro.md new file mode 100644 index 00000000..22e430cd --- /dev/null +++ b/books/architecture/src/rpc/types/intro.md @@ -0,0 +1 @@ +# ⚪️ Types diff --git a/books/architecture/src/rpc/types/json.md b/books/architecture/src/rpc/types/json.md new file mode 100644 index 00000000..0bf93514 --- /dev/null +++ b/books/architecture/src/rpc/types/json.md @@ -0,0 +1 @@ +# ⚪️ JSON diff --git a/books/architecture/src/rpc/types/other.md b/books/architecture/src/rpc/types/other.md new file mode 100644 index 00000000..49a36cce --- /dev/null +++ b/books/architecture/src/rpc/types/other.md @@ -0,0 +1 @@ +# ⚪️ Other diff --git a/books/architecture/src/storage/blockchain.md b/books/architecture/src/storage/blockchain.md new file mode 100644 index 00000000..60466879 --- /dev/null +++ b/books/architecture/src/storage/blockchain.md @@ -0,0 +1 @@ +# ⚪️ Blockchain diff --git a/books/architecture/src/storage/database-abstraction.md b/books/architecture/src/storage/database-abstraction.md new file mode 100644 index 00000000..b21a192c --- /dev/null +++ b/books/architecture/src/storage/database-abstraction.md @@ -0,0 +1 @@ +# ⚪️ Database abstraction diff --git a/books/architecture/src/storage/intro.md b/books/architecture/src/storage/intro.md new file mode 100644 index 00000000..214cf15d --- /dev/null +++ b/books/architecture/src/storage/intro.md @@ -0,0 +1 @@ +# ⚪️ Storage diff --git a/books/architecture/src/storage/pruning.md b/books/architecture/src/storage/pruning.md new file mode 100644 index 00000000..cfeee698 --- /dev/null +++ b/books/architecture/src/storage/pruning.md @@ -0,0 +1 @@ +# ⚪️ Pruning diff --git a/books/architecture/src/storage/transaction-pool.md b/books/architecture/src/storage/transaction-pool.md new file mode 100644 index 00000000..4eb139b2 --- /dev/null +++ b/books/architecture/src/storage/transaction-pool.md @@ -0,0 +1 @@ +# ⚪️ Transaction pool diff --git a/books/architecture/src/testing/intro.md b/books/architecture/src/testing/intro.md new file mode 100644 index 00000000..397ae90d --- /dev/null +++ b/books/architecture/src/testing/intro.md @@ -0,0 +1 @@ +# ⚪️ Testing diff --git a/books/architecture/src/testing/monero-data.md b/books/architecture/src/testing/monero-data.md new file mode 100644 index 00000000..915af288 --- /dev/null +++ b/books/architecture/src/testing/monero-data.md @@ -0,0 +1 @@ +# ⚪️ Monero data diff --git a/books/architecture/src/testing/rpc-client.md b/books/architecture/src/testing/rpc-client.md new file mode 100644 index 00000000..5a373c29 --- /dev/null +++ b/books/architecture/src/testing/rpc-client.md @@ -0,0 +1 @@ +# ⚪️ RPC client diff --git a/books/architecture/src/testing/spawning-monerod.md b/books/architecture/src/testing/spawning-monerod.md new file mode 100644 index 00000000..15522665 --- /dev/null +++ b/books/architecture/src/testing/spawning-monerod.md @@ -0,0 +1 @@ +# ⚪️ Spawning monerod diff --git a/books/architecture/src/zmq/intro.md b/books/architecture/src/zmq/intro.md new file mode 100644 index 00000000..0b668b33 --- /dev/null +++ b/books/architecture/src/zmq/intro.md @@ -0,0 +1 @@ +# ⚪️ ZMQ diff --git a/books/architecture/src/todo.md b/books/architecture/src/zmq/todo.md similarity index 100% rename from books/architecture/src/todo.md rename to books/architecture/src/zmq/todo.md diff --git a/books/protocol/src/SUMMARY.md b/books/protocol/src/SUMMARY.md index 1a4b1f0e..682e0e74 100644 --- a/books/protocol/src/SUMMARY.md +++ b/books/protocol/src/SUMMARY.md @@ -23,5 +23,14 @@ - [Bulletproofs+](./consensus_rules/transactions/ring_ct/bulletproofs+.md) - [P2P Network](./p2p_network.md) - [Levin Protocol](./p2p_network/levin.md) - - [P2P Messages](./p2p_network/messages.md) + - [Admin Messages](./p2p_network/levin/admin.md) + - [Protocol Messages](./p2p_network/levin/protocol.md) + - [Common Types](./p2p_network/common_types.md) + - [Message Flows](./p2p_network/message_flows.md) + - [Handshake](./p2p_network/message_flows/handshake.md) + - [Timed Sync](./p2p_network/message_flows/timed_sync.md) + - [New Block](./p2p_network/message_flows/new_block.md) + - [New Transactions](./p2p_network/message_flows/new_transactions.md) + - [Chain Sync](./p2p_network/message_flows/chain_sync.md) + - [Get Blocks](./p2p_network/message_flows/get_blocks.md) - [Pruning](./pruning.md) diff --git a/books/protocol/src/p2p_network.md b/books/protocol/src/p2p_network.md index e0d9a799..89bd1be9 100644 --- a/books/protocol/src/p2p_network.md +++ b/books/protocol/src/p2p_network.md @@ -1,3 +1,3 @@ # P2P Network -This chapter contains descriptions of Monero's peer to peer network, including messages, flows, expected responses, etc. +This chapter contains descriptions of Monero's peer to peer network, including messages, flows, etc. diff --git a/books/protocol/src/p2p_network/common_types.md b/books/protocol/src/p2p_network/common_types.md new file mode 100644 index 00000000..0bf29cb2 --- /dev/null +++ b/books/protocol/src/p2p_network/common_types.md @@ -0,0 +1,116 @@ +# Common P2P Types + +This chapter contains definitions of types used in multiple P2P messages. + +### Support Flags + +Support flags specify any protocol extensions the peer supports, currently only the first bit is used: + +`FLUFFY_BLOCKS = 1` - for if the peer supports receiving fluffy blocks. + +### Basic Node Data [^b-n-d] { #basic-node-data } + +| Fields | Type | Description | +|------------------------|---------------------------------------|-------------------------------------------------------------------------------------------| +| `network_id` | A UUID (epee string) | A fixed constant value for a specific network (mainnet,testnet,stagenet) | +| `my_port` | u32 | The peer's inbound port, if the peer does not want inbound connections this should be `0` | +| `rpc_port` | u16 | The peer's RPC port, if the peer does not want inbound connections this should be `0` | +| `rpc_credits_per_hash` | u32 | States how much it costs to use this node in credits per hashes, `0` being free | +| `peer_id` | u64 | A fixed ID for the node, set to 1 for anonymity networks | +| `support_flags` | [support flags](#support-flags) (u32) | Specifies any protocol extensions the peer supports | + +### Core Sync Data [^c-s-d] { #core-sync-data } + +| Fields | Type | Description | +|-------------------------------|------------------------|---------------------------------------------------------------| +| `current_height` | u64 | The current chain height | +| `cumulative_difficulty` | u64 | The low 64 bits of the cumulative difficulty | +| `cumulative_difficulty_top64` | u64 | The high 64 bits of the cumulative difficulty | +| `top_id` | [u8; 32] (epee string) | The hash of the top block | +| `top_version` | u8 | The hardfork version of the top block | +| `pruning_seed` | u32 | THe pruning seed of the node, `0` if the node does no pruning | + +### Network Address [^network-addr] { #network-address } + +Network addresses are serialized differently than other types, the fields needed depend on the `type` field: + +| Fields | Type | Description | +| ------ | --------------------------------------- | ---------------- | +| `type` | u8 | The address type | +| `addr` | An object whose fields depend on `type` | The address | + +#### IPv4 + +`type = 1` + +| Fields | Type | Description | +| -------- | ---- | ---------------- | +| `m_ip` | u32 | The IPv4 address | +| `m_port` | u16 | The port | + + +#### IPv6 + +`type = 2` + +| Fields | Type | Description | +| -------- | ---------------------- | ---------------- | +| `addr` | [u8; 16] (epee string) | The IPv6 address | +| `m_port` | u16 | The port | + +#### Tor + +TODO: + +#### I2p + +TODO: + +### Peer List Entry Base [^pl-entry-base] { #peer-list-entry-base } + +| Fields | Type | Description | +|------------------------|-------------------------------------|-------------------------------------------------------------------------------------------------------| +| `adr` | [Network Address](#network-address) | The address of the peer | +| `id` | u64 | The random, self assigned, ID of this node | +| `last_seen` | i64 | A field marking when this peer was last seen, although this is zeroed before sending over the network | +| `pruning_seed` | u32 | This peer's pruning seed, `0` if the peer does no pruning | +| `rpc_port` | u16 | This node's RPC port, `0` if this peer has no public RPC port. | +| `rpc_credits_per_hash` | u32 | States how much it costs to use this node in credits per hashes, `0` being free | + +### Tx Blob Entry [^tb-entry] { #tx-blob-entry } + +| Fields | Type | Description | +| --------------- | ---------------------- | --------------------------------------- | +| `blob` | bytes (epee string) | The pruned tx blob | +| `prunable_hash` | [u8; 32] (epee string) | The hash of the prunable part of the tx | + +### Block Complete Entry [^bc-entry] { #block-complete-entry } + +| Fields | Type | Description | +|----------------|---------------------|-----------------------------------------------------------| +| `pruned` | bool | True if the block is pruned, false otherwise | +| `block` | bytes (epee string) | The block blob | +| `block_weight` | u64 | The block's weight | +| `txs` | depends on `pruned` | The transaction blobs, the exact type depends on `pruned` | + +If `pruned` is true: + +`txs` is a vector of [Tx Blob Entry](#tx-blob-entry) + +If `pruned` is false: + +`txs` is a vector of bytes. + +--- + +[^b-n-d]: + +[^c-s-d]: + +[^network-addr]: + +[^pl-entry-base]: + +[^tb-entry]: + +[^bc-entry]: diff --git a/books/protocol/src/p2p_network/epee.md b/books/protocol/src/p2p_network/epee.md deleted file mode 100644 index 2f8161d8..00000000 --- a/books/protocol/src/p2p_network/epee.md +++ /dev/null @@ -1,3 +0,0 @@ -# Epee Binary Format - -The epee binary format is described here: TODO diff --git a/books/protocol/src/p2p_network/levin.md b/books/protocol/src/p2p_network/levin.md index de746606..ec92f7d7 100644 --- a/books/protocol/src/p2p_network/levin.md +++ b/books/protocol/src/p2p_network/levin.md @@ -10,16 +10,16 @@ of buckets that will be combined into a single message. ### Bucket Format | Field | Type | Size (bytes) | -| ------ | ----------------------------- | ------------ | +|--------|-------------------------------|--------------| | Header | [BucketHeader](#bucketheader) | 33 | | Body | bytes | dynamic | ### BucketHeader -Format: +Format[^header-format]: | Field | Type | Size (bytes) | -| ---------------- | ------ | ------------ | +|------------------|--------|--------------| | Signature | LE u64 | 8 | | Size | LE u64 | 8 | | Expect Response | bool | 1 | @@ -32,7 +32,7 @@ Format: The signature field is fixed for every bucket and is used to tell apart peers running different protocols. -Its value should be `0x0101010101012101` +Its value should be `0x0101010101012101` [^signature] #### Size @@ -53,7 +53,7 @@ responses should be `1`. #### Flags -This is a bit-flag field that determines what type of bucket this is: +This is a bit-flag field that determines what type of bucket this is[^flags]: | Type | Bits set | | -------------- | ----------- | @@ -66,3 +66,17 @@ This is a bit-flag field that determines what type of bucket this is: #### Protocol Version This is a fixed value of 1. + +## Bucket Body + +All bucket bodies are serialized in the epee binary format which is described here: https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/docs/PORTABLE_STORAGE.md + +Exact message types are described in the next chapters. + +--- + +[^header-format]: + +[^signature]: + +[^flags]: diff --git a/books/protocol/src/p2p_network/levin/admin.md b/books/protocol/src/p2p_network/levin/admin.md new file mode 100644 index 00000000..a7186468 --- /dev/null +++ b/books/protocol/src/p2p_network/levin/admin.md @@ -0,0 +1,102 @@ +# Admin Messages + +This chapter describes admin messages, and documents the current admin messages. Admin messages are a subset of messages that handle connection +creation, making sure connections are still alive, and sharing peer lists. + +## Levin + +All admin messages are in the request/response levin format. This means requests will set the [expect response bit](./levin.md#expect-response) and +responses will set the return code to [`1`](./levin.md#return-code). + +## Messages + +### Handshake + +ID: `1001`[^handshake-id] + +#### Request [^handshake-req] { #handshake-request } + +| Fields | Type | Description | +|----------------|-------------------------------------------------------|--------------------------------------| +| `node_data` | [basic node data](../common_types.md#basic-node-data) | Static information about our node | +| `payload_data` | [core sync data](../common_types.md#core-sync-data) | Information on the node's sync state | + +#### Response [^handshake-res] { #handshake-response } + +| Fields | Type | Description | +|----------------------|--------------------------------------------------------------------------|-----------------------------------------| +| `node_data` | [basic node data](../common_types.md#basic-node-data) | Static information about our node | +| `payload_data` | [core sync data](../common_types.md#core-sync-data) | Information on the node's sync state | +| `local_peerlist_new` | A Vec of [peer list entry base](../common_types.md#peer-list-entry-base) | A list of peers in the node's peer list | + +### Timed Sync + +ID: `1002`[^timed-sync-id] + +#### Request [^timed-sync-req] { #timed-sync-request } + +| Fields | Type | Description | +| -------------- | --------------------------------------------------- | ------------------------------------ | +| `payload_data` | [core sync data](../common_types.md#core-sync-data) | Information on the node's sync state | + +#### Response [^timed-sync-res] { #timed-sync-response } + +| Fields | Type | Description | +|----------------------|--------------------------------------------------------------------------|-----------------------------------------| +| `payload_data` | [core sync data](../common_types.md#core-sync-data) | Information on the node's sync state | +| `local_peerlist_new` | A Vec of [peer list entry base](../common_types.md#peer-list-entry-base) | A list of peers in the node's peer list | + +### Ping + +ID: `1003`[^ping-id] + +#### Request [^ping-req] { #ping-request } + +No data is serialized for a ping request. + +#### Response [^ping-res] { #ping-response } + +| Fields | Type | Description | +| --------- | ------ | --------------------------------- | +| `status` | string | Will be `OK` for successful pings | +| `peer_id` | u64 | The self assigned id of the peer | + +### Request Support Flags + +ID: `1007`[^support-flags] + +#### Request [^sf-req] { #support-flags-request } + +No data is serialized for a support flags request. + +#### Response [^sf-res] { #support-flags-response } + +| Fields | Type | Description | +| --------------- | ---- | ------------------------------------------------------------ | +| `support_flags` | u32 | The peer's [support flags](../common_types.md#support-flags) | + +--- + +[^handshake-id]: + +[^handshake-req]: + +[^handshake-res]: + +[^timed-sync-id]: + +[^timed-sync-req]: + +[^timed-sync-res]: + +[^ping-id]: + +[^ping-req]: + +[^ping-res]: + +[^support-flags]: + +[^sf-req]: + +[^sf-res]: diff --git a/books/protocol/src/p2p_network/levin/protocol.md b/books/protocol/src/p2p_network/levin/protocol.md new file mode 100644 index 00000000..a52ca1da --- /dev/null +++ b/books/protocol/src/p2p_network/levin/protocol.md @@ -0,0 +1,121 @@ +# Protocol Messages + +This chapter describes protocol messages, and documents the current protocol messages. Protocol messages are used to share protocol data +like blocks and transactions. + +## Levin + +All protocol messages are in the notification levin format. Although there are some messages that fall under requests/responses, levin will treat them as notifications. + +All admin messages are in the request/response levin format. This means requests will set the [expect response bit](../levin.md#expect-response) and +responses will set the return code to [`1`](../levin.md#return-code). + +## Messages + +### Notify New Block + +ID: `2001`[^notify-new-block-id] + +| Fields | Type | Description | +| --------------------------- | --------------------------------------------------------------- | ------------------------ | +| `b` | [Block Complete Entry](../common_types.md#block-complete-entry) | The full block | +| `current_blockchain_height` | u64 | The current chain height | + +### Notify New Transactions + +ID: `2002`[^notify-new-transactions-id] + +| Fields | Type | Description | +| ------------------- | ----------------- | ------------------------------------------------------ | +| `txs` | A vector of bytes | The txs | +| `_` | Bytes | Padding to prevent traffic volume analysis | +| `dandelionpp_fluff` | bool | True if this message contains fluff txs, false if stem | + +### Notify Request Get Objects + +ID: `2003`[^notify-request-get-objects-id] + +| Fields | Type | Description | +|----------|----------------------------------------------------|------------------------------------------------------------| +| `blocks` | A vector of [u8; 32] serialized as a single string | The block IDs requested | +| `prune` | bool | True if we want the blocks in pruned form, false otherwise | + +### Notify Response Get Objects + +ID: `2004`[^notify-response-get-objects-id] + +| Fields | Type | Description | +| --------------------------- | --------------------------------------------------------------------------- | ------------------------------ | +| `blocks` | A vector of [Block Complete Entry](../common_types.md#block-complete-entry) | The blocks that were requested | +| `missed_ids` | A vector of [u8; 32] serialized as a single string | IDs of any missed blocks | +| `current_blockchain_height` | u64 | The current blockchain height | + +### Notify Request Chain + +ID: `2006`[^notify-request-chain-id] + +| Fields | Type | Description | +|-------------|----------------------------------------------------|-------------------------------------------------------------------------------------------------------| +| `block_ids` | A vector of [u8; 32] serialized as a single string | A list of block IDs in reverse chronological order, the top and genesis block will always be included | +| `prune` | bool | True if we want the response to contain pruned blocks, false otherwise | + +### Notify Response Chain Entry + +ID: `2007`[^notify-response-chain-entry-id] + +| Fields | Type | Description | +|-------------------------------|----------------------------------------------------|------------------------------------------------| +| `start_height` | u64 | The start height of the entry | +| `total_height` | u64 | The height of the peer's blockchain | +| `cumulative_difficulty` | u64 | The low 64 bits of the cumulative difficulty | +| `cumulative_difficulty_top64` | u64 | The high 64 bits of the cumulative difficulty | +| `m_block_ids` | A vector of [u8; 32] serialized as a single string | The block IDs in this entry | +| `m_block_weights` | A vector of u64 serialized as a single string | The block weights | +| `first_block` | bytes (epee string) | The header of the first block in `m_block_ids` | + +### Notify New Fluffy Block + +ID: `2008`[^notify-new-fluffy-block-id] + +| Fields | Type | Description | +| --------------------------- | --------------------------------------------------------------- | ------------------------------------- | +| `b` | [Block Complete Entry](../common_types.md#block-complete-entry) | The block, may or may not contain txs | +| `current_blockchain_height` | u64 | The current chain height | + +### Notify Request Fluffy Missing Tx + +ID: `2009`[^notify-request-fluffy-missing-tx-id] + +| Fields | Type | Description | +|-----------------------------|-----------------------------------------------|--------------------------------------------| +| `block_hash` | [u8; 32] serialized as a string | The block hash txs are needed from | +| `current_blockchain_height` | u64 | The current chain height | +| `missing_tx_indices` | A vector of u64 serialized as a single string | The indices of the needed txs in the block | + +### Notify Get Txpool Compliment + +ID: `2010`[^notify-get-txpool-compliment-id] + +| Fields | Type | Description | +| -------- | ------------------------------------------- | ---------------------- | +| `hashes` | A vector of [u8; 32] serialized as a string | The current txpool txs | + +--- + +[^notify-new-block-id]: + +[^notify-new-transactions-id]: + +[^notify-request-get-objects-id]: + +[^notify-response-get-objects-id]: + +[^notify-request-chain-id]: + +[^notify-response-chain-entry-id]: + +[^notify-new-fluffy-block-id]: + +[^notify-request-fluffy-missing-tx-id]: + +[^notify-get-txpool-compliment-id]: diff --git a/books/protocol/src/p2p_network/message_flows.md b/books/protocol/src/p2p_network/message_flows.md new file mode 100644 index 00000000..8f1004ce --- /dev/null +++ b/books/protocol/src/p2p_network/message_flows.md @@ -0,0 +1,19 @@ +# Message Flows + +Message flows are sets of messages sent between peers, that achieve an identifiable goal, like a handshake. +Some message flows are complex, involving many message types, whereas others are simple, requiring only 1. + +The message flows here are not every possible request/response. + +When documenting checks on the messages, not all checks are documented, only the ones notable. This should help +to reduce the maintenance burden. + +## Different Flows + +- [Handshakes](./message_flows/handshake.md) +- [Timed Sync](./message_flows/timed_sync.md) +- [New Block](./message_flows/new_block.md) +- [New Transactions](./message_flows/new_transactions.md) +- [Chain Sync](./message_flows/chain_sync.md) +- [Get Blocks](./message_flows/get_blocks.md) + diff --git a/books/protocol/src/p2p_network/message_flows/chain_sync.md b/books/protocol/src/p2p_network/message_flows/chain_sync.md new file mode 100644 index 00000000..1b661324 --- /dev/null +++ b/books/protocol/src/p2p_network/message_flows/chain_sync.md @@ -0,0 +1,28 @@ +# Chain Sync + +Chain sync is the first step in syncing a peer's blockchain, it allows a peers to find the split point in their chains and for the peer +to learn about the missing block IDs. + +## Flow + +The first step is for the initiating peer is to get its compact chain history. The compact chain history must be in reverse chronological +order, with the first block being the top block and the last the genesis, if the only block is the genesis then that only needs to be included +once. The blocks in the middle are not enforced to be at certain locations, however `monerod` will use the top 11 blocks and will then go power +of 2 offsets from then on, i.e. `{13, 17, 25, ...}` + +Then, with the compact history, the initiating peer will send a [request chain](../levin/protocol.md#notify-request-chain) message, the receiving +peer will then find the split point and return a [response chain entry](../levin/protocol.md#notify-response-chain-entry) message. + +The `response chain entry` will contain a list of block IDs with the first being a common ancestor and the rest being the next blocks that come after +that block in the peer's chain. + +### Response Checks + +- There must be an overlapping block.[^res-overlapping-block] +- The amount of returned block IDs must be less than `25,000`.[^res-max-blocks] + +--- + +[^res-overlapping-block]: + +[^res-max-blocks]: \ No newline at end of file diff --git a/books/protocol/src/p2p_network/message_flows/get_blocks.md b/books/protocol/src/p2p_network/message_flows/get_blocks.md new file mode 100644 index 00000000..eacca7f2 --- /dev/null +++ b/books/protocol/src/p2p_network/message_flows/get_blocks.md @@ -0,0 +1,19 @@ +# Get Blocks + +The get block flow is used to download batches of blocks from a peer. + +## Flow + +The initiating peer needs a list of block IDs that the receiving peer has, this can be done with +the [chain sync flow](./chain_sync.md). + +With a list a block IDs the initiating peer will send a [get objects request](../levin/protocol.md#notify-request-get-objects) message, the receiving +peer will then respond with [get objects response](../levin/protocol.md#notify-response-get-objects). + +### Request Checks + +- The amount of blocks must be less than `100`.[^max-block-requests] + +--- + +[^max-block-requests]: diff --git a/books/protocol/src/p2p_network/message_flows/handshake.md b/books/protocol/src/p2p_network/message_flows/handshake.md new file mode 100644 index 00000000..2a4abe13 --- /dev/null +++ b/books/protocol/src/p2p_network/message_flows/handshake.md @@ -0,0 +1,51 @@ +# Handshakes + +Handshakes are used to establish connections to peers. + +## Flow + +The default handshake flow is made up of the connecting peer sending a [handshake request](../levin/admin.md#handshake-request) and the +receiving peer responding with a [handshake response](../levin/admin.md#handshake-response). + +It should be noted that not all other messages are banned during handshakes, for example, support flag requests and even some protocol +requests can be sent. + +### Handshake Request Checks + +The receiving peer will check: + +- The `network_id` is network ID expected.[^network-id] +- The connection is an incoming connection.[^req-incoming-only] +- The peer hasn't already completed a handshake.[^double-handshake] +- If the network zone is public, then the `peer_id` must not be the same as ours.[^same-peer-id] +- The core sync data is not malformed.[^core-sync-data-checks] + +### Handshake Response Checks + +The initiating peer will check: + +- The `network_id` is network ID expected.[^res-network-id] +- The number of peers in the peer list is less than `250`.[^max-peer-list-res] +- All peers in the peer list are in the same zone.[^peers-all-in-same-zone] +- The core sync data is not malformed.[^core-sync-data-checks] +- If the network zone is public, then the `peer_id` must not be the same as ours.[^same-peer-id-res] + +--- + +[^network-id]: + +[^req-incoming-only]: + +[^double-handshake]: + +[^same-peer-id]: + +[^core-sync-data-checks]: + +[^res-network-id]: + +[^max-peer-list-res]: + +[^peers-all-in-same-zone]: + +[^same-peer-id-res]: diff --git a/books/protocol/src/p2p_network/message_flows/new_block.md b/books/protocol/src/p2p_network/message_flows/new_block.md new file mode 100644 index 00000000..452aa44f --- /dev/null +++ b/books/protocol/src/p2p_network/message_flows/new_block.md @@ -0,0 +1,29 @@ +# New Block + +This is used whenever a new block is to be sent to peers. Only the fluffy block flow is described here, as the other method is deprecated. + +## Flow + +First the peer with the new block will send a [new fluffy block](../levin/protocol.md#notify-new-fluffy-block) notification, if the receiving +peer has all the txs in the block then the flow is complete. Otherwise the peer sends a [fluffy missing transactions request](../levin/protocol.md#notify-request-fluffy-missing-tx) +to the first peer, the first peer will then respond with again a [new fluffy block](../levin/protocol.md#notify-new-fluffy-block) notification but +with the transactions requested. + +```bob + + ,-----------. ,----------. + | Initiator | | Receiver | + `-----+-----' `-----+----' + | New Fluffy Block | + |-------------------->| + | | + | Missing Txs Request | + |<- - - - - - - - - - | + | | + | New Fluffy Block | + | - - - - - - - - - ->| + | | + | | + V v +``` + diff --git a/books/protocol/src/p2p_network/message_flows/new_transactions.md b/books/protocol/src/p2p_network/message_flows/new_transactions.md new file mode 100644 index 00000000..2a90a3f4 --- /dev/null +++ b/books/protocol/src/p2p_network/message_flows/new_transactions.md @@ -0,0 +1,16 @@ +# New Transactions + +Monero uses the dandelion++ protocol to pass transactions around the network, this flow just describes the actual tx passing between nodes part. + +## Flow + +This flow is pretty simple, the txs are put into a [new transactions](../levin/protocol.md#notify-new-transactions) notification and sent to +peers. + +Hopefully in the future [this is changed](https://github.com/monero-project/monero/issues/9334). + +There must be no duplicate txs in the notification.[^duplicate-txs] + +--- + +[^duplicate-txs]: \ No newline at end of file diff --git a/books/protocol/src/p2p_network/message_flows/timed_sync.md b/books/protocol/src/p2p_network/message_flows/timed_sync.md new file mode 100644 index 00000000..4d258d72 --- /dev/null +++ b/books/protocol/src/p2p_network/message_flows/timed_sync.md @@ -0,0 +1,28 @@ +# Timed Syncs + +A timed sync request is sent every 60 seconds to make sure the connection is still live. + +## Flow + +First the timed sync initiator will send a [timed sync request](../levin/admin.md#timed-sync-request), the receiver will then +respond with a [timed sync response](../levin/admin.md#timed-sync-response) + +### Timed Sync Request Checks + +- The core sync data is not malformed.[^core-sync-data-checks] + + +### Timed Sync Response Checks + +- The core sync data is not malformed.[^core-sync-data-checks] +- The number of peers in the peer list is less than `250`.[^max-peer-list-res] +- All peers in the peer list are in the same zone.[^peers-all-in-same-zone] + +--- + +[^core-sync-data-checks]: + +[^max-peer-list-res]: + +[^peers-all-in-same-zone]: + diff --git a/books/protocol/src/p2p_network/messages.md b/books/protocol/src/p2p_network/messages.md deleted file mode 100644 index c3f18287..00000000 --- a/books/protocol/src/p2p_network/messages.md +++ /dev/null @@ -1,37 +0,0 @@ -# P2P Messages - -This chapter contains every P2P message. - -## Index - -## Types - -Types used in multiple P2P messages. - -### Support Flags - -Support flags specify any protocol extensions the peer supports, currently only the first bit is used: - -`FLUFFY_BLOCKS = 1` - for if the peer supports receiving fluffy blocks. - -### Basic Node Data - -| Fields | Type (Epee Type) | Description | -| ---------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------- | -| `network_id` | A UUID (String) | A fixed constant value for a specific network (mainnet,testnet,stagenet) | -| `my_port` | u32 (u32) | The peer's inbound port, if the peer does not want inbound connections this should be `0` | -| `rpc_port` | u16 (u16) | The peer's RPC port, if the peer does not want inbound connections this should be `0` | -| `rpc_credits_per_hash` | u32 (u32) | TODO | -| `peer_id` | u64 (u64) | A fixed ID for the node, set to 1 for anonymity networks | -| `support_flags` | [support flags](#support-flags) (u32) | Specifies any protocol extensions the peer supports | - -## Messages - -### Handshake Requests - -levin command: 1001 - -| Fields | Type (Epee Type) | Description | -| ----------- | -------------------------------------------- | ----------- | -| `node_data` | [basic node data](#basic-node-data) (Object) | | -| | | | diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml index c39e1c26..bd3994a7 100644 --- a/consensus/Cargo.toml +++ b/consensus/Cargo.toml @@ -27,6 +27,7 @@ tokio = { workspace = true, features = ["rt"] } tokio-util = { workspace = true } hex = { workspace = true } +rand = { workspace = true } [dev-dependencies] cuprate-test-utils = { path = "../test-utils" } @@ -35,5 +36,6 @@ cuprate-consensus-rules = {path = "./rules", features = ["proptest"]} hex-literal = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"]} +tokio-test = { workspace = true } proptest = { workspace = true } proptest-derive = { workspace = true } \ No newline at end of file diff --git a/consensus/fast-sync/src/create.rs b/consensus/fast-sync/src/create.rs index 65801aa7..8b6e96b9 100644 --- a/consensus/fast-sync/src/create.rs +++ b/consensus/fast-sync/src/create.rs @@ -6,7 +6,10 @@ use tower::{Service, ServiceExt}; use cuprate_blockchain::{ config::ConfigBuilder, cuprate_database::RuntimeError, service::DatabaseReadHandle, }; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use cuprate_fast_sync::{hash_of_hashes, BlockId, HashOfHashes}; @@ -19,7 +22,7 @@ async fn read_batch( let mut block_ids = Vec::::with_capacity(BATCH_SIZE); for height in height_from..(height_from + BATCH_SIZE) { - let request = BCReadRequest::BlockHash(height); + let request = BCReadRequest::BlockHash(height, Chain::Main); let response_channel = handle.ready().await?.call(request); let response = response_channel.await?; diff --git a/consensus/rules/src/blocks.rs b/consensus/rules/src/blocks.rs index a3aa811e..c9f41d9d 100644 --- a/consensus/rules/src/blocks.rs +++ b/consensus/rules/src/blocks.rs @@ -148,7 +148,7 @@ fn block_size_sanity_check( /// Sanity check on the block weight. /// /// ref: -fn check_block_weight( +pub fn check_block_weight( block_weight: usize, median_for_block_reward: usize, ) -> Result<(), BlockError> { @@ -184,7 +184,7 @@ fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> { /// Checks the blocks timestamp is in the valid range. /// /// ref: -fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> { +pub fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> { if block.header.timestamp < median_timestamp || block.header.timestamp > current_unix_timestamp() + BLOCK_FUTURE_TIME_LIMIT { diff --git a/consensus/rules/src/hard_forks.rs b/consensus/rules/src/hard_forks.rs index ba6288cc..6b983149 100644 --- a/consensus/rules/src/hard_forks.rs +++ b/consensus/rules/src/hard_forks.rs @@ -38,7 +38,7 @@ pub enum HardForkError { } /// Information about a given hard-fork. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct HFInfo { height: usize, threshold: usize, @@ -50,7 +50,7 @@ impl HFInfo { } /// Information about every hard-fork Monero has had. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct HFsInfo([HFInfo; NUMB_OF_HARD_FORKS]); impl HFsInfo { @@ -243,7 +243,7 @@ impl HardFork { } /// A struct holding the current voting state of the blockchain. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct HFVotes { votes: [usize; NUMB_OF_HARD_FORKS], vote_list: VecDeque, @@ -293,6 +293,28 @@ impl HFVotes { } } + /// Pop a number of blocks from the top of the cache and push some values into the front of the cache, + /// i.e. the oldest blocks. + /// + /// `old_block_votes` should contain the HFs below the window that now will be in the window after popping + /// blocks from the top. + /// + /// # Panics + /// + /// This will panic if `old_block_votes` contains more HFs than `numb_blocks`. + pub fn reverse_blocks(&mut self, numb_blocks: usize, old_block_votes: Self) { + assert!(old_block_votes.vote_list.len() <= numb_blocks); + + for hf in self.vote_list.drain(self.vote_list.len() - numb_blocks..) { + self.votes[hf as usize - 1] -= 1; + } + + for old_vote in old_block_votes.vote_list.into_iter().rev() { + self.vote_list.push_front(old_vote); + self.votes[old_vote as usize - 1] += 1; + } + } + /// Returns the total votes for a hard-fork. /// /// ref: diff --git a/consensus/src/block.rs b/consensus/src/block.rs index 54bf00d7..1b36eb92 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -12,31 +12,35 @@ use monero_serai::{ block::Block, transaction::{Input, Transaction}, }; -use rayon::prelude::*; use tower::{Service, ServiceExt}; -use tracing::instrument; + +use cuprate_helper::asynch::rayon_spawn_async; +use cuprate_types::{ + AltBlockInformation, VerifiedBlockInformation, VerifiedTransactionInformation, +}; use cuprate_consensus_rules::{ blocks::{ - calculate_pow_hash, check_block, check_block_pow, is_randomx_seed_height, - randomx_seed_height, BlockError, RandomX, + calculate_pow_hash, check_block, check_block_pow, randomx_seed_height, BlockError, RandomX, }, - hard_forks::HardForkError, miner_tx::MinerTxError, ConsensusError, HardFork, }; -use cuprate_helper::asynch::rayon_spawn_async; -use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; use crate::{ - context::{ - rx_vms::RandomXVM, BlockChainContextRequest, BlockChainContextResponse, - RawBlockChainContext, - }, + context::{BlockChainContextRequest, BlockChainContextResponse, RawBlockChainContext}, transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse}, Database, ExtendedConsensusError, }; +mod alt_block; +mod batch_prepare; +mod free; + +use alt_block::sanity_check_alt_block; +use batch_prepare::batch_prepare_main_chain_block; +use free::pull_ordered_transactions; + /// A pre-prepared block with all data needed to verify it, except the block's proof of work. #[derive(Debug)] pub struct PreparedBlockExPow { @@ -53,7 +57,7 @@ pub struct PreparedBlockExPow { /// The block's hash. pub block_hash: [u8; 32], /// The height of the block. - pub height: usize, + pub height: u64, /// The weight of the block's miner transaction. pub miner_tx_weight: usize, @@ -70,7 +74,7 @@ impl PreparedBlockExPow { let (hf_version, hf_vote) = HardFork::from_block_header(&block.header).map_err(BlockError::HardForkError)?; - let Some(Input::Gen(height)) = block.miner_transaction.prefix().inputs.first() else { + let Some(Input::Gen(height)) = block.miner_tx.prefix.inputs.first() else { Err(ConsensusError::Block(BlockError::MinerTxError( MinerTxError::InputNotOfTypeGen, )))? @@ -84,7 +88,7 @@ impl PreparedBlockExPow { block_hash: block.hash(), height: *height, - miner_tx_weight: block.miner_transaction.weight(), + miner_tx_weight: block.miner_tx.weight(), block, }) } @@ -124,7 +128,7 @@ impl PreparedBlock { let (hf_version, hf_vote) = HardFork::from_block_header(&block.header).map_err(BlockError::HardForkError)?; - let Some(Input::Gen(height)) = block.miner_transaction.prefix().inputs.first() else { + let [Input::Gen(height)] = &block.miner_tx.prefix.inputs[..] else { Err(ConsensusError::Block(BlockError::MinerTxError( MinerTxError::InputNotOfTypeGen, )))? @@ -138,12 +142,12 @@ impl PreparedBlock { block_hash: block.hash(), pow_hash: calculate_pow_hash( randomx_vm, - &block.serialize_pow_hash(), + &block.serialize_hashable(), *height, &hf_version, )?, - miner_tx_weight: block.miner_transaction.weight(), + miner_tx_weight: block.miner_tx.weight(), block, }) } @@ -168,12 +172,12 @@ impl PreparedBlock { block_hash: block.block_hash, pow_hash: calculate_pow_hash( randomx_vm, - &block.block.serialize_pow_hash(), + &block.block.serialize_hashable(), block.height, &block.hf_version, )?, - miner_tx_weight: block.block.miner_transaction.weight(), + miner_tx_weight: block.block.miner_tx.weight(), block: block.block, }) } @@ -191,6 +195,7 @@ pub enum VerifyBlockRequest { /// The already prepared block. block: PreparedBlock, /// The full list of transactions for this block, in the order given in `block`. + // TODO: Remove the Arc here txs: Vec>, }, /// Batch prepares a list of blocks and transactions for verification. @@ -198,6 +203,16 @@ pub enum VerifyBlockRequest { /// The list of blocks and their transactions (not necessarily in the order given in the block). blocks: Vec<(Block, Vec)>, }, + /// A request to sanity check an alt block, also returning the cumulative difficulty of the alt chain. + /// + /// Unlike requests to verify main chain blocks, you do not need to add the returned block to the context + /// service, you will still have to add it to the database though. + AltChain { + /// The alt block to sanity check. + block: Block, + /// The alt transactions. + prepared_txs: HashMap<[u8; 32], TransactionVerificationData>, + }, } /// A response from a verify block request. @@ -205,6 +220,8 @@ pub enum VerifyBlockRequest { pub enum VerifyBlockResponse { /// This block is valid. MainChain(VerifiedBlockInformation), + /// The sanity checked alt block. + AltChain(AltBlockInformation), /// A list of prepared blocks for verification, you should call [`VerifyBlockRequest::MainChainPrepped`] on each of the returned /// blocks to fully verify them. MainChainBatchPrepped(Vec<(PreparedBlock, Vec>)>), @@ -296,206 +313,20 @@ where verify_prepped_main_chain_block(block, txs, context_svc, tx_verifier_svc, None) .await } + VerifyBlockRequest::AltChain { + block, + prepared_txs, + } => sanity_check_alt_block(block, prepared_txs, context_svc).await, } } .boxed() } } -/// Batch prepares a list of blocks for verification. -#[instrument(level = "debug", name = "batch_prep_blocks", skip_all, fields(amt = blocks.len()))] -async fn batch_prepare_main_chain_block( - blocks: Vec<(Block, Vec)>, - mut context_svc: C, -) -> Result -where - C: Service< - BlockChainContextRequest, - Response = BlockChainContextResponse, - Error = tower::BoxError, - > + Send - + 'static, - C::Future: Send + 'static, -{ - let (blocks, txs): (Vec<_>, Vec<_>) = blocks.into_iter().unzip(); - - tracing::debug!("Calculating block hashes."); - let blocks: Vec = rayon_spawn_async(|| { - blocks - .into_iter() - .map(PreparedBlockExPow::new) - .collect::, _>>() - }) - .await?; - - let Some(last_block) = blocks.last() else { - return Err(ExtendedConsensusError::NoBlocksToVerify); - }; - - // hard-forks cannot be reversed, so the last block will contain the highest hard fork (provided the - // batch is valid). - let top_hf_in_batch = last_block.hf_version; - - // A Vec of (timestamp, HF) for each block to calculate the expected difficulty for each block. - let mut timestamps_hfs = Vec::with_capacity(blocks.len()); - let mut new_rx_vm = None; - - tracing::debug!("Checking blocks follow each other."); - - // For every block make sure they have the correct height and previous ID - for window in blocks.windows(2) { - let block_0 = &window[0]; - let block_1 = &window[1]; - - // Make sure no blocks in the batch have a higher hard fork than the last block. - if block_0.hf_version > top_hf_in_batch { - Err(ConsensusError::Block(BlockError::HardForkError( - HardForkError::VersionIncorrect, - )))?; - } - - if block_0.block_hash != block_1.block.header.previous - || block_0.height != block_1.height - 1 - { - tracing::debug!("Blocks do not follow each other, verification failed."); - Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))?; - } - - // Cache any potential RX VM seeds as we may need them for future blocks in the batch. - if is_randomx_seed_height(block_0.height) && top_hf_in_batch >= HardFork::V12 { - new_rx_vm = Some((block_0.height, block_0.block_hash)); - } - - timestamps_hfs.push((block_0.block.header.timestamp, block_0.hf_version)) - } - - // Get the current blockchain context. - let BlockChainContextResponse::Context(checked_context) = context_svc - .ready() - .await? - .call(BlockChainContextRequest::GetContext) - .await - .map_err(Into::::into)? - else { - panic!("Context service returned wrong response!"); - }; - - // Calculate the expected difficulties for each block in the batch. - let BlockChainContextResponse::BatchDifficulties(difficulties) = context_svc - .ready() - .await? - .call(BlockChainContextRequest::BatchGetDifficulties( - timestamps_hfs, - )) - .await - .map_err(Into::::into)? - else { - panic!("Context service returned wrong response!"); - }; - - let context = checked_context.unchecked_blockchain_context().clone(); - - // Make sure the blocks follow the main chain. - - if context.chain_height != blocks[0].height { - tracing::debug!("Blocks do not follow main chain, verification failed."); - - Err(ConsensusError::Block(BlockError::MinerTxError( - MinerTxError::InputsHeightIncorrect, - )))?; - } - - if context.top_hash != blocks[0].block.header.previous { - tracing::debug!("Blocks do not follow main chain, verification failed."); - - Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))?; - } - - let mut rx_vms = if top_hf_in_batch < HardFork::V12 { - HashMap::new() - } else { - let BlockChainContextResponse::RxVms(rx_vms) = context_svc - .ready() - .await? - .call(BlockChainContextRequest::GetCurrentRxVm) - .await? - else { - panic!("Blockchain context service returned wrong response!"); - }; - - rx_vms - }; - - // If we have a RX seed in the batch calculate it. - if let Some((new_vm_height, new_vm_seed)) = new_rx_vm { - tracing::debug!("New randomX seed in batch, initialising VM"); - - let new_vm = rayon_spawn_async(move || { - Arc::new(RandomXVM::new(&new_vm_seed).expect("RandomX VM gave an error on set up!")) - }) - .await; - - context_svc - .oneshot(BlockChainContextRequest::NewRXVM(( - new_vm_seed, - new_vm.clone(), - ))) - .await - .map_err(Into::::into)?; - - rx_vms.insert(new_vm_height, new_vm); - } - - tracing::debug!("Calculating PoW and prepping transaction"); - - let blocks = rayon_spawn_async(move || { - blocks - .into_par_iter() - .zip(difficulties) - .zip(txs) - .map(|((block, difficultly), txs)| { - // Calculate the PoW for the block. - let height = block.height; - let block = PreparedBlock::new_prepped( - block, - rx_vms.get(&randomx_seed_height(height)).map(AsRef::as_ref), - )?; - - // Check the PoW - check_block_pow(&block.pow_hash, difficultly).map_err(ConsensusError::Block)?; - - // Now setup the txs. - let mut txs = txs - .into_par_iter() - .map(|tx| { - let tx = TransactionVerificationData::new(tx)?; - Ok::<_, ConsensusError>((tx.tx_hash, tx)) - }) - .collect::, _>>()?; - - // Order the txs correctly. - let mut ordered_txs = Vec::with_capacity(txs.len()); - - for tx_hash in &block.block.transactions { - let tx = txs - .remove(tx_hash) - .ok_or(ExtendedConsensusError::TxsIncludedWithBlockIncorrect)?; - ordered_txs.push(Arc::new(tx)); - } - - Ok((block, ordered_txs)) - }) - .collect::, ExtendedConsensusError>>() - }) - .await?; - - Ok(VerifyBlockResponse::MainChainBatchPrepped(blocks)) -} - /// Verifies a prepared block. async fn verify_main_chain_block( block: Block, - mut txs: HashMap<[u8; 32], TransactionVerificationData>, + txs: HashMap<[u8; 32], TransactionVerificationData>, mut context_svc: C, tx_verifier_svc: TxV, ) -> Result @@ -527,8 +358,9 @@ where ); // Set up the block and just pass it to [`verify_prepped_main_chain_block`] - // We just use the raw `hardfork_version` here, no need to turn it into a `HardFork`. - let rx_vms = if block.header.hardfork_version < 12 { + + // We just use the raw `major_version` here, no need to turn it into a `HardFork`. + let rx_vms = if block.header.major_version < 12 { HashMap::new() } else { let BlockChainContextResponse::RxVms(rx_vms) = context_svc @@ -556,20 +388,11 @@ where .map_err(ConsensusError::Block)?; // Check that the txs included are what we need and that there are not any extra. - - let mut ordered_txs = Vec::with_capacity(txs.len()); - - tracing::debug!("Ordering transactions for block."); - - if !prepped_block.block.transactions.is_empty() { - for tx_hash in &prepped_block.block.transactions { - let tx = txs - .remove(tx_hash) - .ok_or(ExtendedConsensusError::TxsIncludedWithBlockIncorrect)?; - ordered_txs.push(Arc::new(tx)); - } - drop(txs); - } + // TODO: Remove the Arc here + let ordered_txs = pull_ordered_transactions(&prepped_block.block, txs)? + .into_iter() + .map(Arc::new) + .collect(); verify_prepped_main_chain_block( prepped_block, @@ -603,8 +426,7 @@ where } else { let BlockChainContextResponse::Context(checked_context) = context_svc .oneshot(BlockChainContextRequest::GetContext) - .await - .map_err(Into::::into)? + .await? else { panic!("Context service returned wrong response!"); }; @@ -621,12 +443,12 @@ where check_block_pow(&prepped_block.pow_hash, context.next_difficulty) .map_err(ConsensusError::Block)?; - if prepped_block.block.transactions.len() != txs.len() { + if prepped_block.block.txs.len() != txs.len() { return Err(ExtendedConsensusError::TxsIncludedWithBlockIncorrect); } - if !prepped_block.block.transactions.is_empty() { - for (expected_tx_hash, tx) in prepped_block.block.transactions.iter().zip(txs.iter()) { + if !prepped_block.block.txs.is_empty() { + for (expected_tx_hash, tx) in prepped_block.block.txs.iter().zip(txs.iter()) { if expected_tx_hash != &tx.tx_hash { return Err(ExtendedConsensusError::TxsIncludedWithBlockIncorrect); } diff --git a/consensus/src/block/alt_block.rs b/consensus/src/block/alt_block.rs new file mode 100644 index 00000000..cf6f2132 --- /dev/null +++ b/consensus/src/block/alt_block.rs @@ -0,0 +1,304 @@ +//! Alt Blocks +//! +//! Alt blocks are sanity checked by [`sanity_check_alt_block`], that function will also compute the cumulative +//! difficulty of the alt chain so callers will know if they should re-org to the alt chain. +use std::{collections::HashMap, sync::Arc}; + +use monero_serai::{block::Block, transaction::Input}; +use tower::{Service, ServiceExt}; + +use cuprate_consensus_rules::{ + blocks::{ + check_block_pow, check_block_weight, check_timestamp, randomx_seed_height, BlockError, + }, + miner_tx::MinerTxError, + ConsensusError, +}; +use cuprate_helper::asynch::rayon_spawn_async; +use cuprate_types::{AltBlockInformation, Chain, ChainId, VerifiedTransactionInformation}; + +use crate::{ + block::{free::pull_ordered_transactions, PreparedBlock}, + context::{ + difficulty::DifficultyCache, + rx_vms::RandomXVM, + weight::{self, BlockWeightsCache}, + AltChainContextCache, AltChainRequestToken, BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW, + }, + transactions::TransactionVerificationData, + BlockChainContextRequest, BlockChainContextResponse, ExtendedConsensusError, + VerifyBlockResponse, +}; + +/// This function sanity checks an alt-block. +/// +/// Returns [`AltBlockInformation`], which contains the cumulative difficulty of the alt chain. +/// +/// This function only checks the block's PoW and its weight. +pub async fn sanity_check_alt_block( + block: Block, + txs: HashMap<[u8; 32], TransactionVerificationData>, + mut context_svc: C, +) -> Result +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send + + 'static, + C::Future: Send + 'static, +{ + // Fetch the alt-chains context cache. + let BlockChainContextResponse::AltChainContextCache(mut alt_context_cache) = context_svc + .ready() + .await? + .call(BlockChainContextRequest::AltChainContextCache { + prev_id: block.header.previous, + _token: AltChainRequestToken, + }) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + // Check if the block's miner input is formed correctly. + let [Input::Gen(height)] = &block.miner_tx.prefix.inputs[..] else { + Err(ConsensusError::Block(BlockError::MinerTxError( + MinerTxError::InputNotOfTypeGen, + )))? + }; + + if *height != alt_context_cache.chain_height { + Err(ConsensusError::Block(BlockError::MinerTxError( + MinerTxError::InputsHeightIncorrect, + )))? + } + + // prep the alt block. + let prepped_block = { + let rx_vm = alt_rx_vm( + alt_context_cache.chain_height, + block.header.major_version, + alt_context_cache.parent_chain, + &mut alt_context_cache, + &mut context_svc, + ) + .await?; + + rayon_spawn_async(move || PreparedBlock::new(block, rx_vm.as_deref())).await? + }; + + // get the difficulty cache for this alt chain. + let difficulty_cache = alt_difficulty_cache( + prepped_block.block.header.previous, + &mut alt_context_cache, + &mut context_svc, + ) + .await?; + + // Check the alt block timestamp is in the correct range. + if let Some(median_timestamp) = + difficulty_cache.median_timestamp(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW.try_into().unwrap()) + { + check_timestamp(&prepped_block.block, median_timestamp).map_err(ConsensusError::Block)? + }; + + let next_difficulty = difficulty_cache.next_difficulty(&prepped_block.hf_version); + // make sure the block's PoW is valid for this difficulty. + check_block_pow(&prepped_block.pow_hash, next_difficulty).map_err(ConsensusError::Block)?; + + let cumulative_difficulty = difficulty_cache.cumulative_difficulty() + next_difficulty; + + let ordered_txs = pull_ordered_transactions(&prepped_block.block, txs)?; + + let block_weight = + prepped_block.miner_tx_weight + ordered_txs.iter().map(|tx| tx.tx_weight).sum::(); + + let alt_weight_cache = alt_weight_cache( + prepped_block.block.header.previous, + &mut alt_context_cache, + &mut context_svc, + ) + .await?; + + // Check the block weight is below the limit. + check_block_weight( + block_weight, + alt_weight_cache.median_for_block_reward(&prepped_block.hf_version), + ) + .map_err(ConsensusError::Block)?; + + let long_term_weight = weight::calculate_block_long_term_weight( + &prepped_block.hf_version, + block_weight, + alt_weight_cache.median_long_term_weight(), + ); + + // Get the chainID or generate a new one if this is the first alt block in this alt chain. + let chain_id = *alt_context_cache + .chain_id + .get_or_insert_with(|| ChainId(rand::random())); + + // Create the alt block info. + let block_info = AltBlockInformation { + block_hash: prepped_block.block_hash, + block: prepped_block.block, + block_blob: prepped_block.block_blob, + txs: ordered_txs + .into_iter() + .map(|tx| VerifiedTransactionInformation { + tx_blob: tx.tx_blob, + tx_weight: tx.tx_weight, + fee: tx.fee, + tx_hash: tx.tx_hash, + tx: tx.tx, + }) + .collect(), + pow_hash: prepped_block.pow_hash, + weight: block_weight, + height: alt_context_cache.chain_height, + long_term_weight, + cumulative_difficulty, + chain_id, + }; + + // Add this block to the cache. + alt_context_cache.add_new_block( + block_info.height, + block_info.block_hash, + block_info.weight, + block_info.long_term_weight, + block_info.block.header.timestamp, + ); + + // Add this alt cache back to the context service. + context_svc + .oneshot(BlockChainContextRequest::AddAltChainContextCache { + prev_id: block_info.block.header.previous, + cache: alt_context_cache, + _token: AltChainRequestToken, + }) + .await?; + + Ok(VerifyBlockResponse::AltChain(block_info)) +} + +/// Retrieves the alt RX VM for the chosen block height. +/// +/// If the `hf` is less than 12 (the height RX activates), then [`None`] is returned. +async fn alt_rx_vm( + block_height: u64, + hf: u8, + parent_chain: Chain, + alt_chain_context: &mut AltChainContextCache, + context_svc: C, +) -> Result>, ExtendedConsensusError> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send, + C::Future: Send + 'static, +{ + if hf < 12 { + return Ok(None); + } + + let seed_height = randomx_seed_height(block_height); + + let cached_vm = match alt_chain_context.cached_rx_vm.take() { + // If the VM is cached and the height is the height we need, we can use this VM. + Some((cached_seed_height, vm)) if seed_height == cached_seed_height => { + (cached_seed_height, vm) + } + // Otherwise we need to make a new VM. + _ => { + let BlockChainContextResponse::AltChainRxVM(vm) = context_svc + .oneshot(BlockChainContextRequest::AltChainRxVM { + height: block_height, + chain: parent_chain, + _token: AltChainRequestToken, + }) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + (seed_height, vm) + } + }; + + Ok(Some( + alt_chain_context.cached_rx_vm.insert(cached_vm).1.clone(), + )) +} + +/// Returns the [`DifficultyCache`] for the alt chain. +async fn alt_difficulty_cache( + prev_id: [u8; 32], + alt_chain_context: &mut AltChainContextCache, + context_svc: C, +) -> Result<&mut DifficultyCache, ExtendedConsensusError> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send, + C::Future: Send + 'static, +{ + // First look to see if the difficulty cache for this alt chain is already cached. + match &mut alt_chain_context.difficulty_cache { + Some(cache) => Ok(cache), + // Otherwise make a new one. + difficulty_cache => { + let BlockChainContextResponse::AltChainDifficultyCache(cache) = context_svc + .oneshot(BlockChainContextRequest::AltChainDifficultyCache { + prev_id, + _token: AltChainRequestToken, + }) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + Ok(difficulty_cache.insert(cache)) + } + } +} + +/// Returns the [`BlockWeightsCache`] for the alt chain. +async fn alt_weight_cache( + prev_id: [u8; 32], + alt_chain_context: &mut AltChainContextCache, + context_svc: C, +) -> Result<&mut BlockWeightsCache, ExtendedConsensusError> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send, + C::Future: Send + 'static, +{ + // First look to see if the weight cache for this alt chain is already cached. + match &mut alt_chain_context.weight_cache { + Some(cache) => Ok(cache), + // Otherwise make a new one. + weight_cache => { + let BlockChainContextResponse::AltChainWeightCache(cache) = context_svc + .oneshot(BlockChainContextRequest::AltChainWeightCache { + prev_id, + _token: AltChainRequestToken, + }) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + Ok(weight_cache.insert(cache)) + } + } +} diff --git a/consensus/src/block/batch_prepare.rs b/consensus/src/block/batch_prepare.rs new file mode 100644 index 00000000..64d1ccb5 --- /dev/null +++ b/consensus/src/block/batch_prepare.rs @@ -0,0 +1,207 @@ +use std::{collections::HashMap, sync::Arc}; + +use monero_serai::{block::Block, transaction::Transaction}; +use rayon::prelude::*; +use tower::{Service, ServiceExt}; +use tracing::instrument; + +use cuprate_consensus_rules::{ + blocks::{check_block_pow, is_randomx_seed_height, randomx_seed_height, BlockError}, + hard_forks::HardForkError, + miner_tx::MinerTxError, + ConsensusError, HardFork, +}; +use cuprate_helper::asynch::rayon_spawn_async; + +use crate::{ + block::{free::pull_ordered_transactions, PreparedBlock, PreparedBlockExPow}, + context::rx_vms::RandomXVM, + transactions::TransactionVerificationData, + BlockChainContextRequest, BlockChainContextResponse, ExtendedConsensusError, + VerifyBlockResponse, +}; + +/// Batch prepares a list of blocks for verification. +#[instrument(level = "debug", name = "batch_prep_blocks", skip_all, fields(amt = blocks.len()))] +pub(crate) async fn batch_prepare_main_chain_block( + blocks: Vec<(Block, Vec)>, + mut context_svc: C, +) -> Result +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Send + + 'static, + C::Future: Send + 'static, +{ + let (blocks, txs): (Vec<_>, Vec<_>) = blocks.into_iter().unzip(); + + tracing::debug!("Calculating block hashes."); + let blocks: Vec = rayon_spawn_async(|| { + blocks + .into_iter() + .map(PreparedBlockExPow::new) + .collect::, _>>() + }) + .await?; + + let Some(last_block) = blocks.last() else { + return Err(ExtendedConsensusError::NoBlocksToVerify); + }; + + // hard-forks cannot be reversed, so the last block will contain the highest hard fork (provided the + // batch is valid). + let top_hf_in_batch = last_block.hf_version; + + // A Vec of (timestamp, HF) for each block to calculate the expected difficulty for each block. + let mut timestamps_hfs = Vec::with_capacity(blocks.len()); + let mut new_rx_vm = None; + + tracing::debug!("Checking blocks follow each other."); + + // For every block make sure they have the correct height and previous ID + for window in blocks.windows(2) { + let block_0 = &window[0]; + let block_1 = &window[1]; + + // Make sure no blocks in the batch have a higher hard fork than the last block. + if block_0.hf_version > top_hf_in_batch { + Err(ConsensusError::Block(BlockError::HardForkError( + HardForkError::VersionIncorrect, + )))?; + } + + if block_0.block_hash != block_1.block.header.previous + || block_0.height != block_1.height - 1 + { + tracing::debug!("Blocks do not follow each other, verification failed."); + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))?; + } + + // Cache any potential RX VM seeds as we may need them for future blocks in the batch. + if is_randomx_seed_height(block_0.height) && top_hf_in_batch >= HardFork::V12 { + new_rx_vm = Some((block_0.height, block_0.block_hash)); + } + + timestamps_hfs.push((block_0.block.header.timestamp, block_0.hf_version)) + } + + // Get the current blockchain context. + let BlockChainContextResponse::Context(checked_context) = context_svc + .ready() + .await? + .call(BlockChainContextRequest::GetContext) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + // Calculate the expected difficulties for each block in the batch. + let BlockChainContextResponse::BatchDifficulties(difficulties) = context_svc + .ready() + .await? + .call(BlockChainContextRequest::BatchGetDifficulties( + timestamps_hfs, + )) + .await? + else { + panic!("Context service returned wrong response!"); + }; + + let context = checked_context.unchecked_blockchain_context().clone(); + + // Make sure the blocks follow the main chain. + + if context.chain_height != blocks[0].height { + tracing::debug!("Blocks do not follow main chain, verification failed."); + + Err(ConsensusError::Block(BlockError::MinerTxError( + MinerTxError::InputsHeightIncorrect, + )))?; + } + + if context.top_hash != blocks[0].block.header.previous { + tracing::debug!("Blocks do not follow main chain, verification failed."); + + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))?; + } + + let mut rx_vms = if top_hf_in_batch < HardFork::V12 { + HashMap::new() + } else { + let BlockChainContextResponse::RxVms(rx_vms) = context_svc + .ready() + .await? + .call(BlockChainContextRequest::GetCurrentRxVm) + .await? + else { + panic!("Blockchain context service returned wrong response!"); + }; + + rx_vms + }; + + // If we have a RX seed in the batch calculate it. + if let Some((new_vm_height, new_vm_seed)) = new_rx_vm { + tracing::debug!("New randomX seed in batch, initialising VM"); + + let new_vm = rayon_spawn_async(move || { + Arc::new(RandomXVM::new(&new_vm_seed).expect("RandomX VM gave an error on set up!")) + }) + .await; + + // Give the new VM to the context service, so it can cache it. + context_svc + .oneshot(BlockChainContextRequest::NewRXVM(( + new_vm_seed, + new_vm.clone(), + ))) + .await?; + + rx_vms.insert(new_vm_height, new_vm); + } + + tracing::debug!("Calculating PoW and prepping transaction"); + + let blocks = rayon_spawn_async(move || { + blocks + .into_par_iter() + .zip(difficulties) + .zip(txs) + .map(|((block, difficultly), txs)| { + // Calculate the PoW for the block. + let height = block.height; + let block = PreparedBlock::new_prepped( + block, + rx_vms.get(&randomx_seed_height(height)).map(AsRef::as_ref), + )?; + + // Check the PoW + check_block_pow(&block.pow_hash, difficultly).map_err(ConsensusError::Block)?; + + // Now setup the txs. + let txs = txs + .into_par_iter() + .map(|tx| { + let tx = TransactionVerificationData::new(tx)?; + Ok::<_, ConsensusError>((tx.tx_hash, tx)) + }) + .collect::, _>>()?; + + // Order the txs correctly. + // TODO: Remove the Arc here + let ordered_txs = pull_ordered_transactions(&block.block, txs)? + .into_iter() + .map(Arc::new) + .collect(); + + Ok((block, ordered_txs)) + }) + .collect::, ExtendedConsensusError>>() + }) + .await?; + + Ok(VerifyBlockResponse::MainChainBatchPrepped(blocks)) +} diff --git a/consensus/src/block/free.rs b/consensus/src/block/free.rs new file mode 100644 index 00000000..8a61e801 --- /dev/null +++ b/consensus/src/block/free.rs @@ -0,0 +1,32 @@ +//! Free functions for block verification +use std::collections::HashMap; + +use monero_serai::block::Block; + +use crate::{transactions::TransactionVerificationData, ExtendedConsensusError}; + +/// Returns a list of transactions, pulled from `txs` in the order they are in the [`Block`]. +/// +/// Will error if a tx need is not in `txs` or if `txs` contain more txs than needed. +pub(crate) fn pull_ordered_transactions( + block: &Block, + mut txs: HashMap<[u8; 32], TransactionVerificationData>, +) -> Result, ExtendedConsensusError> { + if block.txs.len() != txs.len() { + return Err(ExtendedConsensusError::TxsIncludedWithBlockIncorrect); + } + + let mut ordered_txs = Vec::with_capacity(txs.len()); + + if !block.txs.is_empty() { + for tx_hash in &block.txs { + let tx = txs + .remove(tx_hash) + .ok_or(ExtendedConsensusError::TxsIncludedWithBlockIncorrect)?; + ordered_txs.push(tx); + } + drop(txs); + } + + Ok(ordered_txs) +} diff --git a/consensus/src/context.rs b/consensus/src/context.rs index 886a1577..f1432810 100644 --- a/consensus/src/context.rs +++ b/consensus/src/context.rs @@ -27,16 +27,22 @@ pub(crate) mod hardforks; pub(crate) mod rx_vms; pub(crate) mod weight; +mod alt_chains; mod task; mod tokens; +use cuprate_types::Chain; +use difficulty::DifficultyCache; +use rx_vms::RandomXVM; +use weight::BlockWeightsCache; + +pub(crate) use alt_chains::{sealed::AltChainRequestToken, AltChainContextCache}; pub use difficulty::DifficultyCacheConfig; pub use hardforks::HardForkConfig; -use rx_vms::RandomXVM; pub use tokens::*; pub use weight::BlockWeightsCacheConfig; -const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60; +pub(crate) const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60; /// Config for the context service. pub struct ContextConfig { @@ -233,6 +239,74 @@ pub enum BlockChainContextRequest { NewRXVM(([u8; 32], Arc)), /// A request to add a new block to the cache. Update(NewBlockData), + /// Pop blocks from the cache to the specified height. + PopBlocks { + /// The number of blocks to pop from the top of the chain. + /// + /// # Panics + /// + /// This will panic if the number of blocks will pop the genesis block. + numb_blocks: u64, + }, + /// Clear the alt chain context caches. + ClearAltCache, + //----------------------------------------------------------------------------------------------------------- AltChainRequests + /// A request for an alt chain context cache. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle getting the alt cache. + AltChainContextCache { + /// The previous block field in a [`BlockHeader`](monero_serai::block::BlockHeader). + prev_id: [u8; 32], + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, + /// A request for a difficulty cache of an alternative chin. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle getting the difficulty cache of an alt chain. + AltChainDifficultyCache { + /// The previous block field in a [`BlockHeader`](monero_serai::block::BlockHeader). + prev_id: [u8; 32], + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, + /// A request for a block weight cache of an alternative chin. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle getting the weight cache of an alt chain. + AltChainWeightCache { + /// The previous block field in a [`BlockHeader`](monero_serai::block::BlockHeader). + prev_id: [u8; 32], + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, + /// A request for a RX VM for an alternative chin. + /// + /// Response variant: [`BlockChainContextResponse::AltChainRxVM`]. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle getting the randomX VM of an alt chain. + AltChainRxVM { + /// The height the RandomX VM is needed for. + height: u64, + /// The chain to look in for the seed. + chain: Chain, + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, + /// A request to add an alt chain context cache to the context cache. + /// + /// This variant is private and is not callable from outside this crate, the block verifier service will + /// handle returning the alt cache to the context service. + AddAltChainContextCache { + /// The previous block field in a [`BlockHeader`](monero_serai::block::BlockHeader). + prev_id: [u8; 32], + /// The cache. + cache: Box, + /// An internal token to prevent external crates calling this request. + _token: AltChainRequestToken, + }, } pub enum BlockChainContextResponse { @@ -242,7 +316,15 @@ pub enum BlockChainContextResponse { RxVms(HashMap>), /// A list of difficulties. BatchDifficulties(Vec), - /// Ok response. + /// An alt chain context cache. + AltChainContextCache(Box), + /// A difficulty cache for an alt chain. + AltChainDifficultyCache(DifficultyCache), + /// A randomX VM for an alt chain. + AltChainRxVM(Arc), + /// A weight cache for an alt chain + AltChainWeightCache(BlockWeightsCache), + /// A generic Ok response. Ok, } diff --git a/consensus/src/context/alt_chains.rs b/consensus/src/context/alt_chains.rs new file mode 100644 index 00000000..71af8a1e --- /dev/null +++ b/consensus/src/context/alt_chains.rs @@ -0,0 +1,215 @@ +use std::{collections::HashMap, sync::Arc}; + +use tower::ServiceExt; + +use cuprate_consensus_rules::{blocks::BlockError, ConsensusError}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, ChainId, +}; + +use crate::{ + ExtendedConsensusError, + __private::Database, + context::{difficulty::DifficultyCache, rx_vms::RandomXVM, weight::BlockWeightsCache}, +}; + +pub(crate) mod sealed { + /// A token that should be hard to create from outside this crate. + /// + /// It is currently possible to safely create this from outside this crate, **DO NOT** rely on this + /// as it will be broken once we find a way to completely seal this. + #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] + pub struct AltChainRequestToken; +} + +/// The context cache of an alternative chain. +#[derive(Debug, Clone)] +pub struct AltChainContextCache { + /// The alt chain weight cache, [`None`] if it has not been built yet. + pub weight_cache: Option, + /// The alt chain difficulty cache, [`None`] if it has not been built yet. + pub difficulty_cache: Option, + + /// A cached RX VM. + pub cached_rx_vm: Option<(u64, Arc)>, + + /// The chain height of the alt chain. + pub chain_height: u64, + /// The top hash of the alt chain. + pub top_hash: [u8; 32], + /// The [`ChainID`] of the alt chain. + pub chain_id: Option, + /// The parent [`Chain`] of this alt chain. + pub parent_chain: Chain, +} + +impl AltChainContextCache { + /// Add a new block to the cache. + pub fn add_new_block( + &mut self, + height: u64, + block_hash: [u8; 32], + block_weight: usize, + long_term_block_weight: usize, + timestamp: u64, + ) { + if let Some(difficulty_cache) = &mut self.difficulty_cache { + difficulty_cache.new_block(height, timestamp, difficulty_cache.cumulative_difficulty()); + } + + if let Some(weight_cache) = &mut self.weight_cache { + weight_cache.new_block(height, block_weight, long_term_block_weight); + } + + self.chain_height += 1; + self.top_hash = block_hash; + } +} + +/// A map of top IDs to alt chains. +pub struct AltChainMap { + alt_cache_map: HashMap<[u8; 32], Box>, +} + +impl AltChainMap { + pub fn new() -> Self { + Self { + alt_cache_map: HashMap::new(), + } + } + + pub fn clear(&mut self) { + self.alt_cache_map.clear(); + } + + /// Add an alt chain cache to the map. + pub fn add_alt_cache(&mut self, prev_id: [u8; 32], alt_cache: Box) { + self.alt_cache_map.insert(prev_id, alt_cache); + } + + /// Attempts to take an [`AltChainContextCache`] from the map, returning [`None`] if no cache is + /// present. + pub async fn get_alt_chain_context( + &mut self, + prev_id: [u8; 32], + database: D, + ) -> Result, ExtendedConsensusError> { + if let Some(cache) = self.alt_cache_map.remove(&prev_id) { + return Ok(cache); + } + + // find the block with hash == prev_id. + let BCResponse::FindBlock(res) = + database.oneshot(BCReadRequest::FindBlock(prev_id)).await? + else { + panic!("Database returned wrong response"); + }; + + let Some((parent_chain, top_height)) = res else { + // Couldn't find prev_id + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))? + }; + + Ok(Box::new(AltChainContextCache { + weight_cache: None, + difficulty_cache: None, + cached_rx_vm: None, + chain_height: top_height, + top_hash: prev_id, + chain_id: None, + parent_chain, + })) + } +} + +/// Builds a [`DifficultyCache`] for an alt chain. +pub async fn get_alt_chain_difficulty_cache( + prev_id: [u8; 32], + main_chain_difficulty_cache: &DifficultyCache, + mut database: D, +) -> Result { + // find the block with hash == prev_id. + let BCResponse::FindBlock(res) = database + .ready() + .await? + .call(BCReadRequest::FindBlock(prev_id)) + .await? + else { + panic!("Database returned wrong response"); + }; + + let Some((chain, top_height)) = res else { + // Can't find prev_id + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))? + }; + + Ok(match chain { + Chain::Main => { + // prev_id is in main chain, we can use the fast path and clone the main chain cache. + let mut difficulty_cache = main_chain_difficulty_cache.clone(); + difficulty_cache + .pop_blocks_main_chain( + difficulty_cache.last_accounted_height - top_height, + database, + ) + .await?; + + difficulty_cache + } + Chain::Alt(_) => { + // prev_id is in an alt chain, completely rebuild the cache. + DifficultyCache::init_from_chain_height( + top_height + 1, + main_chain_difficulty_cache.config, + database, + chain, + ) + .await? + } + }) +} + +/// Builds a [`BlockWeightsCache`] for an alt chain. +pub async fn get_alt_chain_weight_cache( + prev_id: [u8; 32], + main_chain_weight_cache: &BlockWeightsCache, + mut database: D, +) -> Result { + // find the block with hash == prev_id. + let BCResponse::FindBlock(res) = database + .ready() + .await? + .call(BCReadRequest::FindBlock(prev_id)) + .await? + else { + panic!("Database returned wrong response"); + }; + + let Some((chain, top_height)) = res else { + // Can't find prev_id + Err(ConsensusError::Block(BlockError::PreviousIDIncorrect))? + }; + + Ok(match chain { + Chain::Main => { + // prev_id is in main chain, we can use the fast path and clone the main chain cache. + let mut weight_cache = main_chain_weight_cache.clone(); + weight_cache + .pop_blocks_main_chain(weight_cache.tip_height - top_height, database) + .await?; + + weight_cache + } + Chain::Alt(_) => { + // prev_id is in an alt chain, completely rebuild the cache. + BlockWeightsCache::init_from_chain_height( + top_height + 1, + main_chain_weight_cache.config, + database, + chain, + ) + .await? + } + }) +} diff --git a/consensus/src/context/difficulty.rs b/consensus/src/context/difficulty.rs index 710b2986..b025dfcd 100644 --- a/consensus/src/context/difficulty.rs +++ b/consensus/src/context/difficulty.rs @@ -12,7 +12,10 @@ use tower::ServiceExt; use tracing::instrument; use cuprate_helper::num::median; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use crate::{Database, ExtendedConsensusError, HardFork}; @@ -28,7 +31,7 @@ const DIFFICULTY_LAG: usize = 15; /// Configuration for the difficulty cache. /// -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct DifficultyCacheConfig { pub(crate) window: usize, pub(crate) cut: usize, @@ -45,8 +48,8 @@ impl DifficultyCacheConfig { } /// Returns the total amount of blocks we need to track to calculate difficulty - pub fn total_block_count(&self) -> usize { - self.window + self.lag + pub fn total_block_count(&self) -> u64 { + (self.window + self.lag).try_into().unwrap() } /// The amount of blocks we account for after removing the outliers. @@ -68,14 +71,14 @@ impl DifficultyCacheConfig { /// This struct is able to calculate difficulties from blockchain information. /// #[derive(Debug, Clone, Eq, PartialEq)] -pub(crate) struct DifficultyCache { +pub struct DifficultyCache { /// The list of timestamps in the window. /// len <= [`DIFFICULTY_BLOCKS_COUNT`] pub(crate) timestamps: VecDeque, /// The current cumulative difficulty of the chain. pub(crate) cumulative_difficulties: VecDeque, /// The last height we accounted for. - pub(crate) last_accounted_height: usize, + pub(crate) last_accounted_height: u64, /// The config pub(crate) config: DifficultyCacheConfig, } @@ -84,9 +87,10 @@ impl DifficultyCache { /// Initialize the difficulty cache from the specified chain height. #[instrument(name = "init_difficulty_cache", level = "info", skip(database, config))] pub async fn init_from_chain_height( - chain_height: usize, + chain_height: u64, config: DifficultyCacheConfig, database: D, + chain: Chain, ) -> Result { tracing::info!("Initializing difficulty cache this may take a while."); @@ -98,7 +102,9 @@ impl DifficultyCache { } let (timestamps, cumulative_difficulties) = - get_blocks_in_pow_info(database.clone(), block_start..chain_height).await?; + get_blocks_in_pow_info(database.clone(), block_start..chain_height, chain).await?; + + debug_assert_eq!(timestamps.len() as u64, chain_height - block_start); tracing::info!( "Current chain height: {}, accounting for {} blocks timestamps", @@ -116,8 +122,72 @@ impl DifficultyCache { Ok(diff) } + /// Pop some blocks from the top of the cache. + /// + /// The cache will be returned to the state it would have been in `numb_blocks` ago. + /// + /// # Invariant + /// + /// This _must_ only be used on a main-chain cache. + #[instrument(name = "pop_blocks_diff_cache", skip_all, fields(numb_blocks = numb_blocks))] + pub async fn pop_blocks_main_chain( + &mut self, + numb_blocks: u64, + database: D, + ) -> Result<(), ExtendedConsensusError> { + let Some(retained_blocks) = self + .timestamps + .len() + .checked_sub(usize::try_from(numb_blocks).unwrap()) + else { + // More blocks to pop than we have in the cache, so just restart a new cache. + *self = Self::init_from_chain_height( + self.last_accounted_height - numb_blocks + 1, + self.config, + database, + Chain::Main, + ) + .await?; + + return Ok(()); + }; + + let current_chain_height = self.last_accounted_height + 1; + + let mut new_start_height = current_chain_height + .saturating_sub(self.config.total_block_count()) + .saturating_sub(numb_blocks); + + // skip the genesis block. + if new_start_height == 0 { + new_start_height = 1; + } + + let (mut timestamps, mut cumulative_difficulties) = get_blocks_in_pow_info( + database, + new_start_height + // current_chain_height - self.timestamps.len() blocks are already in the cache. + ..(current_chain_height - u64::try_from(self.timestamps.len()).unwrap()), + Chain::Main, + ) + .await?; + + self.timestamps.drain(retained_blocks..); + self.cumulative_difficulties.drain(retained_blocks..); + timestamps.append(&mut self.timestamps); + cumulative_difficulties.append(&mut self.cumulative_difficulties); + + self.timestamps = timestamps; + self.cumulative_difficulties = cumulative_difficulties; + self.last_accounted_height -= numb_blocks; + + assert_eq!(self.timestamps.len(), self.cumulative_difficulties.len()); + + Ok(()) + } + /// Add a new block to the difficulty cache. - pub fn new_block(&mut self, height: usize, timestamp: u64, cumulative_difficulty: u128) { + pub fn new_block(&mut self, height: u64, timestamp: u64, cumulative_difficulty: u128) { assert_eq!(self.last_accounted_height + 1, height); self.last_accounted_height += 1; @@ -129,7 +199,7 @@ impl DifficultyCache { self.cumulative_difficulties .push_back(cumulative_difficulty); - if self.timestamps.len() > self.config.total_block_count() { + if u64::try_from(self.timestamps.len()).unwrap() > self.config.total_block_count() { self.timestamps.pop_front(); self.cumulative_difficulties.pop_front(); } @@ -174,7 +244,7 @@ impl DifficultyCache { let last_cum_diff = cumulative_difficulties.back().copied().unwrap_or(1); cumulative_difficulties.push_back(last_cum_diff + *difficulties.last().unwrap()); - if timestamps.len() > self.config.total_block_count() { + if u64::try_from(timestamps.len()).unwrap() > self.config.total_block_count() { diff_info_popped.push(( timestamps.pop_front().unwrap(), cumulative_difficulties.pop_front().unwrap(), @@ -196,21 +266,22 @@ impl DifficultyCache { /// /// Will return [`None`] if there aren't enough blocks. pub fn median_timestamp(&self, numb_blocks: usize) -> Option { - let mut timestamps = if self.last_accounted_height + 1 == numb_blocks { - // if the chain height is equal to `numb_blocks` add the genesis block. - // otherwise if the chain height is less than `numb_blocks` None is returned - // and if its more than it would be excluded from calculations. - let mut timestamps = self.timestamps.clone(); - // all genesis blocks have a timestamp of 0. - // https://cuprate.github.io/monero-book/consensus_rules/genesis_block.html - timestamps.push_front(0); - timestamps.into() - } else { - self.timestamps - .range(self.timestamps.len().checked_sub(numb_blocks)?..) - .copied() - .collect::>() - }; + let mut timestamps = + if self.last_accounted_height + 1 == u64::try_from(numb_blocks).unwrap() { + // if the chain height is equal to `numb_blocks` add the genesis block. + // otherwise if the chain height is less than `numb_blocks` None is returned + // and if it's more it would be excluded from calculations. + let mut timestamps = self.timestamps.clone(); + // all genesis blocks have a timestamp of 0. + // https://cuprate.github.io/monero-book/consensus_rules/genesis_block.html + timestamps.push_front(0); + timestamps.into() + } else { + self.timestamps + .range(self.timestamps.len().checked_sub(numb_blocks)?..) + .copied() + .collect::>() + }; timestamps.sort_unstable(); debug_assert_eq!(timestamps.len(), numb_blocks); @@ -297,12 +368,16 @@ fn get_window_start_and_end( #[instrument(name = "get_blocks_timestamps", skip(database), level = "info")] async fn get_blocks_in_pow_info( database: D, - block_heights: Range, + block_heights: Range, + chain: Chain, ) -> Result<(VecDeque, VecDeque), ExtendedConsensusError> { tracing::info!("Getting blocks timestamps"); let BCResponse::BlockExtendedHeaderInRange(ext_header) = database - .oneshot(BCReadRequest::BlockExtendedHeaderInRange(block_heights)) + .oneshot(BCReadRequest::BlockExtendedHeaderInRange( + block_heights, + chain, + )) .await? else { panic!("Database sent incorrect response"); diff --git a/consensus/src/context/hardforks.rs b/consensus/src/context/hardforks.rs index 696ff857..f183fc4e 100644 --- a/consensus/src/context/hardforks.rs +++ b/consensus/src/context/hardforks.rs @@ -4,7 +4,10 @@ use tower::ServiceExt; use tracing::instrument; use cuprate_consensus_rules::{HFVotes, HFsInfo, HardFork}; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use crate::{Database, ExtendedConsensusError}; @@ -15,7 +18,7 @@ const DEFAULT_WINDOW_SIZE: usize = 10080; // supermajority window check length - /// Configuration for hard-forks. /// -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct HardForkConfig { /// The network we are on. pub(crate) info: HFsInfo, @@ -50,7 +53,7 @@ impl HardForkConfig { } /// A struct that keeps track of the current hard-fork and current votes. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct HardForkState { /// The current active hard-fork. pub(crate) current_hardfork: HardFork, @@ -117,6 +120,50 @@ impl HardForkState { Ok(hfs) } + /// Pop some blocks from the top of the cache. + /// + /// The cache will be returned to the state it would have been in `numb_blocks` ago. + /// + /// # Invariant + /// + /// This _must_ only be used on a main-chain cache. + pub async fn pop_blocks_main_chain( + &mut self, + numb_blocks: u64, + database: D, + ) -> Result<(), ExtendedConsensusError> { + let Some(retained_blocks) = self.votes.total_votes().checked_sub(self.config.window) else { + *self = Self::init_from_chain_height( + self.last_height + 1 - numb_blocks, + self.config, + database, + ) + .await?; + + return Ok(()); + }; + + let current_chain_height = self.last_height + 1; + + let oldest_votes = get_votes_in_range( + database, + current_chain_height + .saturating_sub(self.config.window) + .saturating_sub(numb_blocks) + ..current_chain_height + .saturating_sub(numb_blocks) + .saturating_sub(retained_blocks), + usize::try_from(numb_blocks).unwrap(), + ) + .await?; + + self.votes + .reverse_blocks(usize::try_from(numb_blocks).unwrap(), oldest_votes); + self.last_height -= numb_blocks; + + Ok(()) + } + /// Add a new block to the cache. pub fn new_block(&mut self, vote: HardFork, height: usize) { // We don't _need_ to take in `height` but it's for safety, so we don't silently loose track @@ -168,7 +215,10 @@ async fn get_votes_in_range( let mut votes = HFVotes::new(window_size); let BCResponse::BlockExtendedHeaderInRange(vote_list) = database - .oneshot(BCReadRequest::BlockExtendedHeaderInRange(block_heights)) + .oneshot(BCReadRequest::BlockExtendedHeaderInRange( + block_heights, + Chain::Main, + )) .await? else { panic!("Database sent incorrect response!"); diff --git a/consensus/src/context/rx_vms.rs b/consensus/src/context/rx_vms.rs index d5fbd797..31546486 100644 --- a/consensus/src/context/rx_vms.rs +++ b/consensus/src/context/rx_vms.rs @@ -15,12 +15,16 @@ use thread_local::ThreadLocal; use tower::ServiceExt; use tracing::instrument; +use cuprate_consensus_rules::blocks::randomx_seed_height; use cuprate_consensus_rules::{ blocks::{is_randomx_seed_height, RandomX, RX_SEEDHASH_EPOCH_BLOCKS}, HardFork, }; use cuprate_helper::asynch::rayon_spawn_async; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use crate::{Database, ExtendedConsensusError}; @@ -70,9 +74,9 @@ impl RandomX for RandomXVM { #[derive(Clone, Debug)] pub struct RandomXVMCache { /// The top [`RX_SEEDS_CACHED`] RX seeds. - pub(crate) seeds: VecDeque<(usize, [u8; 32])>, + pub(crate) seeds: VecDeque<(u64, [u8; 32])>, /// The VMs for `seeds` (if after hf 12, otherwise this will be empty). - pub(crate) vms: HashMap>, + pub(crate) vms: HashMap>, /// A single cached VM that was given to us from a part of Cuprate. pub(crate) cached_vm: Option<([u8; 32], Arc)>, @@ -81,7 +85,7 @@ pub struct RandomXVMCache { impl RandomXVMCache { #[instrument(name = "init_rx_vm_cache", level = "info", skip(database))] pub async fn init_from_chain_height( - chain_height: usize, + chain_height: u64, hf: &HardFork, database: D, ) -> Result { @@ -90,8 +94,7 @@ impl RandomXVMCache { tracing::debug!("last {RX_SEEDS_CACHED} randomX seed heights: {seed_heights:?}",); - let seeds: VecDeque<(usize, [u8; 32])> = - seed_heights.into_iter().zip(seed_hashes).collect(); + let seeds: VecDeque<(u64, [u8; 32])> = seed_heights.into_iter().zip(seed_hashes).collect(); let vms = if hf >= &HardFork::V12 { tracing::debug!("Creating RandomX VMs"); @@ -125,8 +128,40 @@ impl RandomXVMCache { self.cached_vm.replace(vm); } - /// Get the RandomX VMs. - pub async fn get_vms(&mut self) -> HashMap> { + /// Creates a RX VM for an alt chain, looking at the main chain RX VMs to see if we can use one + /// of them first. + pub async fn get_alt_vm( + &mut self, + height: u64, + chain: Chain, + database: D, + ) -> Result, ExtendedConsensusError> { + let seed_height = randomx_seed_height(height); + + let BCResponse::BlockHash(seed_hash) = database + .oneshot(BCReadRequest::BlockHash(seed_height, chain)) + .await? + else { + panic!("Database returned wrong response!"); + }; + + for (vm_main_chain_height, vm_seed_hash) in &self.seeds { + if vm_seed_hash == &seed_hash { + let Some(vm) = self.vms.get(vm_main_chain_height) else { + break; + }; + + return Ok(vm.clone()); + } + } + + let alt_vm = rayon_spawn_async(move || Arc::new(RandomXVM::new(&seed_hash).unwrap())).await; + + Ok(alt_vm) + } + + /// Get the main-chain RandomX VMs. + pub async fn get_vms(&mut self) -> HashMap> { match self.seeds.len().checked_sub(self.vms.len()) { // No difference in the amount of seeds to VMs. Some(0) => (), @@ -177,10 +212,16 @@ impl RandomXVMCache { self.vms.clone() } + /// Removes all the RandomX VMs above the `new_height`. + pub fn pop_blocks_main_chain(&mut self, new_height: u64) { + self.seeds.retain(|(height, _)| *height < new_height); + self.vms.retain(|height, _| *height < new_height); + } + /// Add a new block to the VM cache. /// /// hash is the block hash not the blocks PoW hash. - pub fn new_block(&mut self, height: usize, hash: &[u8; 32]) { + pub fn new_block(&mut self, height: u64, hash: &[u8; 32]) { if is_randomx_seed_height(height) { tracing::debug!("Block {height} is a randomX seed height, adding it to the cache.",); @@ -201,7 +242,7 @@ impl RandomXVMCache { /// Get the last `amount` of RX seeds, the top height returned here will not necessarily be the RX VM for the top block /// in the chain as VMs include some lag before a seed activates. -pub(crate) fn get_last_rx_seed_heights(mut last_height: usize, mut amount: usize) -> Vec { +pub(crate) fn get_last_rx_seed_heights(mut last_height: u64, mut amount: usize) -> Vec { let mut seeds = Vec::with_capacity(amount); if is_randomx_seed_height(last_height) { seeds.push(last_height); @@ -224,7 +265,7 @@ pub(crate) fn get_last_rx_seed_heights(mut last_height: usize, mut amount: usize /// Gets the block hashes for the heights specified. async fn get_block_hashes( - heights: Vec, + heights: Vec, database: D, ) -> Result, ExtendedConsensusError> { let mut fut = FuturesOrdered::new(); @@ -232,8 +273,10 @@ async fn get_block_hashes( for height in heights { let db = database.clone(); fut.push_back(async move { - let BCResponse::BlockHash(hash) = - db.clone().oneshot(BCReadRequest::BlockHash(height)).await? + let BCResponse::BlockHash(hash) = db + .clone() + .oneshot(BCReadRequest::BlockHash(height, Chain::Main)) + .await? else { panic!("Database sent incorrect response!"); }; diff --git a/consensus/src/context/task.rs b/consensus/src/context/task.rs index 83286720..38cccb09 100644 --- a/consensus/src/context/task.rs +++ b/consensus/src/context/task.rs @@ -9,14 +9,20 @@ use tower::ServiceExt; use tracing::Instrument; use cuprate_consensus_rules::blocks::ContextToVerifyBlock; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; - -use super::{ - difficulty, hardforks, rx_vms, weight, BlockChainContext, BlockChainContextRequest, - BlockChainContextResponse, ContextConfig, RawBlockChainContext, ValidityToken, - BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW, +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; + +use crate::{ + context::{ + alt_chains::{get_alt_chain_difficulty_cache, get_alt_chain_weight_cache, AltChainMap}, + difficulty, hardforks, rx_vms, weight, BlockChainContext, BlockChainContextRequest, + BlockChainContextResponse, ContextConfig, RawBlockChainContext, ValidityToken, + BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW, + }, + Database, ExtendedConsensusError, }; -use crate::{Database, ExtendedConsensusError}; /// A request from the context service to the context task. pub(super) struct ContextTaskRequest { @@ -29,7 +35,7 @@ pub(super) struct ContextTaskRequest { } /// The Context task that keeps the blockchain context and handles requests. -pub struct ContextTask { +pub struct ContextTask { /// A token used to invalidate previous contexts when a new /// block is added to the chain. current_validity_token: ValidityToken, @@ -43,25 +49,25 @@ pub struct ContextTask { /// The hard-fork state cache. hardfork_state: hardforks::HardForkState, + alt_chain_cache_map: AltChainMap, + /// The current chain height. chain_height: usize, /// The top block hash. top_block_hash: [u8; 32], /// The total amount of coins generated. already_generated_coins: u64, + + database: D, } -impl ContextTask { +impl ContextTask { /// Initialize the [`ContextTask`], this will need to pull a lot of data from the database so may take a /// while to complete. - pub async fn init_context( + pub async fn init_context( cfg: ContextConfig, mut database: D, - ) -> Result - where - D: Database + Clone + Send + Sync + 'static, - D::Future: Send + 'static, - { + ) -> Result { let ContextConfig { difficulty_cfg, weights_config, @@ -82,7 +88,7 @@ impl ContextTask { let BCResponse::GeneratedCoins(already_generated_coins) = database .ready() .await? - .call(BCReadRequest::GeneratedCoins) + .call(BCReadRequest::GeneratedCoins(chain_height - 1)) .await? else { panic!("Database sent incorrect response!"); @@ -95,14 +101,24 @@ impl ContextTask { let db = database.clone(); let difficulty_cache_handle = tokio::spawn(async move { - difficulty::DifficultyCache::init_from_chain_height(chain_height, difficulty_cfg, db) - .await + difficulty::DifficultyCache::init_from_chain_height( + chain_height, + difficulty_cfg, + db, + Chain::Main, + ) + .await }); let db = database.clone(); let weight_cache_handle = tokio::spawn(async move { - weight::BlockWeightsCache::init_from_chain_height(chain_height, weights_config, db) - .await + weight::BlockWeightsCache::init_from_chain_height( + chain_height, + weights_config, + db, + Chain::Main, + ) + .await }); // Wait for the hardfork state to finish first as we need it to start the randomX VM cache. @@ -120,9 +136,11 @@ impl ContextTask { weight_cache: weight_cache_handle.await.unwrap()?, rx_vm_cache: rx_seed_handle.await.unwrap()?, hardfork_state, + alt_chain_cache_map: AltChainMap::new(), chain_height, already_generated_coins, top_block_hash, + database, }; Ok(context_svc) @@ -211,6 +229,98 @@ impl ContextTask { BlockChainContextResponse::Ok } + BlockChainContextRequest::PopBlocks { numb_blocks } => { + assert!(numb_blocks < self.chain_height); + + self.difficulty_cache + .pop_blocks_main_chain(numb_blocks, self.database.clone()) + .await?; + self.weight_cache + .pop_blocks_main_chain(numb_blocks, self.database.clone()) + .await?; + self.rx_vm_cache + .pop_blocks_main_chain(self.chain_height - numb_blocks - 1); + self.hardfork_state + .pop_blocks_main_chain(numb_blocks, self.database.clone()) + .await?; + + self.alt_chain_cache_map.clear(); + + self.chain_height -= numb_blocks; + + let BCResponse::GeneratedCoins(already_generated_coins) = self + .database + .ready() + .await? + .call(BCReadRequest::GeneratedCoins(self.chain_height - 1)) + .await? + else { + panic!("Database sent incorrect response!"); + }; + + let BCResponse::BlockHash(top_block_hash) = self + .database + .ready() + .await? + .call(BCReadRequest::BlockHash(self.chain_height - 1, Chain::Main)) + .await? + else { + panic!("Database returned incorrect response!"); + }; + + self.already_generated_coins = already_generated_coins; + self.top_block_hash = top_block_hash; + + std::mem::replace(&mut self.current_validity_token, ValidityToken::new()) + .set_data_invalid(); + + BlockChainContextResponse::Ok + } + BlockChainContextRequest::ClearAltCache => { + self.alt_chain_cache_map.clear(); + + BlockChainContextResponse::Ok + } + BlockChainContextRequest::AltChainContextCache { prev_id, _token } => { + BlockChainContextResponse::AltChainContextCache( + self.alt_chain_cache_map + .get_alt_chain_context(prev_id, &mut self.database) + .await?, + ) + } + BlockChainContextRequest::AltChainDifficultyCache { prev_id, _token } => { + BlockChainContextResponse::AltChainDifficultyCache( + get_alt_chain_difficulty_cache( + prev_id, + &self.difficulty_cache, + self.database.clone(), + ) + .await?, + ) + } + BlockChainContextRequest::AltChainWeightCache { prev_id, _token } => { + BlockChainContextResponse::AltChainWeightCache( + get_alt_chain_weight_cache(prev_id, &self.weight_cache, self.database.clone()) + .await?, + ) + } + BlockChainContextRequest::AltChainRxVM { + height, + chain, + _token, + } => BlockChainContextResponse::AltChainRxVM( + self.rx_vm_cache + .get_alt_vm(height, chain, &mut self.database) + .await?, + ), + BlockChainContextRequest::AddAltChainContextCache { + prev_id, + cache, + _token, + } => { + self.alt_chain_cache_map.add_alt_cache(prev_id, cache); + BlockChainContextResponse::Ok + } }) } diff --git a/consensus/src/context/weight.rs b/consensus/src/context/weight.rs index 2b9289ec..10840863 100644 --- a/consensus/src/context/weight.rs +++ b/consensus/src/context/weight.rs @@ -8,36 +8,37 @@ //! use std::{ cmp::{max, min}, - collections::VecDeque, ops::Range, }; -use rayon::prelude::*; use tower::ServiceExt; use tracing::instrument; use cuprate_consensus_rules::blocks::{penalty_free_zone, PENALTY_FREE_ZONE_5}; -use cuprate_helper::{asynch::rayon_spawn_async, num::median}; -use cuprate_types::blockchain::{BCReadRequest, BCResponse}; +use cuprate_helper::{asynch::rayon_spawn_async, num::RollingMedian}; +use cuprate_types::{ + blockchain::{BCReadRequest, BCResponse}, + Chain, +}; use crate::{Database, ExtendedConsensusError, HardFork}; /// The short term block weight window. -const SHORT_TERM_WINDOW: usize = 100; +const SHORT_TERM_WINDOW: u64 = 100; /// The long term block weight window. -const LONG_TERM_WINDOW: usize = 100000; +const LONG_TERM_WINDOW: u64 = 100000; /// Configuration for the block weight cache. /// -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct BlockWeightsCacheConfig { - short_term_window: usize, - long_term_window: usize, + short_term_window: u64, + long_term_window: u64, } impl BlockWeightsCacheConfig { /// Creates a new [`BlockWeightsCacheConfig`] - pub const fn new(short_term_window: usize, long_term_window: usize) -> BlockWeightsCacheConfig { + pub const fn new(short_term_window: u64, long_term_window: u64) -> BlockWeightsCacheConfig { BlockWeightsCacheConfig { short_term_window, long_term_window, @@ -58,78 +59,134 @@ impl BlockWeightsCacheConfig { /// /// These calculations require a lot of data from the database so by caching /// this data it reduces the load on the database. -#[derive(Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct BlockWeightsCache { /// The short term block weights. - short_term_block_weights: VecDeque, + short_term_block_weights: RollingMedian, /// The long term block weights. - long_term_weights: VecDeque, - - /// The short term block weights sorted so we don't have to sort them every time we need - /// the median. - cached_sorted_long_term_weights: Vec, - /// The long term block weights sorted so we don't have to sort them every time we need - /// the median. - cached_sorted_short_term_weights: Vec, + long_term_weights: RollingMedian, /// The height of the top block. - tip_height: usize, + pub(crate) tip_height: u64, - /// The block weight config. - config: BlockWeightsCacheConfig, + pub(crate) config: BlockWeightsCacheConfig, } impl BlockWeightsCache { /// Initialize the [`BlockWeightsCache`] at the the given chain height. #[instrument(name = "init_weight_cache", level = "info", skip(database, config))] pub async fn init_from_chain_height( - chain_height: usize, + chain_height: u64, config: BlockWeightsCacheConfig, database: D, + chain: Chain, ) -> Result { tracing::info!("Initializing weight cache this may take a while."); let long_term_weights = get_long_term_weight_in_range( chain_height.saturating_sub(config.long_term_window)..chain_height, database.clone(), + chain, ) .await?; let short_term_block_weights = get_blocks_weight_in_range( chain_height.saturating_sub(config.short_term_window)..chain_height, database, + chain, ) .await?; tracing::info!("Initialized block weight cache, chain-height: {:?}, long term weights length: {:?}, short term weights length: {:?}", chain_height, long_term_weights.len(), short_term_block_weights.len()); - let mut cloned_short_term_weights = short_term_block_weights.clone(); - let mut cloned_long_term_weights = long_term_weights.clone(); Ok(BlockWeightsCache { - short_term_block_weights: short_term_block_weights.into(), - long_term_weights: long_term_weights.into(), - - cached_sorted_long_term_weights: rayon_spawn_async(|| { - cloned_long_term_weights.par_sort_unstable(); - cloned_long_term_weights + short_term_block_weights: rayon_spawn_async(move || { + RollingMedian::from_vec( + short_term_block_weights, + usize::try_from(config.short_term_window).unwrap(), + ) }) .await, - cached_sorted_short_term_weights: rayon_spawn_async(|| { - cloned_short_term_weights.par_sort_unstable(); - cloned_short_term_weights + long_term_weights: rayon_spawn_async(move || { + RollingMedian::from_vec( + long_term_weights, + usize::try_from(config.long_term_window).unwrap(), + ) }) .await, - tip_height: chain_height - 1, config, }) } + /// Pop some blocks from the top of the cache. + /// + /// The cache will be returned to the state it would have been in `numb_blocks` ago. + #[instrument(name = "pop_blocks_weight_cache", skip_all, fields(numb_blocks = numb_blocks))] + pub async fn pop_blocks_main_chain( + &mut self, + numb_blocks: u64, + database: D, + ) -> Result<(), ExtendedConsensusError> { + if self.long_term_weights.window_len() <= usize::try_from(numb_blocks).unwrap() { + // More blocks to pop than we have in the cache, so just restart a new cache. + *self = Self::init_from_chain_height( + self.tip_height - numb_blocks + 1, + self.config, + database, + Chain::Main, + ) + .await?; + + return Ok(()); + } + + let chain_height = self.tip_height + 1; + + let new_long_term_start_height = chain_height + .saturating_sub(self.config.long_term_window) + .saturating_sub(numb_blocks); + + let old_long_term_weights = get_long_term_weight_in_range( + new_long_term_start_height + // current_chain_height - self.long_term_weights.len() blocks are already in the cache. + ..(chain_height - u64::try_from(self.long_term_weights.window_len()).unwrap()), + database.clone(), + Chain::Main, + ) + .await?; + + let new_short_term_start_height = chain_height + .saturating_sub(self.config.short_term_window) + .saturating_sub(numb_blocks); + + let old_short_term_weights = get_blocks_weight_in_range( + new_short_term_start_height + // current_chain_height - self.long_term_weights.len() blocks are already in the cache. + ..(chain_height - u64::try_from(self.short_term_block_weights.window_len()).unwrap()), + database, + Chain::Main + ) + .await?; + + for _ in 0..numb_blocks { + self.short_term_block_weights.pop_back(); + self.long_term_weights.pop_back(); + } + + self.long_term_weights.append_front(old_long_term_weights); + self.short_term_block_weights + .append_front(old_short_term_weights); + self.tip_height -= numb_blocks; + + Ok(()) + } + /// Add a new block to the cache. /// /// The block_height **MUST** be one more than the last height the cache has /// seen. - pub fn new_block(&mut self, block_height: usize, block_weight: usize, long_term_weight: usize) { + pub fn new_block(&mut self, block_height: u64, block_weight: usize, long_term_weight: usize) { assert_eq!(self.tip_height + 1, block_height); self.tip_height += 1; tracing::debug!( @@ -139,72 +196,19 @@ impl BlockWeightsCache { long_term_weight ); - // add the new block to the `long_term_weights` list and the sorted `cached_sorted_long_term_weights` list. - self.long_term_weights.push_back(long_term_weight); - match self - .cached_sorted_long_term_weights - .binary_search(&long_term_weight) - { - Ok(idx) | Err(idx) => self - .cached_sorted_long_term_weights - .insert(idx, long_term_weight), - } + self.long_term_weights.push(long_term_weight); - // If the list now has too many entries remove the oldest. - if self.long_term_weights.len() > self.config.long_term_window { - let val = self - .long_term_weights - .pop_front() - .expect("long term window can't be negative"); - - match self.cached_sorted_long_term_weights.binary_search(&val) { - Ok(idx) => self.cached_sorted_long_term_weights.remove(idx), - Err(_) => panic!("Long term cache has incorrect values!"), - }; - } - - // add the block to the short_term_block_weights and the sorted cached_sorted_short_term_weights list. - self.short_term_block_weights.push_back(block_weight); - match self - .cached_sorted_short_term_weights - .binary_search(&block_weight) - { - Ok(idx) | Err(idx) => self - .cached_sorted_short_term_weights - .insert(idx, block_weight), - } - - // If there are now too many entries remove the oldest. - if self.short_term_block_weights.len() > self.config.short_term_window { - let val = self - .short_term_block_weights - .pop_front() - .expect("short term window can't be negative"); - - match self.cached_sorted_short_term_weights.binary_search(&val) { - Ok(idx) => self.cached_sorted_short_term_weights.remove(idx), - Err(_) => panic!("Short term cache has incorrect values"), - }; - } - - debug_assert_eq!( - self.cached_sorted_long_term_weights.len(), - self.long_term_weights.len() - ); - debug_assert_eq!( - self.cached_sorted_short_term_weights.len(), - self.short_term_block_weights.len() - ); + self.short_term_block_weights.push(block_weight); } /// Returns the median long term weight over the last [`LONG_TERM_WINDOW`] blocks, or custom amount of blocks in the config. pub fn median_long_term_weight(&self) -> usize { - median(&self.cached_sorted_long_term_weights) + self.long_term_weights.median() } /// Returns the median weight over the last [`SHORT_TERM_WINDOW`] blocks, or custom amount of blocks in the config. pub fn median_short_term_weight(&self) -> usize { - median(&self.cached_sorted_short_term_weights) + self.short_term_block_weights.median() } /// Returns the effective median weight, used for block reward calculations and to calculate @@ -286,13 +290,14 @@ pub fn calculate_block_long_term_weight( /// Gets the block weights from the blocks with heights in the range provided. #[instrument(name = "get_block_weights", skip(database))] async fn get_blocks_weight_in_range( - range: Range, + range: Range, database: D, + chain: Chain, ) -> Result, ExtendedConsensusError> { tracing::info!("getting block weights."); let BCResponse::BlockExtendedHeaderInRange(ext_headers) = database - .oneshot(BCReadRequest::BlockExtendedHeaderInRange(range)) + .oneshot(BCReadRequest::BlockExtendedHeaderInRange(range, chain)) .await? else { panic!("Database sent incorrect response!") @@ -307,13 +312,14 @@ async fn get_blocks_weight_in_range( /// Gets the block long term weights from the blocks with heights in the range provided. #[instrument(name = "get_long_term_weights", skip(database), level = "info")] async fn get_long_term_weight_in_range( - range: Range, + range: Range, database: D, + chain: Chain, ) -> Result, ExtendedConsensusError> { tracing::info!("getting block long term weights."); let BCResponse::BlockExtendedHeaderInRange(ext_headers) = database - .oneshot(BCReadRequest::BlockExtendedHeaderInRange(range)) + .oneshot(BCReadRequest::BlockExtendedHeaderInRange(range, chain)) .await? else { panic!("Database sent incorrect response!") diff --git a/consensus/src/tests/context/difficulty.rs b/consensus/src/tests/context/difficulty.rs index c9886f3c..b59f62ef 100644 --- a/consensus/src/tests/context/difficulty.rs +++ b/consensus/src/tests/context/difficulty.rs @@ -1,15 +1,15 @@ use std::collections::VecDeque; -use proptest::collection::size_range; +use proptest::collection::{size_range, vec}; use proptest::{prelude::*, prop_assert_eq, prop_compose, proptest}; -use cuprate_helper::num::median; - use crate::{ context::difficulty::*, tests::{context::data::DIF_3000000_3002000, mock_db::*}, HardFork, }; +use cuprate_helper::num::median; +use cuprate_types::Chain; const TEST_WINDOW: usize = 72; const TEST_CUT: usize = 6; @@ -26,9 +26,13 @@ async fn first_3_blocks_fixed_difficulty() -> Result<(), tower::BoxError> { let genesis = DummyBlockExtendedHeader::default().with_difficulty_info(0, 1); db_builder.add_block(genesis); - let mut difficulty_cache = - DifficultyCache::init_from_chain_height(1, TEST_DIFFICULTY_CONFIG, db_builder.finish(None)) - .await?; + let mut difficulty_cache = DifficultyCache::init_from_chain_height( + 1, + TEST_DIFFICULTY_CONFIG, + db_builder.finish(None), + Chain::Main, + ) + .await?; for height in 1..3 { assert_eq!(difficulty_cache.next_difficulty(&HardFork::V1), 1); @@ -42,9 +46,13 @@ async fn genesis_block_skipped() -> Result<(), tower::BoxError> { let mut db_builder = DummyDatabaseBuilder::default(); let genesis = DummyBlockExtendedHeader::default().with_difficulty_info(0, 1); db_builder.add_block(genesis); - let diff_cache = - DifficultyCache::init_from_chain_height(1, TEST_DIFFICULTY_CONFIG, db_builder.finish(None)) - .await?; + let diff_cache = DifficultyCache::init_from_chain_height( + 1, + TEST_DIFFICULTY_CONFIG, + db_builder.finish(None), + Chain::Main, + ) + .await?; assert!(diff_cache.cumulative_difficulties.is_empty()); assert!(diff_cache.timestamps.is_empty()); Ok(()) @@ -66,8 +74,9 @@ async fn calculate_diff_3000000_3002000() -> Result<(), tower::BoxError> { let mut diff_cache = DifficultyCache::init_from_chain_height( 3_000_720, - cfg.clone(), + cfg, db_builder.finish(Some(3_000_720)), + Chain::Main, ) .await?; @@ -208,4 +217,52 @@ proptest! { } } + + #[test] + fn pop_blocks_below_total_blocks( + mut database in arb_dummy_database(20), + new_blocks in vec(any::<(u64, u128)>(), 0..500) + ) { + tokio_test::block_on(async move { + let old_cache = DifficultyCache::init_from_chain_height(19, TEST_DIFFICULTY_CONFIG, database.clone(), Chain::Main).await.unwrap(); + + let blocks_to_pop = new_blocks.len(); + + let mut new_cache = old_cache.clone(); + for (timestamp, cumulative_difficulty) in new_blocks.into_iter() { + database.add_block(DummyBlockExtendedHeader::default().with_difficulty_info(timestamp, cumulative_difficulty)); + new_cache.new_block(new_cache.last_accounted_height+1, timestamp, cumulative_difficulty); + } + + new_cache.pop_blocks_main_chain(blocks_to_pop as u64, database).await?; + + prop_assert_eq!(new_cache, old_cache); + + Ok::<_, TestCaseError>(()) + })?; + } + + #[test] + fn pop_blocks_above_total_blocks( + mut database in arb_dummy_database(2000), + new_blocks in vec(any::<(u64, u128)>(), 0..5_000) + ) { + tokio_test::block_on(async move { + let old_cache = DifficultyCache::init_from_chain_height(1999, TEST_DIFFICULTY_CONFIG, database.clone(), Chain::Main).await.unwrap(); + + let blocks_to_pop = new_blocks.len(); + + let mut new_cache = old_cache.clone(); + for (timestamp, cumulative_difficulty) in new_blocks.into_iter() { + database.add_block(DummyBlockExtendedHeader::default().with_difficulty_info(timestamp, cumulative_difficulty)); + new_cache.new_block(new_cache.last_accounted_height+1, timestamp, cumulative_difficulty); + } + + new_cache.pop_blocks_main_chain(blocks_to_pop as u64, database).await?; + + prop_assert_eq!(new_cache, old_cache); + + Ok::<_, TestCaseError>(()) + })?; + } } diff --git a/consensus/src/tests/context/hardforks.rs b/consensus/src/tests/context/hardforks.rs index f6f0f234..d003b3cc 100644 --- a/consensus/src/tests/context/hardforks.rs +++ b/consensus/src/tests/context/hardforks.rs @@ -1,3 +1,5 @@ +use proptest::{collection::vec, prelude::*}; + use cuprate_consensus_rules::hard_forks::{HFInfo, HFsInfo, HardFork, NUMB_OF_HARD_FORKS}; use crate::{ @@ -82,3 +84,44 @@ async fn hf_v15_v16_correct() { assert_eq!(state.current_hardfork, HardFork::V16); } + +proptest! { + fn pop_blocks( + hfs in vec(any::(), 0..100), + extra_hfs in vec(any::(), 0..100) + ) { + tokio_test::block_on(async move { + let numb_hfs = hfs.len() as u64; + let numb_pop_blocks = extra_hfs.len() as u64; + + let mut db_builder = DummyDatabaseBuilder::default(); + + for hf in hfs { + db_builder.add_block( + DummyBlockExtendedHeader::default().with_hard_fork_info(hf, hf), + ); + } + + let db = db_builder.finish(Some(numb_hfs as usize)); + + let mut state = HardForkState::init_from_chain_height( + numb_hfs, + TEST_HARD_FORK_CONFIG, + db.clone(), + ) + .await?; + + let state_clone = state.clone(); + + for (i, hf) in extra_hfs.into_iter().enumerate() { + state.new_block(hf, state.last_height + u64::try_from(i).unwrap() + 1); + } + + state.pop_blocks_main_chain(numb_pop_blocks, db).await?; + + prop_assert_eq!(state_clone, state); + + Ok::<(), TestCaseError>(()) + })?; + } +} diff --git a/consensus/src/tests/context/weight.rs b/consensus/src/tests/context/weight.rs index 902d446a..83c8bb95 100644 --- a/consensus/src/tests/context/weight.rs +++ b/consensus/src/tests/context/weight.rs @@ -6,6 +6,7 @@ use crate::{ tests::{context::data::BW_2850000_3050000, mock_db::*}, HardFork, }; +use cuprate_types::Chain; pub const TEST_WEIGHT_CONFIG: BlockWeightsCacheConfig = BlockWeightsCacheConfig::new(100, 5000); @@ -21,6 +22,7 @@ async fn blocks_out_of_window_not_counted() -> Result<(), tower::BoxError> { 5000, TEST_WEIGHT_CONFIG, db_builder.finish(None), + Chain::Main, ) .await?; assert_eq!(weight_cache.median_long_term_weight(), 2500); @@ -37,6 +39,74 @@ async fn blocks_out_of_window_not_counted() -> Result<(), tower::BoxError> { Ok(()) } +#[tokio::test] +async fn pop_blocks_greater_than_window() -> Result<(), tower::BoxError> { + let mut db_builder = DummyDatabaseBuilder::default(); + for weight in 1..=5000 { + let block = DummyBlockExtendedHeader::default().with_weight_into(weight, weight); + db_builder.add_block(block); + } + + let database = db_builder.finish(None); + + let mut weight_cache = BlockWeightsCache::init_from_chain_height( + 5000, + TEST_WEIGHT_CONFIG, + database.clone(), + Chain::Main, + ) + .await?; + + let old_cache = weight_cache.clone(); + + weight_cache.new_block(5000, 0, 0); + weight_cache.new_block(5001, 0, 0); + weight_cache.new_block(5002, 0, 0); + + weight_cache + .pop_blocks_main_chain(3, database) + .await + .unwrap(); + + assert_eq!(weight_cache, old_cache); + + Ok(()) +} + +#[tokio::test] +async fn pop_blocks_less_than_window() -> Result<(), tower::BoxError> { + let mut db_builder = DummyDatabaseBuilder::default(); + for weight in 1..=500 { + let block = DummyBlockExtendedHeader::default().with_weight_into(weight, weight); + db_builder.add_block(block); + } + + let database = db_builder.finish(None); + + let mut weight_cache = BlockWeightsCache::init_from_chain_height( + 500, + TEST_WEIGHT_CONFIG, + database.clone(), + Chain::Main, + ) + .await?; + + let old_cache = weight_cache.clone(); + + weight_cache.new_block(500, 0, 0); + weight_cache.new_block(501, 0, 0); + weight_cache.new_block(502, 0, 0); + + weight_cache + .pop_blocks_main_chain(3, database) + .await + .unwrap(); + + assert_eq!(weight_cache, old_cache); + + Ok(()) +} + #[tokio::test] async fn weight_cache_calculates_correct_median() -> Result<(), tower::BoxError> { let mut db_builder = DummyDatabaseBuilder::default(); @@ -44,9 +114,13 @@ async fn weight_cache_calculates_correct_median() -> Result<(), tower::BoxError> let block = DummyBlockExtendedHeader::default().with_weight_into(0, 0); db_builder.add_block(block); - let mut weight_cache = - BlockWeightsCache::init_from_chain_height(1, TEST_WEIGHT_CONFIG, db_builder.finish(None)) - .await?; + let mut weight_cache = BlockWeightsCache::init_from_chain_height( + 1, + TEST_WEIGHT_CONFIG, + db_builder.finish(None), + Chain::Main, + ) + .await?; for height in 1..=100 { weight_cache.new_block(height as u64, height, height); @@ -76,6 +150,7 @@ async fn calc_bw_ltw_2850000_3050000() { 2950000, TEST_WEIGHT_CONFIG, db_builder.finish(Some(2950000)), + Chain::Main, ) .await .unwrap(); diff --git a/consensus/src/tests/mock_db.rs b/consensus/src/tests/mock_db.rs index d1c62550..c4fd75d1 100644 --- a/consensus/src/tests/mock_db.rs +++ b/consensus/src/tests/mock_db.rs @@ -127,6 +127,12 @@ pub struct DummyDatabase { dummy_height: Option, } +impl DummyDatabase { + pub fn add_block(&mut self, block: DummyBlockExtendedHeader) { + self.blocks.write().unwrap().push(block) + } +} + impl Service for DummyDatabase { type Response = BCResponse; type Error = BoxError; @@ -161,12 +167,12 @@ impl Service for DummyDatabase { .ok_or("block not in database!")?, ) } - BCReadRequest::BlockHash(id) => { + BCReadRequest::BlockHash(id, _) => { let mut hash = [0; 32]; hash[0..8].copy_from_slice(&id.to_le_bytes()); BCResponse::BlockHash(hash) } - BCReadRequest::BlockExtendedHeaderInRange(range) => { + BCReadRequest::BlockExtendedHeaderInRange(range, _) => { let mut end = usize::try_from(range.end).unwrap(); let mut start = usize::try_from(range.start).unwrap(); @@ -200,7 +206,7 @@ impl Service for DummyDatabase { BCResponse::ChainHeight(height, top_hash) } - BCReadRequest::GeneratedCoins => BCResponse::GeneratedCoins(0), + BCReadRequest::GeneratedCoins(_) => BCResponse::GeneratedCoins(0), _ => unimplemented!("the context svc should not need these requests!"), }) } diff --git a/helper/src/num.rs b/helper/src/num.rs index cc1feb1b..f90357e9 100644 --- a/helper/src/num.rs +++ b/helper/src/num.rs @@ -8,6 +8,9 @@ use core::{ ops::{Add, Div, Mul, Sub}, }; +#[cfg(feature = "std")] +mod rolling_median; + //---------------------------------------------------------------------------------------------------- Types // INVARIANT: must be private. // Protects against outside-crate implementations. @@ -15,6 +18,9 @@ mod private { pub trait Sealed: Copy + PartialOrd + core::fmt::Display {} } +#[cfg(feature = "std")] +pub use rolling_median::RollingMedian; + /// Non-floating point numbers /// /// This trait is sealed and is only implemented on: diff --git a/helper/src/num/rolling_median.rs b/helper/src/num/rolling_median.rs new file mode 100644 index 00000000..2babda2c --- /dev/null +++ b/helper/src/num/rolling_median.rs @@ -0,0 +1,150 @@ +use std::{ + collections::VecDeque, + ops::{Add, Div, Mul, Sub}, +}; + +use crate::num::median; + +/// A rolling median type. +/// +/// This keeps track of a window of items and allows calculating the [`RollingMedian::median`] of them. +/// +/// Example: +/// ```rust +/// # use cuprate_helper::num::RollingMedian; +/// let mut rolling_median = RollingMedian::new(2); +/// +/// rolling_median.push(1); +/// assert_eq!(rolling_median.median(), 1); +/// assert_eq!(rolling_median.window_len(), 1); +/// +/// rolling_median.push(3); +/// assert_eq!(rolling_median.median(), 2); +/// assert_eq!(rolling_median.window_len(), 2); +/// +/// rolling_median.push(5); +/// assert_eq!(rolling_median.median(), 4); +/// assert_eq!(rolling_median.window_len(), 2); +/// ``` +/// +// TODO: a more efficient structure is probably possible. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] +pub struct RollingMedian { + /// The window of items, in order of insertion. + window: VecDeque, + /// The window of items, sorted. + sorted_window: Vec, + + /// The target window length. + target_window: usize, +} + +impl RollingMedian +where + T: Ord + + PartialOrd + + Add + + Sub + + Div + + Mul + + Copy + + From, +{ + /// Creates a new [`RollingMedian`] with a certain target window length. + /// + /// `target_window` is the maximum amount of items to keep in the rolling window. + pub fn new(target_window: usize) -> Self { + Self { + window: VecDeque::with_capacity(target_window), + sorted_window: Vec::with_capacity(target_window), + target_window, + } + } + + /// Creates a new [`RollingMedian`] from a [`Vec`] with a certain target window length. + /// + /// `target_window` is the maximum amount of items to keep in the rolling window. + /// + /// # Panics + /// This function panics if `vec.len() > target_window`. + pub fn from_vec(vec: Vec, target_window: usize) -> Self { + assert!(vec.len() <= target_window); + + let mut sorted_window = vec.clone(); + sorted_window.sort_unstable(); + + Self { + window: vec.into(), + sorted_window, + target_window, + } + } + + /// Pops the front of the window, i.e. the oldest item. + /// + /// This is often not needed as [`RollingMedian::push`] will handle popping old values when they fall + /// out of the window. + pub fn pop_front(&mut self) { + if let Some(item) = self.window.pop_front() { + match self.sorted_window.binary_search(&item) { + Ok(idx) => { + self.sorted_window.remove(idx); + } + Err(_) => panic!("Value expected to be in sorted_window was not there"), + } + } + } + + /// Pops the back of the window, i.e. the youngest item. + pub fn pop_back(&mut self) { + if let Some(item) = self.window.pop_back() { + match self.sorted_window.binary_search(&item) { + Ok(idx) => { + self.sorted_window.remove(idx); + } + Err(_) => panic!("Value expected to be in sorted_window was not there"), + } + } + } + + /// Push an item to the _back_ of the window. + /// + /// This will pop the oldest item in the window if the target length has been exceeded. + pub fn push(&mut self, item: T) { + if self.window.len() >= self.target_window { + self.pop_front(); + } + + self.window.push_back(item); + match self.sorted_window.binary_search(&item) { + Ok(idx) | Err(idx) => self.sorted_window.insert(idx, item), + } + } + + /// Append some values to the _front_ of the window. + /// + /// These new values will be the oldest items in the window. The order of the inputted items will be + /// kept, i.e. the first item in the [`Vec`] will be the oldest item in the queue. + pub fn append_front(&mut self, items: Vec) { + for item in items.into_iter().rev() { + self.window.push_front(item); + match self.sorted_window.binary_search(&item) { + Ok(idx) | Err(idx) => self.sorted_window.insert(idx, item), + } + + if self.window.len() > self.target_window { + self.pop_back(); + } + } + } + + /// Returns the number of items currently in the [`RollingMedian`]. + pub fn window_len(&self) -> usize { + self.window.len() + } + + /// Calculates the median of the values currently in the [`RollingMedian`]. + pub fn median(&self) -> T { + median(&self.sorted_window) + } +} diff --git a/helper/src/time.rs b/helper/src/time.rs index 7bc155f6..28aff7f5 100644 --- a/helper/src/time.rs +++ b/helper/src/time.rs @@ -55,7 +55,7 @@ pub const fn unix_clock(seconds_after_unix_epoch: u64) -> u32 { /// - The seconds returned is guaranteed to be `0..=59` /// - The minutes returned is guaranteed to be `0..=59` /// - The hours returned can be over `23`, as this is not a clock function, -/// see [`secs_to_clock`] for clock-like behavior that wraps around on `24` +/// see [`secs_to_clock`] for clock-like behavior that wraps around on `24` /// /// ```rust /// # use cuprate_helper::time::*; diff --git a/net/fixed-bytes/Cargo.toml b/net/fixed-bytes/Cargo.toml index b592a09e..4c5a1afb 100644 --- a/net/fixed-bytes/Cargo.toml +++ b/net/fixed-bytes/Cargo.toml @@ -6,9 +6,14 @@ license = "MIT" authors = ["Boog900"] [features] -default = ["std"] +default = ["std", "serde"] std = ["bytes/std", "dep:thiserror"] +serde = ["bytes/serde", "dep:serde"] [dependencies] thiserror = { workspace = true, optional = true } -bytes = { workspace = true } \ No newline at end of file +bytes = { workspace = true } +serde = { workspace = true, features = ["derive"], optional = true } + +[dev-dependencies] +serde_json = { workspace = true, features = ["std"] } diff --git a/net/fixed-bytes/README.md b/net/fixed-bytes/README.md new file mode 100644 index 00000000..b96c9fc3 --- /dev/null +++ b/net/fixed-bytes/README.md @@ -0,0 +1,10 @@ +# `cuprate-fixed-bytes` +TODO + +# Feature flags +| Feature flag | Does what | +|--------------|-----------| +| `std` | TODO +| `serde` | Enables `serde` on applicable types + +`serde` is enabled by default. \ No newline at end of file diff --git a/net/fixed-bytes/src/lib.rs b/net/fixed-bytes/src/lib.rs index 8776d309..2e8f1bc5 100644 --- a/net/fixed-bytes/src/lib.rs +++ b/net/fixed-bytes/src/lib.rs @@ -1,3 +1,5 @@ +#![doc = include_str!("../README.md")] + use core::{ fmt::{Debug, Formatter}, ops::{Deref, Index}, @@ -5,7 +7,12 @@ use core::{ use bytes::{BufMut, Bytes, BytesMut}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize}; + #[cfg_attr(feature = "std", derive(thiserror::Error))] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] pub enum FixedByteError { #[cfg_attr( feature = "std", @@ -42,9 +49,31 @@ impl Debug for FixedByteError { /// /// Internally this is just a wrapper around [`Bytes`], with the constructors checking that the length is equal to `N`. /// This implements [`Deref`] with the target being `[u8; N]`. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[repr(transparent)] pub struct ByteArray(Bytes); +#[cfg(feature = "serde")] +impl<'de, const N: usize> Deserialize<'de> for ByteArray { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes = Bytes::deserialize(deserializer)?; + let len = bytes.len(); + if len == N { + Ok(Self(bytes)) + } else { + Err(serde::de::Error::invalid_length( + len, + &N.to_string().as_str(), + )) + } + } +} + impl ByteArray { pub fn take_bytes(self) -> Bytes { self.0 @@ -87,9 +116,31 @@ impl TryFrom> for ByteArray { } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[repr(transparent)] pub struct ByteArrayVec(Bytes); +#[cfg(feature = "serde")] +impl<'de, const N: usize> Deserialize<'de> for ByteArrayVec { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes = Bytes::deserialize(deserializer)?; + let len = bytes.len(); + if len % N == 0 { + Ok(Self(bytes)) + } else { + Err(serde::de::Error::invalid_length( + len, + &N.to_string().as_str(), + )) + } + } +} + impl ByteArrayVec { pub fn len(&self) -> usize { self.0.len() / N @@ -197,6 +248,8 @@ impl Index for ByteArrayVec { #[cfg(test)] mod tests { + use serde_json::{from_str, to_string}; + use super::*; #[test] @@ -207,4 +260,46 @@ mod tests { assert_eq!(bytes.len(), 100); let _ = bytes[99]; } + + /// Tests that `serde` works on [`ByteArray`]. + #[test] + #[cfg(feature = "serde")] + fn byte_array_serde() { + let b = ByteArray::from([1, 0, 0, 0, 1]); + let string = to_string(&b).unwrap(); + assert_eq!(string, "[1,0,0,0,1]"); + let b2 = from_str::>(&string).unwrap(); + assert_eq!(b, b2); + } + + /// Tests that `serde` works on [`ByteArrayVec`]. + #[test] + #[cfg(feature = "serde")] + fn byte_array_vec_serde() { + let b = ByteArrayVec::from([1, 0, 0, 0, 1]); + let string = to_string(&b).unwrap(); + assert_eq!(string, "[1,0,0,0,1]"); + let b2 = from_str::>(&string).unwrap(); + assert_eq!(b, b2); + } + + /// Tests that bad input `serde` fails on [`ByteArray`]. + #[test] + #[cfg(feature = "serde")] + #[should_panic( + expected = r#"called `Result::unwrap()` on an `Err` value: Error("invalid length 4, expected 5", line: 0, column: 0)"# + )] + fn byte_array_bad_deserialize() { + from_str::>("[1,0,0,0]").unwrap(); + } + + /// Tests that bad input `serde` fails on [`ByteArrayVec`]. + #[test] + #[cfg(feature = "serde")] + #[should_panic( + expected = r#"called `Result::unwrap()` on an `Err` value: Error("invalid length 4, expected 5", line: 0, column: 0)"# + )] + fn byte_array_vec_bad_deserialize() { + from_str::>("[1,0,0,0]").unwrap(); + } } diff --git a/net/wire/Cargo.toml b/net/wire/Cargo.toml index c71a77b8..101daa39 100644 --- a/net/wire/Cargo.toml +++ b/net/wire/Cargo.toml @@ -14,6 +14,7 @@ tracing = ["cuprate-levin/tracing"] cuprate-levin = { path = "../levin" } cuprate-epee-encoding = { path = "../epee-encoding" } cuprate-fixed-bytes = { path = "../fixed-bytes" } +cuprate-types = { path = "../../types", default-features = false, features = ["epee"] } bitflags = { workspace = true, features = ["std"] } bytes = { workspace = true, features = ["std"] } diff --git a/net/wire/src/lib.rs b/net/wire/src/lib.rs index 45a2405c..674a2e91 100644 --- a/net/wire/src/lib.rs +++ b/net/wire/src/lib.rs @@ -13,10 +13,10 @@ // copies or substantial portions of the Software. // -//! # Monero Wire +//! # Cuprate Wire //! //! A crate defining Monero network messages and network addresses, -//! built on top of the levin-cuprate crate. +//! built on top of the [`cuprate_levin`] crate. //! //! ## License //! diff --git a/net/wire/src/p2p.rs b/net/wire/src/p2p.rs index 0d448e4f..97431099 100644 --- a/net/wire/src/p2p.rs +++ b/net/wire/src/p2p.rs @@ -177,6 +177,7 @@ fn build_message( Ok(()) } +#[derive(Debug, Clone)] pub enum ProtocolMessage { NewBlock(NewBlock), NewFluffyBlock(NewFluffyBlock), @@ -255,22 +256,23 @@ impl ProtocolMessage { } } -pub enum RequestMessage { +#[derive(Debug, Clone)] +pub enum AdminRequestMessage { Handshake(HandshakeRequest), Ping, SupportFlags, TimedSync(TimedSyncRequest), } -impl RequestMessage { +impl AdminRequestMessage { pub fn command(&self) -> LevinCommand { use LevinCommand as C; match self { - RequestMessage::Handshake(_) => C::Handshake, - RequestMessage::Ping => C::Ping, - RequestMessage::SupportFlags => C::SupportFlags, - RequestMessage::TimedSync(_) => C::TimedSync, + AdminRequestMessage::Handshake(_) => C::Handshake, + AdminRequestMessage::Ping => C::Ping, + AdminRequestMessage::SupportFlags => C::SupportFlags, + AdminRequestMessage::TimedSync(_) => C::TimedSync, } } @@ -278,19 +280,19 @@ impl RequestMessage { use LevinCommand as C; Ok(match command { - C::Handshake => decode_message(RequestMessage::Handshake, buf)?, - C::TimedSync => decode_message(RequestMessage::TimedSync, buf)?, + C::Handshake => decode_message(AdminRequestMessage::Handshake, buf)?, + C::TimedSync => decode_message(AdminRequestMessage::TimedSync, buf)?, C::Ping => { cuprate_epee_encoding::from_bytes::(buf) .map_err(|e| BucketError::BodyDecodingError(e.into()))?; - RequestMessage::Ping + AdminRequestMessage::Ping } C::SupportFlags => { cuprate_epee_encoding::from_bytes::(buf) .map_err(|e| BucketError::BodyDecodingError(e.into()))?; - RequestMessage::SupportFlags + AdminRequestMessage::SupportFlags } _ => return Err(BucketError::UnknownCommand), }) @@ -300,31 +302,34 @@ impl RequestMessage { use LevinCommand as C; match self { - RequestMessage::Handshake(val) => build_message(C::Handshake, val, builder)?, - RequestMessage::TimedSync(val) => build_message(C::TimedSync, val, builder)?, - RequestMessage::Ping => build_message(C::Ping, EmptyMessage, builder)?, - RequestMessage::SupportFlags => build_message(C::SupportFlags, EmptyMessage, builder)?, + AdminRequestMessage::Handshake(val) => build_message(C::Handshake, val, builder)?, + AdminRequestMessage::TimedSync(val) => build_message(C::TimedSync, val, builder)?, + AdminRequestMessage::Ping => build_message(C::Ping, EmptyMessage, builder)?, + AdminRequestMessage::SupportFlags => { + build_message(C::SupportFlags, EmptyMessage, builder)? + } } Ok(()) } } -pub enum ResponseMessage { +#[derive(Debug, Clone)] +pub enum AdminResponseMessage { Handshake(HandshakeResponse), Ping(PingResponse), SupportFlags(SupportFlagsResponse), TimedSync(TimedSyncResponse), } -impl ResponseMessage { +impl AdminResponseMessage { pub fn command(&self) -> LevinCommand { use LevinCommand as C; match self { - ResponseMessage::Handshake(_) => C::Handshake, - ResponseMessage::Ping(_) => C::Ping, - ResponseMessage::SupportFlags(_) => C::SupportFlags, - ResponseMessage::TimedSync(_) => C::TimedSync, + AdminResponseMessage::Handshake(_) => C::Handshake, + AdminResponseMessage::Ping(_) => C::Ping, + AdminResponseMessage::SupportFlags(_) => C::SupportFlags, + AdminResponseMessage::TimedSync(_) => C::TimedSync, } } @@ -332,10 +337,10 @@ impl ResponseMessage { use LevinCommand as C; Ok(match command { - C::Handshake => decode_message(ResponseMessage::Handshake, buf)?, - C::TimedSync => decode_message(ResponseMessage::TimedSync, buf)?, - C::Ping => decode_message(ResponseMessage::Ping, buf)?, - C::SupportFlags => decode_message(ResponseMessage::SupportFlags, buf)?, + C::Handshake => decode_message(AdminResponseMessage::Handshake, buf)?, + C::TimedSync => decode_message(AdminResponseMessage::TimedSync, buf)?, + C::Ping => decode_message(AdminResponseMessage::Ping, buf)?, + C::SupportFlags => decode_message(AdminResponseMessage::SupportFlags, buf)?, _ => return Err(BucketError::UnknownCommand), }) } @@ -344,18 +349,21 @@ impl ResponseMessage { use LevinCommand as C; match self { - ResponseMessage::Handshake(val) => build_message(C::Handshake, val, builder)?, - ResponseMessage::TimedSync(val) => build_message(C::TimedSync, val, builder)?, - ResponseMessage::Ping(val) => build_message(C::Ping, val, builder)?, - ResponseMessage::SupportFlags(val) => build_message(C::SupportFlags, val, builder)?, + AdminResponseMessage::Handshake(val) => build_message(C::Handshake, val, builder)?, + AdminResponseMessage::TimedSync(val) => build_message(C::TimedSync, val, builder)?, + AdminResponseMessage::Ping(val) => build_message(C::Ping, val, builder)?, + AdminResponseMessage::SupportFlags(val) => { + build_message(C::SupportFlags, val, builder)? + } } Ok(()) } } +#[derive(Debug, Clone)] pub enum Message { - Request(RequestMessage), - Response(ResponseMessage), + Request(AdminRequestMessage), + Response(AdminResponseMessage), Protocol(ProtocolMessage), } @@ -390,8 +398,10 @@ impl LevinBody for Message { command: LevinCommand, ) -> Result { Ok(match typ { - MessageType::Request => Message::Request(RequestMessage::decode(body, command)?), - MessageType::Response => Message::Response(ResponseMessage::decode(body, command)?), + MessageType::Request => Message::Request(AdminRequestMessage::decode(body, command)?), + MessageType::Response => { + Message::Response(AdminResponseMessage::decode(body, command)?) + } MessageType::Notification => Message::Protocol(ProtocolMessage::decode(body, command)?), }) } diff --git a/net/wire/src/p2p/common.rs b/net/wire/src/p2p/common.rs index 91adb908..d585d071 100644 --- a/net/wire/src/p2p/common.rs +++ b/net/wire/src/p2p/common.rs @@ -16,10 +16,9 @@ //! Common types that are used across multiple messages. use bitflags::bitflags; -use bytes::{Buf, BufMut, Bytes}; -use cuprate_epee_encoding::{epee_object, EpeeValue, InnerMarker}; -use cuprate_fixed_bytes::ByteArray; +use cuprate_epee_encoding::epee_object; +pub use cuprate_types::{BlockCompleteEntry, PrunedTxBlobEntry, TransactionBlobs}; use crate::NetworkAddress; @@ -168,113 +167,6 @@ epee_object! { rpc_credits_per_hash: u32 = 0_u32, } -/// A pruned tx with the hash of the missing prunable data -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PrunedTxBlobEntry { - /// The Tx - pub tx: Bytes, - /// The Prunable Tx Hash - pub prunable_hash: ByteArray<32>, -} - -epee_object!( - PrunedTxBlobEntry, - tx: Bytes, - prunable_hash: ByteArray<32>, -); - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TransactionBlobs { - Pruned(Vec), - Normal(Vec), - None, -} - -impl TransactionBlobs { - pub fn take_pruned(self) -> Option> { - match self { - TransactionBlobs::Normal(_) => None, - TransactionBlobs::Pruned(txs) => Some(txs), - TransactionBlobs::None => Some(vec![]), - } - } - - pub fn take_normal(self) -> Option> { - match self { - TransactionBlobs::Normal(txs) => Some(txs), - TransactionBlobs::Pruned(_) => None, - TransactionBlobs::None => Some(vec![]), - } - } - - pub fn len(&self) -> usize { - match self { - TransactionBlobs::Normal(txs) => txs.len(), - TransactionBlobs::Pruned(txs) => txs.len(), - TransactionBlobs::None => 0, - } - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -/// A Block that can contain transactions -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct BlockCompleteEntry { - /// True if tx data is pruned - pub pruned: bool, - /// The Block - pub block: Bytes, - /// The Block Weight/Size - pub block_weight: u64, - /// The blocks txs - pub txs: TransactionBlobs, -} - -epee_object!( - BlockCompleteEntry, - pruned: bool = false, - block: Bytes, - block_weight: u64 = 0_u64, - txs: TransactionBlobs = TransactionBlobs::None => tx_blob_read, tx_blob_write, should_write_tx_blobs, -); - -fn tx_blob_read(b: &mut B) -> cuprate_epee_encoding::Result { - let marker = cuprate_epee_encoding::read_marker(b)?; - match marker.inner_marker { - InnerMarker::Object => Ok(TransactionBlobs::Pruned(Vec::read(b, &marker)?)), - InnerMarker::String => Ok(TransactionBlobs::Normal(Vec::read(b, &marker)?)), - _ => Err(cuprate_epee_encoding::Error::Value( - "Invalid marker for tx blobs".to_string(), - )), - } -} - -fn tx_blob_write( - val: TransactionBlobs, - field_name: &str, - w: &mut B, -) -> cuprate_epee_encoding::Result<()> { - if should_write_tx_blobs(&val) { - match val { - TransactionBlobs::Normal(bytes) => { - cuprate_epee_encoding::write_field(bytes, field_name, w)? - } - TransactionBlobs::Pruned(obj) => { - cuprate_epee_encoding::write_field(obj, field_name, w)? - } - TransactionBlobs::None => (), - } - } - Ok(()) -} - -fn should_write_tx_blobs(val: &TransactionBlobs) -> bool { - !val.is_empty() -} - #[cfg(test)] mod tests { diff --git a/net/wire/src/p2p/protocol.rs b/net/wire/src/p2p/protocol.rs index 5e95a4f8..73694d57 100644 --- a/net/wire/src/p2p/protocol.rs +++ b/net/wire/src/p2p/protocol.rs @@ -16,14 +16,14 @@ //! This module defines Monero protocol messages //! //! Protocol message requests don't have to be responded to in order unlike -//! admin messages. +//! admin messages. use bytes::Bytes; use cuprate_epee_encoding::{container_as_blob::ContainerAsBlob, epee_object}; use cuprate_fixed_bytes::{ByteArray, ByteArrayVec}; -use super::common::BlockCompleteEntry; +use crate::p2p::common::BlockCompleteEntry; /// A block that SHOULD have transactions #[derive(Debug, Clone, PartialEq, Eq)] @@ -61,7 +61,7 @@ epee_object!( /// A Request For Blocks #[derive(Debug, Clone, PartialEq, Eq)] pub struct GetObjectsRequest { - /// Block hashes we want + /// Block hashes wanted. pub blocks: ByteArrayVec<32>, /// Pruned pub pruned: bool, diff --git a/p2p/address-book/src/book.rs b/p2p/address-book/src/book.rs index ba9c6671..2f0ce6db 100644 --- a/p2p/address-book/src/book.rs +++ b/p2p/address-book/src/book.rs @@ -27,7 +27,10 @@ use cuprate_p2p_core::{ }; use cuprate_pruning::PruningSeed; -use crate::{peer_list::PeerList, store::save_peers_to_disk, AddressBookConfig, AddressBookError}; +use crate::{ + peer_list::PeerList, store::save_peers_to_disk, AddressBookConfig, AddressBookError, + BorshNetworkZone, +}; #[cfg(test)] mod tests; @@ -45,7 +48,7 @@ pub struct ConnectionPeerEntry { rpc_credits_per_hash: u32, } -pub struct AddressBook { +pub struct AddressBook { /// Our white peers - the peers we have previously connected to. white_list: PeerList, /// Our gray peers - the peers we have been told about but haven't connected to. @@ -66,7 +69,7 @@ pub struct AddressBook { cfg: AddressBookConfig, } -impl AddressBook { +impl AddressBook { pub fn new( cfg: AddressBookConfig, white_peers: Vec>, @@ -351,7 +354,7 @@ impl AddressBook { } } -impl Service> for AddressBook { +impl Service> for AddressBook { type Response = AddressBookResponse; type Error = AddressBookError; type Future = Ready>; diff --git a/p2p/address-book/src/book/tests.rs b/p2p/address-book/src/book/tests.rs index 11f31868..1abea043 100644 --- a/p2p/address-book/src/book/tests.rs +++ b/p2p/address-book/src/book/tests.rs @@ -1,7 +1,7 @@ -use std::{path::PathBuf, sync::Arc, time::Duration}; +use std::{path::PathBuf, time::Duration}; use futures::StreamExt; -use tokio::{sync::Semaphore, time::interval}; +use tokio::time::interval; use cuprate_p2p_core::handles::HandleBuilder; use cuprate_pruning::PruningSeed; @@ -78,11 +78,7 @@ async fn get_white_peers() { async fn add_new_peer_already_connected() { let mut address_book = make_fake_address_book(0, 0); - let semaphore = Arc::new(Semaphore::new(10)); - - let (_, handle) = HandleBuilder::default() - .with_permit(semaphore.clone().try_acquire_owned().unwrap()) - .build(); + let (_, handle) = HandleBuilder::default().build(); address_book .handle_new_connection( @@ -98,9 +94,7 @@ async fn add_new_peer_already_connected() { ) .unwrap(); - let (_, handle) = HandleBuilder::default() - .with_permit(semaphore.try_acquire_owned().unwrap()) - .build(); + let (_, handle) = HandleBuilder::default().build(); assert_eq!( address_book.handle_new_connection( diff --git a/p2p/address-book/src/lib.rs b/p2p/address-book/src/lib.rs index 1ce659f1..c0903485 100644 --- a/p2p/address-book/src/lib.rs +++ b/p2p/address-book/src/lib.rs @@ -10,10 +10,9 @@ //! clear net peers getting linked to their dark counterparts //! and so peers will only get told about peers they can //! connect to. -//! use std::{io::ErrorKind, path::PathBuf, time::Duration}; -use cuprate_p2p_core::NetworkZone; +use cuprate_p2p_core::{NetZoneAddress, NetworkZone}; mod book; mod peer_list; @@ -61,7 +60,7 @@ pub enum AddressBookError { } /// Initializes the P2P address book for a specific network zone. -pub async fn init_address_book( +pub async fn init_address_book( cfg: AddressBookConfig, ) -> Result, std::io::Error> { tracing::info!( @@ -82,3 +81,21 @@ pub async fn init_address_book( Ok(address_book) } + +use sealed::BorshNetworkZone; +mod sealed { + use super::*; + + /// An internal trait for the address book for a [`NetworkZone`] that adds the requirement of [`borsh`] traits + /// onto the network address. + pub trait BorshNetworkZone: NetworkZone { + type BorshAddr: NetZoneAddress + borsh::BorshDeserialize + borsh::BorshSerialize; + } + + impl BorshNetworkZone for T + where + T::Addr: borsh::BorshDeserialize + borsh::BorshSerialize, + { + type BorshAddr = T::Addr; + } +} diff --git a/p2p/address-book/src/store.rs b/p2p/address-book/src/store.rs index 94b0ec24..abc42d69 100644 --- a/p2p/address-book/src/store.rs +++ b/p2p/address-book/src/store.rs @@ -3,9 +3,9 @@ use std::fs; use borsh::{from_slice, to_vec, BorshDeserialize, BorshSerialize}; use tokio::task::{spawn_blocking, JoinHandle}; -use cuprate_p2p_core::{services::ZoneSpecificPeerListEntryBase, NetZoneAddress, NetworkZone}; +use cuprate_p2p_core::{services::ZoneSpecificPeerListEntryBase, NetZoneAddress}; -use crate::{peer_list::PeerList, AddressBookConfig}; +use crate::{peer_list::PeerList, AddressBookConfig, BorshNetworkZone}; // TODO: store anchor and ban list. @@ -21,7 +21,7 @@ struct DeserPeerDataV1 { gray_list: Vec>, } -pub fn save_peers_to_disk( +pub fn save_peers_to_disk( cfg: &AddressBookConfig, white_list: &PeerList, gray_list: &PeerList, @@ -38,7 +38,7 @@ pub fn save_peers_to_disk( spawn_blocking(move || fs::write(&file, &data)) } -pub async fn read_peers_from_disk( +pub async fn read_peers_from_disk( cfg: &AddressBookConfig, ) -> Result< ( diff --git a/p2p/dandelion-tower/Cargo.toml b/p2p/dandelion-tower/Cargo.toml index 5e2fec53..976dad60 100644 --- a/p2p/dandelion-tower/Cargo.toml +++ b/p2p/dandelion-tower/Cargo.toml @@ -10,7 +10,7 @@ default = ["txpool"] txpool = ["dep:rand_distr", "dep:tokio-util", "dep:tokio"] [dependencies] -tower = { workspace = true, features = ["discover", "util"] } +tower = { workspace = true, features = ["util"] } tracing = { workspace = true, features = ["std"] } futures = { workspace = true, features = ["std"] } diff --git a/p2p/dandelion-tower/src/config.rs b/p2p/dandelion-tower/src/config.rs index 71a4e5b2..6266d60a 100644 --- a/p2p/dandelion-tower/src/config.rs +++ b/p2p/dandelion-tower/src/config.rs @@ -42,7 +42,6 @@ pub enum Graph { /// `(-k*(k-1)*hop)/(2*log(1-ep))` /// /// Where `k` is calculated from the fluff probability, `hop` is `time_between_hop` and `ep` is fixed at `0.1`. -/// #[derive(Debug, Clone, Copy)] pub struct DandelionConfig { /// The time it takes for a stem transaction to pass through a node, including network latency. diff --git a/p2p/dandelion-tower/src/lib.rs b/p2p/dandelion-tower/src/lib.rs index f162724f..aa622f30 100644 --- a/p2p/dandelion-tower/src/lib.rs +++ b/p2p/dandelion-tower/src/lib.rs @@ -26,9 +26,9 @@ //! The diffuse service should have a request of [`DiffuseRequest`](traits::DiffuseRequest) and it's error //! should be [`tower::BoxError`]. //! -//! ## Outbound Peer Discoverer +//! ## Outbound Peer TryStream //! -//! The outbound peer [`Discover`](tower::discover::Discover) should provide a stream of randomly selected outbound +//! The outbound peer [`TryStream`](futures::TryStream) should provide a stream of randomly selected outbound //! peers, these peers will then be used to route stem txs to. //! //! The peers will not be returned anywhere, so it is recommended to wrap them in some sort of drop guard that returns @@ -37,10 +37,10 @@ //! ## Peer Service //! //! This service represents a connection to an individual peer, this should be returned from the Outbound Peer -//! Discover. This should immediately send the transaction to the peer when requested, i.e. it should _not_ set +//! TryStream. This should immediately send the transaction to the peer when requested, it should _not_ set //! a timer. //! -//! The diffuse service should have a request of [`StemRequest`](traits::StemRequest) and it's error +//! The peer service should have a request of [`StemRequest`](traits::StemRequest) and its error //! should be [`tower::BoxError`]. //! //! ## Backing Pool diff --git a/p2p/dandelion-tower/src/pool.rs b/p2p/dandelion-tower/src/pool.rs index eddcc670..5f4f7346 100644 --- a/p2p/dandelion-tower/src/pool.rs +++ b/p2p/dandelion-tower/src/pool.rs @@ -16,7 +16,6 @@ //! //! When using your handle to the backing store it must be remembered to keep transactions in the stem pool hidden. //! So handle any requests to the tx-pool like the stem side of the pool does not exist. -//! use std::{ collections::{HashMap, HashSet}, future::Future, @@ -52,7 +51,7 @@ use crate::{ /// /// - `buffer_size` is the size of the channel's buffer between the [`DandelionPoolService`] and [`DandelionPool`]. /// - `dandelion_router` is the router service, kept generic instead of [`DandelionRouter`](crate::DandelionRouter) to allow -/// user to customise routing functionality. +/// user to customise routing functionality. /// - `backing_pool` is the backing transaction storage service /// - `config` is [`DandelionConfig`]. pub fn start_dandelion_pool( diff --git a/p2p/dandelion-tower/src/router.rs b/p2p/dandelion-tower/src/router.rs index 61e962c3..c118c0b7 100644 --- a/p2p/dandelion-tower/src/router.rs +++ b/p2p/dandelion-tower/src/router.rs @@ -6,11 +6,9 @@ //! ### What The Router Does Not Do //! //! It does not handle anything to do with keeping transactions long term, i.e. embargo timers and handling -//! loops in the stem. It is up to implementers to do this if they decide not top use [`DandelionPool`](crate::pool::DandelionPool) -//! +//! loops in the stem. It is up to implementers to do this if they decide not to use [`DandelionPool`](crate::pool::DandelionPool) use std::{ collections::HashMap, - future::Future, hash::Hash, marker::PhantomData, pin::Pin, @@ -18,12 +16,9 @@ use std::{ time::Instant, }; -use futures::TryFutureExt; +use futures::{future::BoxFuture, FutureExt, TryFutureExt, TryStream}; use rand::{distributions::Bernoulli, prelude::*, thread_rng}; -use tower::{ - discover::{Change, Discover}, - Service, -}; +use tower::Service; use crate::{ traits::{DiffuseRequest, StemRequest}, @@ -39,14 +34,22 @@ pub enum DandelionRouterError { /// The broadcast service returned an error. #[error("Broadcast service returned an err: {0}.")] BroadcastError(tower::BoxError), - /// The outbound peer discoverer returned an error, this is critical. - #[error("The outbound peer discoverer returned an err: {0}.")] - OutboundPeerDiscoverError(tower::BoxError), + /// The outbound peer stream returned an error, this is critical. + #[error("The outbound peer stream returned an err: {0}.")] + OutboundPeerStreamError(tower::BoxError), /// The outbound peer discoverer returned [`None`]. #[error("The outbound peer discoverer exited.")] OutboundPeerDiscoverExited, } +/// A response from an attempt to retrieve an outbound peer. +pub enum OutboundPeer { + /// A peer. + Peer(ID, T), + /// The peer store is exhausted and has no more to return. + Exhausted, +} + /// The dandelion++ state. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum State { @@ -116,9 +119,11 @@ pub struct DandelionRouter { impl DandelionRouter where ID: Hash + Eq + Clone, - P: Discover, + P: TryStream, Error = tower::BoxError>, B: Service, Error = tower::BoxError>, + B::Future: Send + 'static, S: Service, Error = tower::BoxError>, + S::Future: Send + 'static, { /// Creates a new [`DandelionRouter`], with the provided services and config. /// @@ -165,15 +170,16 @@ where match ready!(self .outbound_peer_discover .as_mut() - .poll_discover(cx) - .map_err(DandelionRouterError::OutboundPeerDiscoverError)) + .try_poll_next(cx) + .map_err(DandelionRouterError::OutboundPeerStreamError)) .ok_or(DandelionRouterError::OutboundPeerDiscoverExited)?? { - Change::Insert(key, svc) => { + OutboundPeer::Peer(key, svc) => { self.stem_peers.insert(key, svc); } - Change::Remove(key) => { - self.stem_peers.remove(&key); + OutboundPeer::Exhausted => { + tracing::warn!("Failed to retrieve enough outbound peers for optimal dandelion++, privacy may be degraded."); + return Poll::Ready(Ok(())); } } } @@ -181,11 +187,24 @@ where Poll::Ready(Ok(())) } - fn fluff_tx(&mut self, tx: Tx) -> B::Future { - self.broadcast_svc.call(DiffuseRequest(tx)) + fn fluff_tx(&mut self, tx: Tx) -> BoxFuture<'static, Result> { + self.broadcast_svc + .call(DiffuseRequest(tx)) + .map_ok(|_| State::Fluff) + .map_err(DandelionRouterError::BroadcastError) + .boxed() } - fn stem_tx(&mut self, tx: Tx, from: ID) -> S::Future { + fn stem_tx( + &mut self, + tx: Tx, + from: ID, + ) -> BoxFuture<'static, Result> { + if self.stem_peers.is_empty() { + tracing::debug!("Stem peers are empty, fluffing stem transaction."); + return self.fluff_tx(tx); + } + loop { let stem_route = self.stem_routes.entry(from.clone()).or_insert_with(|| { self.stem_peers @@ -201,11 +220,20 @@ where continue; }; - return peer.call(StemRequest(tx)); + return peer + .call(StemRequest(tx)) + .map_ok(|_| State::Stem) + .map_err(DandelionRouterError::PeerError) + .boxed(); } } - fn stem_local_tx(&mut self, tx: Tx) -> S::Future { + fn stem_local_tx(&mut self, tx: Tx) -> BoxFuture<'static, Result> { + if self.stem_peers.is_empty() { + tracing::warn!("Stem peers are empty, no outbound connections to stem local tx to, fluffing instead, privacy will be degraded."); + return self.fluff_tx(tx); + } + loop { let stem_route = self.local_route.get_or_insert_with(|| { self.stem_peers @@ -221,7 +249,11 @@ where continue; }; - return peer.call(StemRequest(tx)); + return peer + .call(StemRequest(tx)) + .map_ok(|_| State::Stem) + .map_err(DandelionRouterError::PeerError) + .boxed(); } } } @@ -238,7 +270,7 @@ S: The Peer service - handles routing messages to a single node. impl Service> for DandelionRouter where ID: Hash + Eq + Clone, - P: Discover, + P: TryStream, Error = tower::BoxError>, B: Service, Error = tower::BoxError>, B::Future: Send + 'static, S: Service, Error = tower::BoxError>, @@ -246,8 +278,7 @@ where { type Response = State; type Error = DandelionRouterError; - type Future = - Pin> + Send + 'static>>; + type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { if self.epoch_start.elapsed() > self.config.epoch_duration { @@ -309,39 +340,23 @@ where tracing::trace!(parent: &self.span, "Handling route request."); match req.state { - TxState::Fluff => Box::pin( - self.fluff_tx(req.tx) - .map_ok(|_| State::Fluff) - .map_err(DandelionRouterError::BroadcastError), - ), + TxState::Fluff => self.fluff_tx(req.tx), TxState::Stem { from } => match self.current_state { State::Fluff => { tracing::debug!(parent: &self.span, "Fluffing stem tx."); - Box::pin( - self.fluff_tx(req.tx) - .map_ok(|_| State::Fluff) - .map_err(DandelionRouterError::BroadcastError), - ) + self.fluff_tx(req.tx) } State::Stem => { tracing::trace!(parent: &self.span, "Steming transaction"); - Box::pin( - self.stem_tx(req.tx, from) - .map_ok(|_| State::Stem) - .map_err(DandelionRouterError::PeerError), - ) + self.stem_tx(req.tx, from) } }, TxState::Local => { tracing::debug!(parent: &self.span, "Steming local tx."); - Box::pin( - self.stem_local_tx(req.tx) - .map_ok(|_| State::Stem) - .map_err(DandelionRouterError::PeerError), - ) + self.stem_local_tx(req.tx) } } } diff --git a/p2p/dandelion-tower/src/tests/mod.rs b/p2p/dandelion-tower/src/tests/mod.rs index 1f3ba3e8..d868a991 100644 --- a/p2p/dandelion-tower/src/tests/mod.rs +++ b/p2p/dandelion-tower/src/tests/mod.rs @@ -3,44 +3,48 @@ mod router; use std::{collections::HashMap, future::Future, hash::Hash, sync::Arc}; -use futures::TryStreamExt; +use futures::{Stream, StreamExt, TryStreamExt}; use tokio::sync::mpsc::{self, UnboundedReceiver}; -use tower::{ - discover::{Discover, ServiceList}, - util::service_fn, - Service, ServiceExt, -}; +use tower::{util::service_fn, Service, ServiceExt}; use crate::{ traits::{TxStoreRequest, TxStoreResponse}, - State, + OutboundPeer, State, }; pub fn mock_discover_svc() -> ( - impl Discover< - Key = usize, - Service = impl Service< - Req, - Future = impl Future> + Send + 'static, - Error = tower::BoxError, - > + Send - + 'static, - Error = tower::BoxError, + impl Stream< + Item = Result< + OutboundPeer< + usize, + impl Service< + Req, + Future = impl Future> + Send + 'static, + Error = tower::BoxError, + > + Send + + 'static, + >, + tower::BoxError, + >, >, - UnboundedReceiver<(u64, Req)>, + UnboundedReceiver<(usize, Req)>, ) { let (tx, rx) = mpsc::unbounded_channel(); - let discover = ServiceList::new((0..).map(move |i| { - let tx_2 = tx.clone(); + let discover = futures::stream::iter(0_usize..1_000_000) + .map(move |i| { + let tx_2 = tx.clone(); - service_fn(move |req| { - tx_2.send((i, req)).unwrap(); + Ok::<_, tower::BoxError>(OutboundPeer::Peer( + i, + service_fn(move |req| { + tx_2.send((i, req)).unwrap(); - async move { Ok::<(), tower::BoxError>(()) } + async move { Ok::<(), tower::BoxError>(()) } + }), + )) }) - })) - .map_err(Into::into); + .map_err(Into::into); (discover, rx) } diff --git a/p2p/p2p-core/Cargo.toml b/p2p/p2p-core/Cargo.toml index f434d51a..9ef8e249 100644 --- a/p2p/p2p-core/Cargo.toml +++ b/p2p/p2p-core/Cargo.toml @@ -23,6 +23,7 @@ tower = { workspace = true, features = ["util", "tracing"] } thiserror = { workspace = true } tracing = { workspace = true, features = ["std", "attributes"] } +hex-literal = { workspace = true } borsh = { workspace = true, features = ["derive", "std"], optional = true } @@ -31,4 +32,5 @@ cuprate-test-utils = {path = "../../test-utils"} hex = { workspace = true, features = ["std"] } tokio = { workspace = true, features = ["net", "rt-multi-thread", "rt", "macros"]} +tokio-test = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/p2p/p2p-core/src/client.rs b/p2p/p2p-core/src/client.rs index 0e81d964..662a8eee 100644 --- a/p2p/p2p-core/src/client.rs +++ b/p2p/p2p-core/src/client.rs @@ -24,10 +24,11 @@ use crate::{ mod connection; mod connector; pub mod handshaker; +mod request_handler; mod timeout_monitor; pub use connector::{ConnectRequest, Connector}; -pub use handshaker::{DoHandshakeRequest, HandShaker, HandshakeError}; +pub use handshaker::{DoHandshakeRequest, HandshakeError, HandshakerBuilder}; /// An internal identifier for a given peer, will be their address if known /// or a random u128 if not. @@ -188,7 +189,8 @@ pub fn mock_client( mut request_handler: S, ) -> Client where - S: crate::PeerRequestHandler, + S: Service + Send + 'static, + S::Future: Send + 'static, { let (tx, mut rx) = mpsc::channel(1); diff --git a/p2p/p2p-core/src/client/connection.rs b/p2p/p2p-core/src/client/connection.rs index 341d8c09..f3f3f6be 100644 --- a/p2p/p2p-core/src/client/connection.rs +++ b/p2p/p2p-core/src/client/connection.rs @@ -2,7 +2,6 @@ //! //! This module handles routing requests from a [`Client`](crate::client::Client) or a broadcast channel to //! a peer. This module also handles routing requests from the connected peer to a request handler. -//! use std::pin::Pin; use futures::{ @@ -15,15 +14,15 @@ use tokio::{ time::{sleep, timeout, Sleep}, }; use tokio_stream::wrappers::ReceiverStream; -use tower::ServiceExt; use cuprate_wire::{LevinCommand, Message, ProtocolMessage}; +use crate::client::request_handler::PeerRequestHandler; use crate::{ constants::{REQUEST_TIMEOUT, SENDING_TIMEOUT}, handles::ConnectionGuard, - BroadcastMessage, MessageID, NetworkZone, PeerError, PeerRequest, PeerRequestHandler, - PeerResponse, SharedError, + AddressBook, BroadcastMessage, CoreSyncSvc, MessageID, NetworkZone, PeerError, PeerRequest, + PeerResponse, PeerSyncSvc, ProtocolRequestHandler, ProtocolResponse, SharedError, }; /// A request to the connection task from a [`Client`](crate::client::Client). @@ -72,7 +71,7 @@ fn levin_command_response(message_id: &MessageID, command: LevinCommand) -> bool } /// This represents a connection to a peer. -pub struct Connection { +pub struct Connection { /// The peer sink - where we send messages to the peer. peer_sink: Z::Sink, @@ -87,7 +86,7 @@ pub struct Connection { broadcast_stream: Pin>, /// The inner handler for any requests that come from the requested peer. - peer_request_handler: ReqHndlr, + peer_request_handler: PeerRequestHandler, /// The connection guard which will send signals to other parts of Cuprate when this connection is dropped. connection_guard: ConnectionGuard, @@ -95,9 +94,13 @@ pub struct Connection { error: SharedError, } -impl Connection +impl Connection where - ReqHndlr: PeerRequestHandler, + Z: NetworkZone, + A: AddressBook, + CS: CoreSyncSvc, + PS: PeerSyncSvc, + PR: ProtocolRequestHandler, BrdcstStrm: Stream + Send + 'static, { /// Create a new connection struct. @@ -105,10 +108,10 @@ where peer_sink: Z::Sink, client_rx: mpsc::Receiver, broadcast_stream: BrdcstStrm, - peer_request_handler: ReqHndlr, + peer_request_handler: PeerRequestHandler, connection_guard: ConnectionGuard, error: SharedError, - ) -> Connection { + ) -> Connection { Connection { peer_sink, state: State::WaitingForRequest, @@ -175,7 +178,9 @@ where return Err(e); } else { // We still need to respond even if the response is this. - let _ = req.response_channel.send(Ok(PeerResponse::NA)); + let _ = req + .response_channel + .send(Ok(PeerResponse::Protocol(ProtocolResponse::NA))); } Ok(()) @@ -185,17 +190,14 @@ where async fn handle_peer_request(&mut self, req: PeerRequest) -> Result<(), PeerError> { tracing::debug!("Received peer request: {:?}", req.id()); - let ready_svc = self.peer_request_handler.ready().await?; - let res = ready_svc.call(req).await?; - if matches!(res, PeerResponse::NA) { - return Ok(()); + let res = self.peer_request_handler.handle_peer_request(req).await?; + + // This will be an error if a response does not need to be sent + if let Ok(res) = res.try_into() { + self.send_message_to_peer(res).await?; } - self.send_message_to_peer( - res.try_into() - .expect("We just checked if the response was `NA`"), - ) - .await + Ok(()) } /// Handles a message from a peer when we are in [`State::WaitingForResponse`]. diff --git a/p2p/p2p-core/src/client/connector.rs b/p2p/p2p-core/src/client/connector.rs index 278d7407..d937165f 100644 --- a/p2p/p2p-core/src/client/connector.rs +++ b/p2p/p2p-core/src/client/connector.rs @@ -4,7 +4,6 @@ //! perform a handshake and create a [`Client`]. //! //! This is where outbound connections are created. -//! use std::{ future::Future, pin::Pin, @@ -16,9 +15,9 @@ use tokio::sync::OwnedSemaphorePermit; use tower::{Service, ServiceExt}; use crate::{ - client::{Client, DoHandshakeRequest, HandShaker, HandshakeError, InternalPeerID}, - AddressBook, BroadcastMessage, ConnectionDirection, CoreSyncSvc, NetworkZone, - PeerRequestHandler, PeerSyncSvc, + client::{handshaker::HandShaker, Client, DoHandshakeRequest, HandshakeError, InternalPeerID}, + AddressBook, BroadcastMessage, ConnectionDirection, CoreSyncSvc, NetworkZone, PeerSyncSvc, + ProtocolRequestHandler, }; /// A request to connect to a peer. @@ -27,30 +26,32 @@ pub struct ConnectRequest { pub addr: Z::Addr, /// A permit which will be held be the connection allowing you to set limits on the number of /// connections. - pub permit: OwnedSemaphorePermit, + /// + /// This doesn't have to be set. + pub permit: Option, } /// The connector service, this service connects to peer and returns the [`Client`]. -pub struct Connector { - handshaker: HandShaker, +pub struct Connector { + handshaker: HandShaker, } -impl - Connector +impl + Connector { /// Create a new connector from a handshaker. - pub fn new(handshaker: HandShaker) -> Self { + pub fn new(handshaker: HandShaker) -> Self { Self { handshaker } } } -impl - Service> for Connector +impl + Service> for Connector where AdrBook: AddressBook + Clone, CSync: CoreSyncSvc + Clone, PSync: PeerSyncSvc + Clone, - ReqHdlr: PeerRequestHandler + Clone, + ProtoHdlr: ProtocolRequestHandler + Clone, BrdcstStrm: Stream + Send + 'static, BrdcstStrmMkr: Fn(InternalPeerID) -> BrdcstStrm + Clone + Send + 'static, { @@ -74,7 +75,7 @@ where permit: req.permit, peer_stream, peer_sink, - direction: ConnectionDirection::OutBound, + direction: ConnectionDirection::Outbound, }; handshaker.ready().await?.call(req).await } diff --git a/p2p/p2p-core/src/client/handshaker.rs b/p2p/p2p-core/src/client/handshaker.rs index 1071b339..67a58d48 100644 --- a/p2p/p2p-core/src/client/handshaker.rs +++ b/p2p/p2p-core/src/client/handshaker.rs @@ -18,7 +18,7 @@ use tokio::{ time::{error::Elapsed, timeout}, }; use tower::{Service, ServiceExt}; -use tracing::{info_span, Instrument}; +use tracing::{info_span, Instrument, Span}; use cuprate_pruning::{PruningError, PruningSeed}; use cuprate_wire::{ @@ -27,13 +27,13 @@ use cuprate_wire::{ PING_OK_RESPONSE_STATUS_TEXT, }, common::PeerSupportFlags, - BasicNodeData, BucketError, LevinCommand, Message, RequestMessage, ResponseMessage, + AdminRequestMessage, AdminResponseMessage, BasicNodeData, BucketError, LevinCommand, Message, }; use crate::{ client::{ - connection::Connection, timeout_monitor::connection_timeout_monitor_task, Client, - InternalPeerID, PeerInformation, + connection::Connection, request_handler::PeerRequestHandler, + timeout_monitor::connection_timeout_monitor_task, Client, InternalPeerID, PeerInformation, }, constants::{ HANDSHAKE_TIMEOUT, MAX_EAGER_PROTOCOL_MESSAGES, MAX_PEERS_IN_PEER_LIST_MESSAGE, @@ -43,9 +43,12 @@ use crate::{ services::PeerSyncRequest, AddressBook, AddressBookRequest, AddressBookResponse, BroadcastMessage, ConnectionDirection, CoreSyncDataRequest, CoreSyncDataResponse, CoreSyncSvc, NetZoneAddress, NetworkZone, - PeerRequestHandler, PeerSyncSvc, SharedError, + PeerSyncSvc, ProtocolRequestHandler, SharedError, }; +pub mod builder; +pub use builder::HandshakerBuilder; + #[derive(Debug, thiserror::Error)] pub enum HandshakeError { #[error("The handshake timed out")] @@ -78,21 +81,21 @@ pub struct DoHandshakeRequest { pub peer_sink: Z::Sink, /// The direction of the connection. pub direction: ConnectionDirection, - /// A permit for this connection. - pub permit: OwnedSemaphorePermit, + /// An [`Option`]al permit for this connection. + pub permit: Option, } /// The peer handshaking service. #[derive(Debug, Clone)] -pub struct HandShaker { +pub struct HandShaker { /// The address book service. address_book: AdrBook, /// The core sync data service. core_sync_svc: CSync, /// The peer sync service. peer_sync_svc: PSync, - /// The peer request handler service. - peer_request_svc: ReqHdlr, + /// The protocol request handler service. + protocol_request_svc: ProtoHdlr, /// Our [`BasicNodeData`] our_basic_node_data: BasicNodeData, @@ -100,42 +103,46 @@ pub struct HandShaker, } -impl - HandShaker +impl + HandShaker { /// Creates a new handshaker. - pub fn new( + fn new( address_book: AdrBook, peer_sync_svc: PSync, core_sync_svc: CSync, - peer_request_svc: ReqHdlr, + protocol_request_svc: ProtoHdlr, broadcast_stream_maker: BrdcstStrmMkr, - our_basic_node_data: BasicNodeData, + connection_parent_span: Span, ) -> Self { Self { address_book, peer_sync_svc, core_sync_svc, - peer_request_svc, + protocol_request_svc, broadcast_stream_maker, our_basic_node_data, + connection_parent_span, _zone: PhantomData, } } } -impl - Service> for HandShaker +impl + Service> + for HandShaker where AdrBook: AddressBook + Clone, CSync: CoreSyncSvc + Clone, PSync: PeerSyncSvc + Clone, - ReqHdlr: PeerRequestHandler + Clone, + ProtoHdlr: ProtocolRequestHandler + Clone, BrdcstStrm: Stream + Send + 'static, BrdcstStrmMkr: Fn(InternalPeerID) -> BrdcstStrm + Clone + Send + 'static, { @@ -152,12 +159,14 @@ where let broadcast_stream_maker = self.broadcast_stream_maker.clone(); let address_book = self.address_book.clone(); - let peer_request_svc = self.peer_request_svc.clone(); + let protocol_request_svc = self.protocol_request_svc.clone(); let core_sync_svc = self.core_sync_svc.clone(); let peer_sync_svc = self.peer_sync_svc.clone(); let our_basic_node_data = self.our_basic_node_data.clone(); - let span = info_span!(parent: &tracing::Span::current(), "handshaker", addr=%req.addr); + let connection_parent_span = self.connection_parent_span.clone(); + + let span = info_span!(parent: &Span::current(), "handshaker", addr=%req.addr); async move { timeout( @@ -168,8 +177,9 @@ where address_book, core_sync_svc, peer_sync_svc, - peer_request_svc, + protocol_request_svc, our_basic_node_data, + connection_parent_span, ), ) .await? @@ -190,11 +200,11 @@ pub async fn ping(addr: N::Addr) -> Result tracing::debug!("Made outbound connection to peer, sending ping."); peer_sink - .send(Message::Request(RequestMessage::Ping).into()) + .send(Message::Request(AdminRequestMessage::Ping).into()) .await?; if let Some(res) = peer_stream.next().await { - if let Message::Response(ResponseMessage::Ping(ping)) = res? { + if let Message::Response(AdminResponseMessage::Ping(ping)) = res? { if ping.status == PING_OK_RESPONSE_STATUS_TEXT { tracing::debug!("Ping successful."); return Ok(ping.peer_id); @@ -220,7 +230,8 @@ pub async fn ping(addr: N::Addr) -> Result } /// This function completes a handshake with the requested peer. -async fn handshake( +#[allow(clippy::too_many_arguments)] +async fn handshake( req: DoHandshakeRequest, broadcast_stream_maker: BrdcstStrmMkr, @@ -228,14 +239,15 @@ async fn handshake Result, HandshakeError> where - AdrBook: AddressBook, - CSync: CoreSyncSvc, - PSync: PeerSyncSvc, - ReqHdlr: PeerRequestHandler, + AdrBook: AddressBook + Clone, + CSync: CoreSyncSvc + Clone, + PSync: PeerSyncSvc + Clone, + ProtoHdlr: ProtocolRequestHandler, BrdcstStrm: Stream + Send + 'static, BrdcstStrmMkr: Fn(InternalPeerID) -> BrdcstStrm + Send + 'static, { @@ -252,19 +264,20 @@ where let mut eager_protocol_messages = Vec::new(); let (peer_core_sync, peer_node_data) = match direction { - ConnectionDirection::InBound => { + ConnectionDirection::Inbound => { // Inbound handshake the peer sends the request. tracing::debug!("waiting for handshake request."); - let Message::Request(RequestMessage::Handshake(handshake_req)) = wait_for_message::( - LevinCommand::Handshake, - true, - &mut peer_sink, - &mut peer_stream, - &mut eager_protocol_messages, - &our_basic_node_data, - ) - .await? + let Message::Request(AdminRequestMessage::Handshake(handshake_req)) = + wait_for_message::( + LevinCommand::Handshake, + true, + &mut peer_sink, + &mut peer_stream, + &mut eager_protocol_messages, + &our_basic_node_data, + ) + .await? else { panic!("wait_for_message returned ok with wrong message."); }; @@ -273,7 +286,7 @@ where // We will respond to the handshake request later. (handshake_req.payload_data, handshake_req.node_data) } - ConnectionDirection::OutBound => { + ConnectionDirection::Outbound => { // Outbound handshake, we send the request. send_hs_request::( &mut peer_sink, @@ -283,7 +296,7 @@ where .await?; // Wait for the handshake response. - let Message::Response(ResponseMessage::Handshake(handshake_res)) = + let Message::Response(AdminResponseMessage::Handshake(handshake_res)) = wait_for_message::( LevinCommand::Handshake, false, @@ -373,13 +386,13 @@ where // public_address, if Some, is the reachable address of the node. let public_address = 'check_out_addr: { match direction { - ConnectionDirection::InBound => { + ConnectionDirection::Inbound => { // First send the handshake response. send_hs_response::( &mut peer_sink, &mut core_sync_svc, &mut address_book, - our_basic_node_data, + our_basic_node_data.clone(), ) .await?; @@ -411,7 +424,7 @@ where // The peer did not specify a reachable port or the ping was not successful. None } - ConnectionDirection::OutBound => { + ConnectionDirection::Outbound => { let InternalPeerID::KnownAddr(outbound_addr) = addr else { unreachable!("How could we make an outbound connection to an unknown address"); }; @@ -424,37 +437,7 @@ where tracing::debug!("Handshake complete."); - // Set up the connection data. - let error_slot = SharedError::new(); let (connection_guard, handle) = HandleBuilder::new().with_permit(permit).build(); - let (connection_tx, client_rx) = mpsc::channel(1); - - let connection = Connection::::new( - peer_sink, - client_rx, - broadcast_stream_maker(addr), - peer_request_svc, - connection_guard, - error_slot.clone(), - ); - - let connection_span = tracing::error_span!(parent: &tracing::Span::none(), "connection", %addr); - let connection_handle = tokio::spawn( - connection - .run(peer_stream.fuse(), eager_protocol_messages) - .instrument(connection_span), - ); - - // Tell the core sync service about the new peer. - peer_sync_svc - .ready() - .await? - .call(PeerSyncRequest::IncomingCoreSyncData( - addr, - handle.clone(), - peer_core_sync, - )) - .await?; // Tell the address book about the new connection. address_book @@ -471,6 +454,21 @@ where }) .await?; + // Tell the core sync service about the new peer. + peer_sync_svc + .ready() + .await? + .call(PeerSyncRequest::IncomingCoreSyncData( + addr, + handle.clone(), + peer_core_sync, + )) + .await?; + + // Set up the connection data. + let error_slot = SharedError::new(); + let (connection_tx, client_rx) = mpsc::channel(1); + let info = PeerInformation { id: addr, handle, @@ -478,6 +476,32 @@ where pruning_seed, }; + let request_handler = PeerRequestHandler { + address_book_svc: address_book.clone(), + our_sync_svc: core_sync_svc.clone(), + peer_sync_svc: peer_sync_svc.clone(), + protocol_request_handler, + our_basic_node_data, + peer_info: info.clone(), + }; + + let connection = Connection::::new( + peer_sink, + client_rx, + broadcast_stream_maker(addr), + request_handler, + connection_guard, + error_slot.clone(), + ); + + let connection_span = + tracing::error_span!(parent: &connection_parent_span, "connection", %addr); + let connection_handle = tokio::spawn( + connection + .run(peer_stream.fuse(), eager_protocol_messages) + .instrument(connection_span), + ); + let semaphore = Arc::new(Semaphore::new(1)); let timeout_handle = tokio::spawn(connection_timeout_monitor_task( @@ -502,7 +526,7 @@ where Ok(client) } -/// Sends a [`RequestMessage::Handshake`] down the peer sink. +/// Sends a [`AdminRequestMessage::Handshake`] down the peer sink. async fn send_hs_request( peer_sink: &mut Z::Sink, core_sync_svc: &mut CSync, @@ -525,13 +549,13 @@ where tracing::debug!("Sending handshake request."); peer_sink - .send(Message::Request(RequestMessage::Handshake(req)).into()) + .send(Message::Request(AdminRequestMessage::Handshake(req)).into()) .await?; Ok(()) } -/// Sends a [`ResponseMessage::Handshake`] down the peer sink. +/// Sends a [`AdminResponseMessage::Handshake`] down the peer sink. async fn send_hs_response( peer_sink: &mut Z::Sink, core_sync_svc: &mut CSync, @@ -568,7 +592,7 @@ where tracing::debug!("Sending handshake response."); peer_sink - .send(Message::Response(ResponseMessage::Handshake(res)).into()) + .send(Message::Response(AdminResponseMessage::Handshake(res)).into()) .await?; Ok(()) @@ -619,7 +643,7 @@ async fn wait_for_message( } match req_message { - RequestMessage::SupportFlags => { + AdminRequestMessage::SupportFlags => { if !allow_support_flag_req { return Err(HandshakeError::PeerSentInvalidMessage( "Peer sent 2 support flag requests", @@ -631,7 +655,7 @@ async fn wait_for_message( allow_support_flag_req = false; continue; } - RequestMessage::Ping => { + AdminRequestMessage::Ping => { if !allow_ping { return Err(HandshakeError::PeerSentInvalidMessage( "Peer sent 2 ping requests", @@ -674,7 +698,7 @@ async fn wait_for_message( )))? } -/// Sends a [`ResponseMessage::SupportFlags`] down the peer sink. +/// Sends a [`AdminResponseMessage::SupportFlags`] down the peer sink. async fn send_support_flags( peer_sink: &mut Z::Sink, support_flags: PeerSupportFlags, @@ -682,7 +706,7 @@ async fn send_support_flags( tracing::debug!("Sending support flag response."); Ok(peer_sink .send( - Message::Response(ResponseMessage::SupportFlags(SupportFlagsResponse { + Message::Response(AdminResponseMessage::SupportFlags(SupportFlagsResponse { support_flags, })) .into(), @@ -690,7 +714,7 @@ async fn send_support_flags( .await?) } -/// Sends a [`ResponseMessage::Ping`] down the peer sink. +/// Sends a [`AdminResponseMessage::Ping`] down the peer sink. async fn send_ping_response( peer_sink: &mut Z::Sink, peer_id: u64, @@ -698,7 +722,7 @@ async fn send_ping_response( tracing::debug!("Sending ping response."); Ok(peer_sink .send( - Message::Response(ResponseMessage::Ping(PingResponse { + Message::Response(AdminResponseMessage::Ping(PingResponse { status: PING_OK_RESPONSE_STATUS_TEXT, peer_id, })) diff --git a/p2p/p2p-core/src/client/handshaker/builder.rs b/p2p/p2p-core/src/client/handshaker/builder.rs new file mode 100644 index 00000000..a40f3962 --- /dev/null +++ b/p2p/p2p-core/src/client/handshaker/builder.rs @@ -0,0 +1,292 @@ +use std::marker::PhantomData; + +use futures::{stream, Stream}; +use tracing::Span; + +use cuprate_wire::BasicNodeData; + +use crate::{ + client::{handshaker::HandShaker, InternalPeerID}, + AddressBook, BroadcastMessage, CoreSyncSvc, NetworkZone, PeerSyncSvc, ProtocolRequestHandler, +}; + +mod dummy; +pub use dummy::{ + DummyAddressBook, DummyCoreSyncSvc, DummyPeerSyncSvc, DummyProtocolRequestHandler, +}; + +/// A [`HandShaker`] [`Service`](tower::Service) builder. +/// +/// This builder applies default values to make usage easier, behaviour and drawbacks of the defaults are documented +/// on the `with_*` method to change it, for example [`HandshakerBuilder::with_protocol_request_handler`]. +/// +/// If you want to use any network other than [`Mainnet`](crate::Network::Mainnet) +/// you will need to change the core sync service with [`HandshakerBuilder::with_core_sync_svc`], +/// see that method for details. +#[derive(Debug, Clone)] +pub struct HandshakerBuilder< + N: NetworkZone, + AdrBook = DummyAddressBook, + CSync = DummyCoreSyncSvc, + PSync = DummyPeerSyncSvc, + ProtoHdlr = DummyProtocolRequestHandler, + BrdcstStrmMkr = fn( + InternalPeerID<::Addr>, + ) -> stream::Pending, +> { + /// The address book service. + address_book: AdrBook, + /// The core sync data service. + core_sync_svc: CSync, + /// The peer sync service. + peer_sync_svc: PSync, + /// The protocol request service. + protocol_request_svc: ProtoHdlr, + /// Our [`BasicNodeData`] + our_basic_node_data: BasicNodeData, + /// A function that returns a stream that will give items to be broadcast by a connection. + broadcast_stream_maker: BrdcstStrmMkr, + /// The [`Span`] that will set as the parent to the connection [`Span`]. + connection_parent_span: Option, + + /// The network zone. + _zone: PhantomData, +} + +impl HandshakerBuilder { + /// Creates a new builder with our node's basic node data. + pub fn new(our_basic_node_data: BasicNodeData) -> Self { + Self { + address_book: DummyAddressBook, + core_sync_svc: DummyCoreSyncSvc::static_mainnet_genesis(), + peer_sync_svc: DummyPeerSyncSvc, + protocol_request_svc: DummyProtocolRequestHandler, + our_basic_node_data, + broadcast_stream_maker: |_| stream::pending(), + connection_parent_span: None, + _zone: PhantomData, + } + } +} + +impl + HandshakerBuilder +{ + /// Changes the address book to the provided one. + /// + /// ## Default Address Book + /// + /// The default address book is used if this function is not called. + /// + /// The default address book's only drawback is that it does not keep track of peers and therefore + /// bans. + pub fn with_address_book( + self, + new_address_book: NAdrBook, + ) -> HandshakerBuilder + where + NAdrBook: AddressBook + Clone, + { + let HandshakerBuilder { + core_sync_svc, + peer_sync_svc, + protocol_request_svc, + our_basic_node_data, + broadcast_stream_maker, + connection_parent_span, + _zone, + .. + } = self; + + HandshakerBuilder { + address_book: new_address_book, + core_sync_svc, + peer_sync_svc, + protocol_request_svc, + our_basic_node_data, + broadcast_stream_maker, + connection_parent_span, + _zone, + } + } + + /// Changes the core sync service to the provided one. + /// + /// The core sync service should keep track of our nodes core sync data. + /// + /// ## Default Core Sync Service + /// + /// The default core sync service is used if this method is not called. + /// + /// The default core sync service will just use the mainnet genesis block, to use other network's + /// genesis see [`DummyCoreSyncSvc::static_stagenet_genesis`] and [`DummyCoreSyncSvc::static_testnet_genesis`]. + /// The drawbacks to keeping this the default is that it will always return the mainnet genesis as our nodes + /// sync info, which means peers won't know our actual chain height, this may or may not be a problem for + /// different use cases. + pub fn with_core_sync_svc( + self, + new_core_sync_svc: NCSync, + ) -> HandshakerBuilder + where + NCSync: CoreSyncSvc + Clone, + { + let HandshakerBuilder { + address_book, + peer_sync_svc, + protocol_request_svc, + our_basic_node_data, + broadcast_stream_maker, + connection_parent_span, + _zone, + .. + } = self; + + HandshakerBuilder { + address_book, + core_sync_svc: new_core_sync_svc, + peer_sync_svc, + protocol_request_svc, + our_basic_node_data, + broadcast_stream_maker, + connection_parent_span, + _zone, + } + } + + /// Changes the peer sync service, which keeps track of peers sync states. + /// + /// ## Default Peer Sync Service + /// + /// The default peer sync service will be used if this method is not called. + /// + /// The default peer sync service will not keep track of peers sync states. + pub fn with_peer_sync_svc( + self, + new_peer_sync_svc: NPSync, + ) -> HandshakerBuilder + where + NPSync: PeerSyncSvc + Clone, + { + let HandshakerBuilder { + address_book, + core_sync_svc, + protocol_request_svc, + our_basic_node_data, + broadcast_stream_maker, + connection_parent_span, + _zone, + .. + } = self; + + HandshakerBuilder { + address_book, + core_sync_svc, + peer_sync_svc: new_peer_sync_svc, + protocol_request_svc, + our_basic_node_data, + broadcast_stream_maker, + connection_parent_span, + _zone, + } + } + + /// Changes the protocol request handler, which handles [`ProtocolRequest`](crate::ProtocolRequest)s to our node. + /// + /// ## Default Protocol Request Handler + /// + /// The default protocol request handler will not respond to any protocol requests, this should not + /// be an issue as long as peers do not think we are ahead of them, if they do they will send requests + /// for our blocks, and we won't respond which will cause them to disconnect. + pub fn with_protocol_request_handler( + self, + new_protocol_handler: NProtoHdlr, + ) -> HandshakerBuilder + where + NProtoHdlr: ProtocolRequestHandler + Clone, + { + let HandshakerBuilder { + address_book, + core_sync_svc, + peer_sync_svc, + our_basic_node_data, + broadcast_stream_maker, + connection_parent_span, + _zone, + .. + } = self; + + HandshakerBuilder { + address_book, + core_sync_svc, + peer_sync_svc, + protocol_request_svc: new_protocol_handler, + our_basic_node_data, + broadcast_stream_maker, + connection_parent_span, + _zone, + } + } + + /// Changes the broadcast stream maker, which is used to create streams that yield messages to broadcast. + /// + /// ## Default Broadcast Stream Maker + /// + /// The default broadcast stream maker just returns [`stream::Pending`], i.e. the returned stream will not + /// produce any messages to broadcast, this is not a problem if your use case does not require broadcasting + /// messages. + pub fn with_broadcast_stream_maker( + self, + new_broadcast_stream_maker: NBrdcstStrmMkr, + ) -> HandshakerBuilder + where + BrdcstStrm: Stream + Send + 'static, + NBrdcstStrmMkr: Fn(InternalPeerID) -> BrdcstStrm + Clone + Send + 'static, + { + let HandshakerBuilder { + address_book, + core_sync_svc, + peer_sync_svc, + protocol_request_svc, + our_basic_node_data, + connection_parent_span, + _zone, + .. + } = self; + + HandshakerBuilder { + address_book, + core_sync_svc, + peer_sync_svc, + protocol_request_svc, + our_basic_node_data, + broadcast_stream_maker: new_broadcast_stream_maker, + connection_parent_span, + _zone, + } + } + + /// Changes the parent [`Span`] of the connection task to the one provided. + /// + /// ## Default Connection Parent Span + /// + /// The default connection span will be [`Span::none`]. + pub fn with_connection_parent_span(self, connection_parent_span: Span) -> Self { + Self { + connection_parent_span: Some(connection_parent_span), + ..self + } + } + + /// Builds the [`HandShaker`]. + pub fn build(self) -> HandShaker { + HandShaker::new( + self.address_book, + self.peer_sync_svc, + self.core_sync_svc, + self.protocol_request_svc, + self.broadcast_stream_maker, + self.our_basic_node_data, + self.connection_parent_span.unwrap_or(Span::none()), + ) + } +} diff --git a/p2p/p2p-core/src/client/handshaker/builder/dummy.rs b/p2p/p2p-core/src/client/handshaker/builder/dummy.rs new file mode 100644 index 00000000..ae97cdce --- /dev/null +++ b/p2p/p2p-core/src/client/handshaker/builder/dummy.rs @@ -0,0 +1,151 @@ +use std::{ + future::{ready, Ready}, + task::{Context, Poll}, +}; + +use tower::Service; + +use cuprate_wire::CoreSyncData; + +use crate::{ + services::{ + AddressBookRequest, AddressBookResponse, CoreSyncDataRequest, CoreSyncDataResponse, + PeerSyncRequest, PeerSyncResponse, + }, + NetworkZone, ProtocolRequest, ProtocolResponse, +}; + +/// A dummy peer sync service, that doesn't actually keep track of peers sync states. +#[derive(Debug, Clone)] +pub struct DummyPeerSyncSvc; + +impl Service> for DummyPeerSyncSvc { + type Response = PeerSyncResponse; + type Error = tower::BoxError; + type Future = Ready>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: PeerSyncRequest) -> Self::Future { + ready(Ok(match req { + PeerSyncRequest::PeersToSyncFrom { .. } => PeerSyncResponse::PeersToSyncFrom(vec![]), + PeerSyncRequest::IncomingCoreSyncData(_, _, _) => PeerSyncResponse::Ok, + })) + } +} + +/// A dummy core sync service that just returns static [`CoreSyncData`]. +#[derive(Debug, Clone)] +pub struct DummyCoreSyncSvc(CoreSyncData); + +impl DummyCoreSyncSvc { + /// Returns a [`DummyCoreSyncSvc`] that will just return the mainnet genesis [`CoreSyncData`]. + pub fn static_mainnet_genesis() -> DummyCoreSyncSvc { + DummyCoreSyncSvc(CoreSyncData { + cumulative_difficulty: 1, + cumulative_difficulty_top64: 0, + current_height: 1, + pruning_seed: 0, + top_id: hex_literal::hex!( + "418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3" + ), + top_version: 1, + }) + } + + /// Returns a [`DummyCoreSyncSvc`] that will just return the testnet genesis [`CoreSyncData`]. + pub fn static_testnet_genesis() -> DummyCoreSyncSvc { + DummyCoreSyncSvc(CoreSyncData { + cumulative_difficulty: 1, + cumulative_difficulty_top64: 0, + current_height: 1, + pruning_seed: 0, + top_id: hex_literal::hex!( + "48ca7cd3c8de5b6a4d53d2861fbdaedca141553559f9be9520068053cda8430b" + ), + top_version: 1, + }) + } + + /// Returns a [`DummyCoreSyncSvc`] that will just return the stagenet genesis [`CoreSyncData`]. + pub fn static_stagenet_genesis() -> DummyCoreSyncSvc { + DummyCoreSyncSvc(CoreSyncData { + cumulative_difficulty: 1, + cumulative_difficulty_top64: 0, + current_height: 1, + pruning_seed: 0, + top_id: hex_literal::hex!( + "76ee3cc98646292206cd3e86f74d88b4dcc1d937088645e9b0cbca84b7ce74eb" + ), + top_version: 1, + }) + } + + /// Returns a [`DummyCoreSyncSvc`] that will return the provided [`CoreSyncData`]. + pub fn static_custom(data: CoreSyncData) -> DummyCoreSyncSvc { + DummyCoreSyncSvc(data) + } +} + +impl Service for DummyCoreSyncSvc { + type Response = CoreSyncDataResponse; + type Error = tower::BoxError; + type Future = Ready>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _: CoreSyncDataRequest) -> Self::Future { + ready(Ok(CoreSyncDataResponse(self.0.clone()))) + } +} + +/// A dummy address book that doesn't actually keep track of peers. +#[derive(Debug, Clone)] +pub struct DummyAddressBook; + +impl Service> for DummyAddressBook { + type Response = AddressBookResponse; + type Error = tower::BoxError; + type Future = Ready>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: AddressBookRequest) -> Self::Future { + ready(Ok(match req { + AddressBookRequest::GetWhitePeers(_) => AddressBookResponse::Peers(vec![]), + AddressBookRequest::TakeRandomGrayPeer { .. } + | AddressBookRequest::TakeRandomPeer { .. } + | AddressBookRequest::TakeRandomWhitePeer { .. } => { + return ready(Err("dummy address book does not hold peers".into())); + } + AddressBookRequest::NewConnection { .. } | AddressBookRequest::IncomingPeerList(_) => { + AddressBookResponse::Ok + } + AddressBookRequest::IsPeerBanned(_) => AddressBookResponse::IsPeerBanned(false), + })) + } +} + +/// A dummy protocol request handler. +#[derive(Debug, Clone)] +pub struct DummyProtocolRequestHandler; + +impl Service for DummyProtocolRequestHandler { + type Response = ProtocolResponse; + type Error = tower::BoxError; + type Future = Ready>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _: ProtocolRequest) -> Self::Future { + ready(Ok(ProtocolResponse::NA)) + } +} diff --git a/p2p/p2p-core/src/client/request_handler.rs b/p2p/p2p-core/src/client/request_handler.rs new file mode 100644 index 00000000..284f9545 --- /dev/null +++ b/p2p/p2p-core/src/client/request_handler.rs @@ -0,0 +1,144 @@ +use futures::TryFutureExt; +use tower::ServiceExt; + +use cuprate_wire::{ + admin::{ + PingResponse, SupportFlagsResponse, TimedSyncRequest, TimedSyncResponse, + PING_OK_RESPONSE_STATUS_TEXT, + }, + AdminRequestMessage, AdminResponseMessage, BasicNodeData, +}; + +use crate::{ + client::PeerInformation, + constants::MAX_PEERS_IN_PEER_LIST_MESSAGE, + services::{ + AddressBookRequest, AddressBookResponse, CoreSyncDataRequest, CoreSyncDataResponse, + PeerSyncRequest, + }, + AddressBook, CoreSyncSvc, NetworkZone, PeerRequest, PeerResponse, PeerSyncSvc, + ProtocolRequestHandler, +}; + +#[derive(thiserror::Error, Debug, Copy, Clone, Eq, PartialEq)] +enum PeerRequestHandlerError { + #[error("Received a handshake request during a connection.")] + ReceivedHandshakeDuringConnection, +} + +/// The peer request handler, handles incoming [`PeerRequest`]s to our node. +#[derive(Debug, Clone)] +pub(crate) struct PeerRequestHandler { + /// The address book service. + pub address_book_svc: A, + /// Our core sync service. + pub our_sync_svc: CS, + /// The peer sync service. + pub peer_sync_svc: PS, + + /// The handler for [`ProtocolRequest`](crate::ProtocolRequest)s to our node. + pub protocol_request_handler: PR, + + /// The basic node data of our node. + pub our_basic_node_data: BasicNodeData, + + /// The information on the connected peer. + pub peer_info: PeerInformation, +} + +impl PeerRequestHandler +where + Z: NetworkZone, + A: AddressBook, + CS: CoreSyncSvc, + PS: PeerSyncSvc, + PR: ProtocolRequestHandler, +{ + /// Handles an incoming [`PeerRequest`] to our node. + pub async fn handle_peer_request( + &mut self, + req: PeerRequest, + ) -> Result { + match req { + PeerRequest::Admin(admin_req) => match admin_req { + AdminRequestMessage::Handshake(_) => { + Err(PeerRequestHandlerError::ReceivedHandshakeDuringConnection.into()) + } + AdminRequestMessage::SupportFlags => { + let support_flags = self.our_basic_node_data.support_flags; + + Ok(PeerResponse::Admin(AdminResponseMessage::SupportFlags( + SupportFlagsResponse { support_flags }, + ))) + } + AdminRequestMessage::Ping => Ok(PeerResponse::Admin(AdminResponseMessage::Ping( + PingResponse { + peer_id: self.our_basic_node_data.peer_id, + status: PING_OK_RESPONSE_STATUS_TEXT, + }, + ))), + AdminRequestMessage::TimedSync(timed_sync_req) => { + let res = self.handle_timed_sync_request(timed_sync_req).await?; + + Ok(PeerResponse::Admin(AdminResponseMessage::TimedSync(res))) + } + }, + + PeerRequest::Protocol(protocol_req) => { + // TODO: add limits here + + self.protocol_request_handler + .ready() + .await? + .call(protocol_req) + .map_ok(PeerResponse::Protocol) + .await + } + } + } + + /// Handles a [`TimedSyncRequest`] to our node. + async fn handle_timed_sync_request( + &mut self, + req: TimedSyncRequest, + ) -> Result { + // TODO: add a limit on the amount of these requests in a certain time period. + + let peer_id = self.peer_info.id; + let handle = self.peer_info.handle.clone(); + + self.peer_sync_svc + .ready() + .await? + .call(PeerSyncRequest::IncomingCoreSyncData( + peer_id, + handle, + req.payload_data, + )) + .await?; + + let AddressBookResponse::Peers(peers) = self + .address_book_svc + .ready() + .await? + .call(AddressBookRequest::GetWhitePeers( + MAX_PEERS_IN_PEER_LIST_MESSAGE, + )) + .await? + else { + panic!("Address book sent incorrect response!"); + }; + + let CoreSyncDataResponse(core_sync_data) = self + .our_sync_svc + .ready() + .await? + .call(CoreSyncDataRequest) + .await?; + + Ok(TimedSyncResponse { + payload_data: core_sync_data, + local_peerlist_new: peers.into_iter().map(Into::into).collect(), + }) + } +} diff --git a/p2p/p2p-core/src/client/timeout_monitor.rs b/p2p/p2p-core/src/client/timeout_monitor.rs index db261b4d..5228edea 100644 --- a/p2p/p2p-core/src/client/timeout_monitor.rs +++ b/p2p/p2p-core/src/client/timeout_monitor.rs @@ -12,7 +12,7 @@ use tokio::{ use tower::ServiceExt; use tracing::instrument; -use cuprate_wire::admin::TimedSyncRequest; +use cuprate_wire::{admin::TimedSyncRequest, AdminRequestMessage, AdminResponseMessage}; use crate::{ client::{connection::ConnectionTaskRequest, InternalPeerID}, @@ -87,15 +87,15 @@ where tracing::debug!(parent: &ping_span, "Sending timed sync to peer"); connection_tx .send(ConnectionTaskRequest { - request: PeerRequest::TimedSync(TimedSyncRequest { + request: PeerRequest::Admin(AdminRequestMessage::TimedSync(TimedSyncRequest { payload_data: core_sync_data, - }), + })), response_channel: tx, permit: Some(permit), }) .await?; - let PeerResponse::TimedSync(timed_sync) = rx.await?? else { + let PeerResponse::Admin(AdminResponseMessage::TimedSync(timed_sync)) = rx.await?? else { panic!("Connection task returned wrong response!"); }; diff --git a/p2p/p2p-core/src/handles.rs b/p2p/p2p-core/src/handles.rs index f3831708..da47b651 100644 --- a/p2p/p2p-core/src/handles.rs +++ b/p2p/p2p-core/src/handles.rs @@ -23,10 +23,8 @@ impl HandleBuilder { } /// Sets the permit for this connection. - /// - /// This must be called at least once. - pub fn with_permit(mut self, permit: OwnedSemaphorePermit) -> Self { - self.permit = Some(permit); + pub fn with_permit(mut self, permit: Option) -> Self { + self.permit = permit; self } @@ -39,7 +37,7 @@ impl HandleBuilder { ( ConnectionGuard { token: token.clone(), - _permit: self.permit.expect("connection permit was not set!"), + _permit: self.permit, }, ConnectionHandle { token: token.clone(), @@ -56,7 +54,7 @@ pub struct BanPeer(pub Duration); /// A struct given to the connection task. pub struct ConnectionGuard { token: CancellationToken, - _permit: OwnedSemaphorePermit, + _permit: Option, } impl ConnectionGuard { diff --git a/p2p/p2p-core/src/lib.rs b/p2p/p2p-core/src/lib.rs index 8703d59e..83cc4d2e 100644 --- a/p2p/p2p-core/src/lib.rs +++ b/p2p/p2p-core/src/lib.rs @@ -1,4 +1,4 @@ -//! # Monero P2P +//! # Cuprate P2P Core //! //! This crate is general purpose P2P networking library for working with Monero. This is a low level //! crate, which means it may seem verbose for a lot of use cases, if you want a crate that handles @@ -6,13 +6,57 @@ //! //! # Network Zones //! -//! This crate abstracts over network zones, Tor/I2p/clearnet with the [NetworkZone] trait. Currently only clearnet is implemented: [ClearNet](network_zones::ClearNet). +//! This crate abstracts over network zones, Tor/I2p/clearnet with the [NetworkZone] trait. Currently only clearnet is implemented: [ClearNet]. //! //! # Usage //! -//! TODO +//! ## Connecting to a peer //! -use std::{fmt::Debug, future::Future, hash::Hash, pin::Pin}; +//! ```rust +//! # use std::{net::SocketAddr, str::FromStr}; +//! # +//! # use tower::ServiceExt; +//! # +//! # use cuprate_p2p_core::{ +//! # client::{ConnectRequest, Connector, HandshakerBuilder}, +//! # ClearNet, Network, +//! # }; +//! # use cuprate_wire::{common::PeerSupportFlags, BasicNodeData}; +//! # use cuprate_test_utils::monerod::monerod; +//! # +//! # tokio_test::block_on(async move { +//! # +//! # let _monerod = monerod::<&str>([]).await; +//! # let addr = _monerod.p2p_addr(); +//! # +//! // The information about our local node. +//! let our_basic_node_data = BasicNodeData { +//! my_port: 0, +//! network_id: Network::Mainnet.network_id(), +//! peer_id: 0, +//! support_flags: PeerSupportFlags::FLUFFY_BLOCKS, +//! rpc_port: 0, +//! rpc_credits_per_hash: 0, +//! }; +//! +//! // See [`HandshakerBuilder`] for information about the default values set, they may not be +//! // appropriate for every use case. +//! let handshaker = HandshakerBuilder::::new(our_basic_node_data).build(); +//! +//! // The outbound connector. +//! let mut connector = Connector::new(handshaker); +//! +//! // The connection. +//! let connection = connector +//! .oneshot(ConnectRequest { +//! addr, +//! permit: None, +//! }) +//! .await +//! .unwrap(); +//! # }); +//! ``` +use std::{fmt::Debug, future::Future, hash::Hash}; use futures::{Sink, Stream}; @@ -25,21 +69,27 @@ pub mod client; mod constants; pub mod error; pub mod handles; -pub mod network_zones; +mod network_zones; pub mod protocol; pub mod services; pub use error::*; +pub use network_zones::{ClearNet, ClearNetServerCfg}; pub use protocol::*; use services::*; +//re-export +pub use cuprate_helper::network::Network; +/// The direction of a connection. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ConnectionDirection { - InBound, - OutBound, + /// An inbound connection to our node. + Inbound, + /// An outbound connection from our node. + Outbound, } -#[cfg(not(feature = "borsh"))] +/// An address on a specific [`NetworkZone`]. pub trait NetZoneAddress: TryFrom + Into @@ -56,46 +106,19 @@ pub trait NetZoneAddress: /// that include the port, to be able to facilitate this network addresses must have a ban ID /// which for hidden services could just be the address it self but for clear net addresses will /// be the IP address. - /// TODO: IP zone banning? - type BanID: Debug + Hash + Eq + Clone + Copy + Send + 'static; - - /// Changes the port of this address to `port`. - fn set_port(&mut self, port: u16); - - fn make_canonical(&mut self); - - fn ban_id(&self) -> Self::BanID; - - fn should_add_to_peer_list(&self) -> bool; -} - -#[cfg(feature = "borsh")] -pub trait NetZoneAddress: - TryFrom - + Into - + std::fmt::Display - + borsh::BorshSerialize - + borsh::BorshDeserialize - + Hash - + Eq - + Copy - + Send - + Sync - + Unpin - + 'static -{ - /// Cuprate needs to be able to ban peers by IP addresses and not just by SocketAddr as - /// that include the port, to be able to facilitate this network addresses must have a ban ID - /// which for hidden services could just be the address it self but for clear net addresses will - /// be the IP address. - /// TODO: IP zone banning? + /// + /// - TODO: IP zone banning? + /// - TODO: rename this to Host. + type BanID: Debug + Hash + Eq + Clone + Copy + Send + 'static; /// Changes the port of this address to `port`. fn set_port(&mut self, port: u16); + /// Turns this address into its canonical form. fn make_canonical(&mut self); + /// Returns the [`Self::BanID`] for this address. fn ban_id(&self) -> Self::BanID; fn should_add_to_peer_list(&self) -> bool; @@ -136,6 +159,15 @@ pub trait NetworkZone: Clone + Copy + Send + 'static { /// Config used to start a server which listens for incoming connections. type ServerCfg: Clone + Debug + Send + 'static; + /// Connects to a peer with the given address. + /// + ///
+ /// + /// This does not complete a handshake with the peer, to do that see the [crate](crate) docs. + /// + ///
+ /// + /// Returns the [`Self::Stream`] and [`Self::Sink`] to send messages to the peer. async fn connect_to_peer( addr: Self::Addr, ) -> Result<(Self::Stream, Self::Sink), std::io::Error>; @@ -206,55 +238,48 @@ pub trait CoreSyncSvc: CoreSyncDataRequest, Response = CoreSyncDataResponse, Error = tower::BoxError, - Future = Pin< - Box< - dyn Future> + Send + 'static, - >, - >, + Future = Self::Future2, > + Send + 'static { + // This allows us to put more restrictive bounds on the future without defining the future here + // explicitly. + type Future2: Future> + Send + 'static; } -impl CoreSyncSvc for T where +impl CoreSyncSvc for T +where T: tower::Service< CoreSyncDataRequest, Response = CoreSyncDataResponse, Error = tower::BoxError, - Future = Pin< - Box< - dyn Future> - + Send - + 'static, - >, - >, > + Send - + 'static + + 'static, + T::Future: Future> + Send + 'static, { + type Future2 = T::Future; } -pub trait PeerRequestHandler: +pub trait ProtocolRequestHandler: tower::Service< - PeerRequest, - Response = PeerResponse, + ProtocolRequest, + Response = ProtocolResponse, Error = tower::BoxError, - Future = Pin< - Box> + Send + 'static>, - >, + Future = Self::Future2, > + Send + 'static { + // This allows us to put more restrictive bounds on the future without defining the future here + // explicitly. + type Future2: Future> + Send + 'static; } -impl PeerRequestHandler for T where - T: tower::Service< - PeerRequest, - Response = PeerResponse, - Error = tower::BoxError, - Future = Pin< - Box> + Send + 'static>, - >, - > + Send - + 'static +impl ProtocolRequestHandler for T +where + T: tower::Service + + Send + + 'static, + T::Future: Future> + Send + 'static, { + type Future2 = T::Future; } diff --git a/p2p/p2p-core/src/protocol.rs b/p2p/p2p-core/src/protocol.rs index 172038f8..5e4f4d7e 100644 --- a/p2p/p2p-core/src/protocol.rs +++ b/p2p/p2p-core/src/protocol.rs @@ -1,13 +1,16 @@ -//! This module defines InternalRequests and InternalResponses. Cuprate's P2P works by translating network messages into an internal -//! request/ response, this is easy for levin "requests" and "responses" (admin messages) but takes a bit more work with "notifications" +//! This module defines [`PeerRequest`] and [`PeerResponse`]. Cuprate's P2P crates works by translating network messages into an internal +//! request/response enums, this is easy for levin "requests" and "responses" (admin messages) but takes a bit more work with "notifications" //! (protocol messages). //! -//! Some notifications are easy to translate, like `GetObjectsRequest` is obviously a request but others like `NewFluffyBlock` are a -//! bit tri cker. To translate a `NewFluffyBlock` into a request/ response we will have to look to see if we asked for `FluffyMissingTransactionsRequest` -//! if we have we interpret `NewFluffyBlock` as a response if not its a request that doesn't require a response. +//! Some notifications are easy to translate, like [`GetObjectsRequest`] is obviously a request but others like [`NewFluffyBlock`] are a +//! bit tricker. To translate a [`NewFluffyBlock`] into a request/ response we will have to look to see if we asked for [`FluffyMissingTransactionsRequest`], +//! if we have, we interpret [`NewFluffyBlock`] as a response, if not, it's a request that doesn't require a response. //! -//! Here is every P2P request/ response. *note admin messages are already request/ response so "Handshake" is actually made of a HandshakeRequest & HandshakeResponse +//! Here is every P2P request/response. //! +//! *note admin messages are already request/response so "Handshake" is actually made of a HandshakeRequest & HandshakeResponse +//! +//! ```md //! Admin: //! Handshake, //! TimedSync, @@ -21,16 +24,14 @@ //! Request: NewBlock, Response: None, //! Request: NewFluffyBlock, Response: None, //! Request: NewTransactions, Response: None +//!``` //! use cuprate_wire::{ - admin::{ - HandshakeRequest, HandshakeResponse, PingResponse, SupportFlagsResponse, TimedSyncRequest, - TimedSyncResponse, - }, protocol::{ ChainRequest, ChainResponse, FluffyMissingTransactionsRequest, GetObjectsRequest, GetObjectsResponse, GetTxPoolCompliment, NewBlock, NewFluffyBlock, NewTransactions, }, + AdminRequestMessage, AdminResponseMessage, }; mod try_from; @@ -60,12 +61,7 @@ pub enum BroadcastMessage { } #[derive(Debug, Clone)] -pub enum PeerRequest { - Handshake(HandshakeRequest), - TimedSync(TimedSyncRequest), - Ping, - SupportFlags, - +pub enum ProtocolRequest { GetObjects(GetObjectsRequest), GetChain(ChainRequest), FluffyMissingTxs(FluffyMissingTransactionsRequest), @@ -75,41 +71,47 @@ pub enum PeerRequest { NewTransactions(NewTransactions), } +#[derive(Debug, Clone)] +pub enum PeerRequest { + Admin(AdminRequestMessage), + Protocol(ProtocolRequest), +} + impl PeerRequest { pub fn id(&self) -> MessageID { match self { - PeerRequest::Handshake(_) => MessageID::Handshake, - PeerRequest::TimedSync(_) => MessageID::TimedSync, - PeerRequest::Ping => MessageID::Ping, - PeerRequest::SupportFlags => MessageID::SupportFlags, - - PeerRequest::GetObjects(_) => MessageID::GetObjects, - PeerRequest::GetChain(_) => MessageID::GetChain, - PeerRequest::FluffyMissingTxs(_) => MessageID::FluffyMissingTxs, - PeerRequest::GetTxPoolCompliment(_) => MessageID::GetTxPoolCompliment, - PeerRequest::NewBlock(_) => MessageID::NewBlock, - PeerRequest::NewFluffyBlock(_) => MessageID::NewFluffyBlock, - PeerRequest::NewTransactions(_) => MessageID::NewTransactions, + PeerRequest::Admin(admin_req) => match admin_req { + AdminRequestMessage::Handshake(_) => MessageID::Handshake, + AdminRequestMessage::TimedSync(_) => MessageID::TimedSync, + AdminRequestMessage::Ping => MessageID::Ping, + AdminRequestMessage::SupportFlags => MessageID::SupportFlags, + }, + PeerRequest::Protocol(protocol_request) => match protocol_request { + ProtocolRequest::GetObjects(_) => MessageID::GetObjects, + ProtocolRequest::GetChain(_) => MessageID::GetChain, + ProtocolRequest::FluffyMissingTxs(_) => MessageID::FluffyMissingTxs, + ProtocolRequest::GetTxPoolCompliment(_) => MessageID::GetTxPoolCompliment, + ProtocolRequest::NewBlock(_) => MessageID::NewBlock, + ProtocolRequest::NewFluffyBlock(_) => MessageID::NewFluffyBlock, + ProtocolRequest::NewTransactions(_) => MessageID::NewTransactions, + }, } } pub fn needs_response(&self) -> bool { !matches!( self, - PeerRequest::NewBlock(_) - | PeerRequest::NewFluffyBlock(_) - | PeerRequest::NewTransactions(_) + PeerRequest::Protocol( + ProtocolRequest::NewBlock(_) + | ProtocolRequest::NewFluffyBlock(_) + | ProtocolRequest::NewTransactions(_) + ) ) } } #[derive(Debug, Clone)] -pub enum PeerResponse { - Handshake(HandshakeResponse), - TimedSync(TimedSyncResponse), - Ping(PingResponse), - SupportFlags(SupportFlagsResponse), - +pub enum ProtocolResponse { GetObjects(GetObjectsResponse), GetChain(ChainResponse), NewFluffyBlock(NewFluffyBlock), @@ -117,20 +119,29 @@ pub enum PeerResponse { NA, } +#[derive(Debug, Clone)] +pub enum PeerResponse { + Admin(AdminResponseMessage), + Protocol(ProtocolResponse), +} + impl PeerResponse { - pub fn id(&self) -> MessageID { - match self { - PeerResponse::Handshake(_) => MessageID::Handshake, - PeerResponse::TimedSync(_) => MessageID::TimedSync, - PeerResponse::Ping(_) => MessageID::Ping, - PeerResponse::SupportFlags(_) => MessageID::SupportFlags, + pub fn id(&self) -> Option { + Some(match self { + PeerResponse::Admin(admin_res) => match admin_res { + AdminResponseMessage::Handshake(_) => MessageID::Handshake, + AdminResponseMessage::TimedSync(_) => MessageID::TimedSync, + AdminResponseMessage::Ping(_) => MessageID::Ping, + AdminResponseMessage::SupportFlags(_) => MessageID::SupportFlags, + }, + PeerResponse::Protocol(protocol_res) => match protocol_res { + ProtocolResponse::GetObjects(_) => MessageID::GetObjects, + ProtocolResponse::GetChain(_) => MessageID::GetChain, + ProtocolResponse::NewFluffyBlock(_) => MessageID::NewBlock, + ProtocolResponse::NewTransactions(_) => MessageID::NewFluffyBlock, - PeerResponse::GetObjects(_) => MessageID::GetObjects, - PeerResponse::GetChain(_) => MessageID::GetChain, - PeerResponse::NewFluffyBlock(_) => MessageID::NewBlock, - PeerResponse::NewTransactions(_) => MessageID::NewFluffyBlock, - - PeerResponse::NA => panic!("Can't get message ID for a non existent response"), - } + ProtocolResponse::NA => return None, + }, + }) } } diff --git a/p2p/p2p-core/src/protocol/try_from.rs b/p2p/p2p-core/src/protocol/try_from.rs index 8e3d026a..8a0b67d2 100644 --- a/p2p/p2p-core/src/protocol/try_from.rs +++ b/p2p/p2p-core/src/protocol/try_from.rs @@ -1,150 +1,111 @@ //! This module contains the implementations of [`TryFrom`] and [`From`] to convert between //! [`Message`], [`PeerRequest`] and [`PeerResponse`]. -use cuprate_wire::{Message, ProtocolMessage, RequestMessage, ResponseMessage}; +use cuprate_wire::{Message, ProtocolMessage}; -use super::{PeerRequest, PeerResponse}; +use crate::{PeerRequest, PeerResponse, ProtocolRequest, ProtocolResponse}; #[derive(Debug)] pub struct MessageConversionError; -macro_rules! match_body { - (match $value: ident {$($body:tt)*} ($left:pat => $right_ty:expr) $($todo:tt)*) => { - match_body!( match $value { - $left => $right_ty, - $($body)* - } $($todo)* ) - }; - (match $value: ident {$($body:tt)*}) => { - match $value { - $($body)* - } - }; -} - -macro_rules! from { - ($left_ty:ident, $right_ty:ident, {$($left:ident $(($val: ident))? = $right:ident $(($vall: ident))?,)+}) => { - impl From<$left_ty> for $right_ty { - fn from(value: $left_ty) -> Self { - match_body!( match value {} - $(($left_ty::$left$(($val))? => $right_ty::$right$(($vall))?))+ - ) +impl From for ProtocolMessage { + fn from(value: ProtocolRequest) -> Self { + match value { + ProtocolRequest::GetObjects(val) => ProtocolMessage::GetObjectsRequest(val), + ProtocolRequest::GetChain(val) => ProtocolMessage::ChainRequest(val), + ProtocolRequest::FluffyMissingTxs(val) => { + ProtocolMessage::FluffyMissingTransactionsRequest(val) } + ProtocolRequest::GetTxPoolCompliment(val) => ProtocolMessage::GetTxPoolCompliment(val), + ProtocolRequest::NewBlock(val) => ProtocolMessage::NewBlock(val), + ProtocolRequest::NewFluffyBlock(val) => ProtocolMessage::NewFluffyBlock(val), + ProtocolRequest::NewTransactions(val) => ProtocolMessage::NewTransactions(val), } - }; + } } -macro_rules! try_from { - ($left_ty:ident, $right_ty:ident, {$($left:ident $(($val: ident))? = $right:ident $(($vall: ident))?,)+}) => { - impl TryFrom<$left_ty> for $right_ty { - type Error = MessageConversionError; - - fn try_from(value: $left_ty) -> Result { - Ok(match_body!( match value { - _ => return Err(MessageConversionError) - } - $(($left_ty::$left$(($val))? => $right_ty::$right$(($vall))?))+ - )) - } - } - }; -} - -macro_rules! from_try_from { - ($left_ty:ident, $right_ty:ident, {$($left:ident $(($val: ident))? = $right:ident $(($vall: ident))?,)+}) => { - try_from!($left_ty, $right_ty, {$($left $(($val))? = $right $(($vall))?,)+}); - from!($right_ty, $left_ty, {$($right $(($val))? = $left $(($vall))?,)+}); - }; -} - -macro_rules! try_from_try_from { - ($left_ty:ident, $right_ty:ident, {$($left:ident $(($val: ident))? = $right:ident $(($vall: ident))?,)+}) => { - try_from!($left_ty, $right_ty, {$($left $(($val))? = $right $(($vall))?,)+}); - try_from!($right_ty, $left_ty, {$($right $(($val))? = $left $(($val))?,)+}); - }; -} - -from_try_from!(PeerRequest, RequestMessage,{ - Handshake(val) = Handshake(val), - Ping = Ping, - SupportFlags = SupportFlags, - TimedSync(val) = TimedSync(val), -}); - -try_from_try_from!(PeerRequest, ProtocolMessage,{ - NewBlock(val) = NewBlock(val), - NewFluffyBlock(val) = NewFluffyBlock(val), - GetObjects(val) = GetObjectsRequest(val), - GetChain(val) = ChainRequest(val), - NewTransactions(val) = NewTransactions(val), - FluffyMissingTxs(val) = FluffyMissingTransactionsRequest(val), - GetTxPoolCompliment(val) = GetTxPoolCompliment(val), -}); - -impl TryFrom for PeerRequest { +impl TryFrom for ProtocolRequest { type Error = MessageConversionError; - fn try_from(value: Message) -> Result { - match value { - Message::Request(req) => Ok(req.into()), - Message::Protocol(pro) => pro.try_into(), - _ => Err(MessageConversionError), - } + fn try_from(value: ProtocolMessage) -> Result { + Ok(match value { + ProtocolMessage::GetObjectsRequest(val) => ProtocolRequest::GetObjects(val), + ProtocolMessage::ChainRequest(val) => ProtocolRequest::GetChain(val), + ProtocolMessage::FluffyMissingTransactionsRequest(val) => { + ProtocolRequest::FluffyMissingTxs(val) + } + ProtocolMessage::GetTxPoolCompliment(val) => ProtocolRequest::GetTxPoolCompliment(val), + ProtocolMessage::NewBlock(val) => ProtocolRequest::NewBlock(val), + ProtocolMessage::NewFluffyBlock(val) => ProtocolRequest::NewFluffyBlock(val), + ProtocolMessage::NewTransactions(val) => ProtocolRequest::NewTransactions(val), + ProtocolMessage::GetObjectsResponse(_) | ProtocolMessage::ChainEntryResponse(_) => { + return Err(MessageConversionError) + } + }) } } impl From for Message { fn from(value: PeerRequest) -> Self { match value { - PeerRequest::Handshake(val) => Message::Request(RequestMessage::Handshake(val)), - PeerRequest::Ping => Message::Request(RequestMessage::Ping), - PeerRequest::SupportFlags => Message::Request(RequestMessage::SupportFlags), - PeerRequest::TimedSync(val) => Message::Request(RequestMessage::TimedSync(val)), - - PeerRequest::NewBlock(val) => Message::Protocol(ProtocolMessage::NewBlock(val)), - PeerRequest::NewFluffyBlock(val) => { - Message::Protocol(ProtocolMessage::NewFluffyBlock(val)) - } - PeerRequest::GetObjects(val) => { - Message::Protocol(ProtocolMessage::GetObjectsRequest(val)) - } - PeerRequest::GetChain(val) => Message::Protocol(ProtocolMessage::ChainRequest(val)), - PeerRequest::NewTransactions(val) => { - Message::Protocol(ProtocolMessage::NewTransactions(val)) - } - PeerRequest::FluffyMissingTxs(val) => { - Message::Protocol(ProtocolMessage::FluffyMissingTransactionsRequest(val)) - } - PeerRequest::GetTxPoolCompliment(val) => { - Message::Protocol(ProtocolMessage::GetTxPoolCompliment(val)) - } + PeerRequest::Admin(val) => Message::Request(val), + PeerRequest::Protocol(val) => Message::Protocol(val.into()), } } } -from_try_from!(PeerResponse, ResponseMessage,{ - Handshake(val) = Handshake(val), - Ping(val) = Ping(val), - SupportFlags(val) = SupportFlags(val), - TimedSync(val) = TimedSync(val), -}); +impl TryFrom for PeerRequest { + type Error = MessageConversionError; -try_from_try_from!(PeerResponse, ProtocolMessage,{ - NewFluffyBlock(val) = NewFluffyBlock(val), - GetObjects(val) = GetObjectsResponse(val), - GetChain(val) = ChainEntryResponse(val), - NewTransactions(val) = NewTransactions(val), + fn try_from(value: Message) -> Result { + match value { + Message::Request(req) => Ok(PeerRequest::Admin(req)), + Message::Protocol(pro) => Ok(PeerRequest::Protocol(pro.try_into()?)), + Message::Response(_) => Err(MessageConversionError), + } + } +} -}); +impl TryFrom for ProtocolMessage { + type Error = MessageConversionError; + + fn try_from(value: ProtocolResponse) -> Result { + Ok(match value { + ProtocolResponse::NewTransactions(val) => ProtocolMessage::NewTransactions(val), + ProtocolResponse::NewFluffyBlock(val) => ProtocolMessage::NewFluffyBlock(val), + ProtocolResponse::GetChain(val) => ProtocolMessage::ChainEntryResponse(val), + ProtocolResponse::GetObjects(val) => ProtocolMessage::GetObjectsResponse(val), + ProtocolResponse::NA => return Err(MessageConversionError), + }) + } +} + +impl TryFrom for ProtocolResponse { + type Error = MessageConversionError; + + fn try_from(value: ProtocolMessage) -> Result { + Ok(match value { + ProtocolMessage::NewTransactions(val) => ProtocolResponse::NewTransactions(val), + ProtocolMessage::NewFluffyBlock(val) => ProtocolResponse::NewFluffyBlock(val), + ProtocolMessage::ChainEntryResponse(val) => ProtocolResponse::GetChain(val), + ProtocolMessage::GetObjectsResponse(val) => ProtocolResponse::GetObjects(val), + ProtocolMessage::ChainRequest(_) + | ProtocolMessage::FluffyMissingTransactionsRequest(_) + | ProtocolMessage::GetObjectsRequest(_) + | ProtocolMessage::GetTxPoolCompliment(_) + | ProtocolMessage::NewBlock(_) => return Err(MessageConversionError), + }) + } +} impl TryFrom for PeerResponse { type Error = MessageConversionError; fn try_from(value: Message) -> Result { match value { - Message::Response(res) => Ok(res.into()), - Message::Protocol(pro) => pro.try_into(), - _ => Err(MessageConversionError), + Message::Response(res) => Ok(PeerResponse::Admin(res)), + Message::Protocol(pro) => Ok(PeerResponse::Protocol(pro.try_into()?)), + Message::Request(_) => Err(MessageConversionError), } } } @@ -154,27 +115,8 @@ impl TryFrom for Message { fn try_from(value: PeerResponse) -> Result { Ok(match value { - PeerResponse::Handshake(val) => Message::Response(ResponseMessage::Handshake(val)), - PeerResponse::Ping(val) => Message::Response(ResponseMessage::Ping(val)), - PeerResponse::SupportFlags(val) => { - Message::Response(ResponseMessage::SupportFlags(val)) - } - PeerResponse::TimedSync(val) => Message::Response(ResponseMessage::TimedSync(val)), - - PeerResponse::NewFluffyBlock(val) => { - Message::Protocol(ProtocolMessage::NewFluffyBlock(val)) - } - PeerResponse::GetObjects(val) => { - Message::Protocol(ProtocolMessage::GetObjectsResponse(val)) - } - PeerResponse::GetChain(val) => { - Message::Protocol(ProtocolMessage::ChainEntryResponse(val)) - } - PeerResponse::NewTransactions(val) => { - Message::Protocol(ProtocolMessage::NewTransactions(val)) - } - - PeerResponse::NA => return Err(MessageConversionError), + PeerResponse::Admin(val) => Message::Response(val), + PeerResponse::Protocol(val) => Message::Protocol(val.try_into()?), }) } } diff --git a/p2p/p2p-core/src/services.rs b/p2p/p2p-core/src/services.rs index 07309fb0..6d66cfa1 100644 --- a/p2p/p2p-core/src/services.rs +++ b/p2p/p2p-core/src/services.rs @@ -6,6 +6,7 @@ use crate::{ NetworkZone, }; +/// A request to the service that keeps track of peers sync states. pub enum PeerSyncRequest { /// Request some peers to sync from. /// @@ -15,10 +16,11 @@ pub enum PeerSyncRequest { current_cumulative_difficulty: u128, block_needed: Option, }, - /// Add/update a peers core sync data to the sync state service. + /// Add/update a peer's core sync data. IncomingCoreSyncData(InternalPeerID, ConnectionHandle, CoreSyncData), } +/// A response from the service that keeps track of peers sync states. pub enum PeerSyncResponse { /// The return value of [`PeerSyncRequest::PeersToSyncFrom`]. PeersToSyncFrom(Vec>), @@ -26,10 +28,16 @@ pub enum PeerSyncResponse { Ok, } +/// A request to the core sync service for our node's [`CoreSyncData`]. pub struct CoreSyncDataRequest; +/// A response from the core sync service containing our [`CoreSyncData`]. pub struct CoreSyncDataResponse(pub CoreSyncData); +/// A [`NetworkZone`] specific [`PeerListEntryBase`]. +/// +/// Using this type instead of [`PeerListEntryBase`] in the address book makes +/// usage easier for the rest of the P2P code as we can guarantee only the correct addresses will be stored and returned. #[derive(Debug, Copy, Clone, Eq, PartialEq)] #[cfg_attr( feature = "borsh", @@ -57,6 +65,7 @@ impl From> for cuprate_wire: } } +/// An error converting a [`PeerListEntryBase`] into a [`ZoneSpecificPeerListEntryBase`]. #[derive(Debug, thiserror::Error)] pub enum PeerListConversionError { #[error("Address is in incorrect zone")] @@ -82,6 +91,7 @@ impl TryFrom } } +/// A request to the address book service. pub enum AddressBookRequest { /// Tells the address book that we have connected or received a connection from a peer. NewConnection { @@ -123,6 +133,7 @@ pub enum AddressBookRequest { IsPeerBanned(Z::Addr), } +/// A response from the address book service. pub enum AddressBookResponse { Ok, Peer(ZoneSpecificPeerListEntryBase), diff --git a/p2p/p2p-core/tests/fragmented_handshake.rs b/p2p/p2p-core/tests/fragmented_handshake.rs index 2e96574c..c19a2a63 100644 --- a/p2p/p2p-core/tests/fragmented_handshake.rs +++ b/p2p/p2p-core/tests/fragmented_handshake.rs @@ -2,7 +2,6 @@ use std::{ net::SocketAddr, pin::Pin, - sync::Arc, task::{Context, Poll}, time::Duration, }; @@ -13,7 +12,6 @@ use tokio::{ tcp::{OwnedReadHalf, OwnedWriteHalf}, TcpListener, TcpStream, }, - sync::Semaphore, time::timeout, }; use tokio_util::{ @@ -24,9 +22,11 @@ use tower::{Service, ServiceExt}; use cuprate_helper::network::Network; use cuprate_p2p_core::{ - client::{ConnectRequest, Connector, DoHandshakeRequest, HandShaker, InternalPeerID}, - network_zones::ClearNetServerCfg, - ConnectionDirection, NetworkZone, + client::{ + handshaker::HandshakerBuilder, ConnectRequest, Connector, DoHandshakeRequest, + InternalPeerID, + }, + ClearNetServerCfg, ConnectionDirection, NetworkZone, }; use cuprate_wire::{ common::PeerSupportFlags, @@ -36,9 +36,6 @@ use cuprate_wire::{ use cuprate_test_utils::monerod::monerod; -mod utils; -use utils::*; - /// A network zone equal to clear net where every message sent is turned into a fragmented message. /// Does not support sending fragmented or dummy messages manually. #[derive(Clone, Copy)] @@ -135,9 +132,6 @@ impl Encoder> for FragmentCodec { #[tokio::test] async fn fragmented_handshake_cuprate_to_monerod() { - let semaphore = Arc::new(Semaphore::new(10)); - let permit = semaphore.acquire_owned().await.unwrap(); - let monerod = monerod(["--fixed-difficulty=1", "--out-peers=0"]).await; let our_basic_node_data = BasicNodeData { @@ -149,14 +143,7 @@ async fn fragmented_handshake_cuprate_to_monerod() { rpc_credits_per_hash: 0, }; - let handshaker = HandShaker::::new( - DummyAddressBook, - DummyPeerSyncSvc, - DummyCoreSyncSvc, - DummyPeerRequestHandlerSvc, - |_| futures::stream::pending(), - our_basic_node_data, - ); + let handshaker = HandshakerBuilder::::new(our_basic_node_data).build(); let mut connector = Connector::new(handshaker); @@ -166,7 +153,7 @@ async fn fragmented_handshake_cuprate_to_monerod() { .unwrap() .call(ConnectRequest { addr: monerod.p2p_addr(), - permit, + permit: None, }) .await .unwrap(); @@ -174,9 +161,6 @@ async fn fragmented_handshake_cuprate_to_monerod() { #[tokio::test] async fn fragmented_handshake_monerod_to_cuprate() { - let semaphore = Arc::new(Semaphore::new(10)); - let permit = semaphore.acquire_owned().await.unwrap(); - let our_basic_node_data = BasicNodeData { my_port: 18081, network_id: Network::Mainnet.network_id(), @@ -186,14 +170,7 @@ async fn fragmented_handshake_monerod_to_cuprate() { rpc_credits_per_hash: 0, }; - let mut handshaker = HandShaker::::new( - DummyAddressBook, - DummyPeerSyncSvc, - DummyCoreSyncSvc, - DummyPeerRequestHandlerSvc, - |_| futures::stream::pending(), - our_basic_node_data, - ); + let mut handshaker = HandshakerBuilder::::new(our_basic_node_data).build(); let ip = "127.0.0.1".parse().unwrap(); @@ -215,8 +192,8 @@ async fn fragmented_handshake_monerod_to_cuprate() { addr: InternalPeerID::KnownAddr(addr.unwrap()), // This is clear net all addresses are known. peer_stream: stream, peer_sink: sink, - direction: ConnectionDirection::InBound, - permit, + direction: ConnectionDirection::Inbound, + permit: None, }) .await .unwrap(); diff --git a/p2p/p2p-core/tests/handles.rs b/p2p/p2p-core/tests/handles.rs index e98cd2d4..47d70b05 100644 --- a/p2p/p2p-core/tests/handles.rs +++ b/p2p/p2p-core/tests/handles.rs @@ -6,10 +6,7 @@ use cuprate_p2p_core::handles::HandleBuilder; #[test] fn send_ban_signal() { - let semaphore = Arc::new(Semaphore::new(5)); - let (guard, mut connection_handle) = HandleBuilder::default() - .with_permit(semaphore.try_acquire_owned().unwrap()) - .build(); + let (guard, mut connection_handle) = HandleBuilder::default().build(); connection_handle.ban_peer(Duration::from_secs(300)); @@ -28,10 +25,7 @@ fn send_ban_signal() { #[test] fn multiple_ban_signals() { - let semaphore = Arc::new(Semaphore::new(5)); - let (guard, mut connection_handle) = HandleBuilder::default() - .with_permit(semaphore.try_acquire_owned().unwrap()) - .build(); + let (guard, mut connection_handle) = HandleBuilder::default().build(); connection_handle.ban_peer(Duration::from_secs(300)); connection_handle.ban_peer(Duration::from_secs(301)); @@ -55,7 +49,7 @@ fn multiple_ban_signals() { fn dropped_guard_sends_disconnect_signal() { let semaphore = Arc::new(Semaphore::new(5)); let (guard, connection_handle) = HandleBuilder::default() - .with_permit(semaphore.try_acquire_owned().unwrap()) + .with_permit(Some(semaphore.try_acquire_owned().unwrap())) .build(); assert!(!connection_handle.is_closed()); diff --git a/p2p/p2p-core/tests/handshake.rs b/p2p/p2p-core/tests/handshake.rs index f9792488..5ce6153a 100644 --- a/p2p/p2p-core/tests/handshake.rs +++ b/p2p/p2p-core/tests/handshake.rs @@ -1,9 +1,8 @@ -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use futures::StreamExt; use tokio::{ io::{duplex, split}, - sync::Semaphore, time::timeout, }; use tokio_util::codec::{FramedRead, FramedWrite}; @@ -13,9 +12,11 @@ use cuprate_helper::network::Network; use cuprate_wire::{common::PeerSupportFlags, BasicNodeData, MoneroWireCodec}; use cuprate_p2p_core::{ - client::{ConnectRequest, Connector, DoHandshakeRequest, HandShaker, InternalPeerID}, - network_zones::{ClearNet, ClearNetServerCfg}, - ConnectionDirection, NetworkZone, + client::{ + handshaker::HandshakerBuilder, ConnectRequest, Connector, DoHandshakeRequest, + InternalPeerID, + }, + ClearNet, ClearNetServerCfg, ConnectionDirection, NetworkZone, }; use cuprate_test_utils::{ @@ -23,18 +24,10 @@ use cuprate_test_utils::{ test_netzone::{TestNetZone, TestNetZoneAddr}, }; -mod utils; -use utils::*; - #[tokio::test] async fn handshake_cuprate_to_cuprate() { // Tests a Cuprate <-> Cuprate handshake by making 2 handshake services and making them talk to // each other. - - let semaphore = Arc::new(Semaphore::new(10)); - let permit_1 = semaphore.clone().acquire_owned().await.unwrap(); - let permit_2 = semaphore.acquire_owned().await.unwrap(); - let our_basic_node_data_1 = BasicNodeData { my_port: 0, network_id: Network::Mainnet.network_id(), @@ -48,23 +41,11 @@ async fn handshake_cuprate_to_cuprate() { let mut our_basic_node_data_2 = our_basic_node_data_1.clone(); our_basic_node_data_2.peer_id = 2344; - let mut handshaker_1 = HandShaker::, _, _, _, _, _>::new( - DummyAddressBook, - DummyPeerSyncSvc, - DummyCoreSyncSvc, - DummyPeerRequestHandlerSvc, - |_| futures::stream::pending(), - our_basic_node_data_1, - ); + let mut handshaker_1 = + HandshakerBuilder::>::new(our_basic_node_data_1).build(); - let mut handshaker_2 = HandShaker::, _, _, _, _, _>::new( - DummyAddressBook, - DummyPeerSyncSvc, - DummyCoreSyncSvc, - DummyPeerRequestHandlerSvc, - |_| futures::stream::pending(), - our_basic_node_data_2, - ); + let mut handshaker_2 = + HandshakerBuilder::>::new(our_basic_node_data_2).build(); let (p1, p2) = duplex(50_000); @@ -75,16 +56,16 @@ async fn handshake_cuprate_to_cuprate() { addr: InternalPeerID::KnownAddr(TestNetZoneAddr(888)), peer_stream: FramedRead::new(p2_receiver, MoneroWireCodec::default()), peer_sink: FramedWrite::new(p2_sender, MoneroWireCodec::default()), - direction: ConnectionDirection::OutBound, - permit: permit_1, + direction: ConnectionDirection::Outbound, + permit: None, }; let p2_handshake_req = DoHandshakeRequest { addr: InternalPeerID::KnownAddr(TestNetZoneAddr(444)), peer_stream: FramedRead::new(p1_receiver, MoneroWireCodec::default()), peer_sink: FramedWrite::new(p1_sender, MoneroWireCodec::default()), - direction: ConnectionDirection::InBound, - permit: permit_2, + direction: ConnectionDirection::Inbound, + permit: None, }; let p1 = tokio::spawn(async move { @@ -114,9 +95,6 @@ async fn handshake_cuprate_to_cuprate() { #[tokio::test] async fn handshake_cuprate_to_monerod() { - let semaphore = Arc::new(Semaphore::new(10)); - let permit = semaphore.acquire_owned().await.unwrap(); - let monerod = monerod(["--fixed-difficulty=1", "--out-peers=0"]).await; let our_basic_node_data = BasicNodeData { @@ -128,14 +106,7 @@ async fn handshake_cuprate_to_monerod() { rpc_credits_per_hash: 0, }; - let handshaker = HandShaker::::new( - DummyAddressBook, - DummyPeerSyncSvc, - DummyCoreSyncSvc, - DummyPeerRequestHandlerSvc, - |_| futures::stream::pending(), - our_basic_node_data, - ); + let handshaker = HandshakerBuilder::::new(our_basic_node_data).build(); let mut connector = Connector::new(handshaker); @@ -145,7 +116,7 @@ async fn handshake_cuprate_to_monerod() { .unwrap() .call(ConnectRequest { addr: monerod.p2p_addr(), - permit, + permit: None, }) .await .unwrap(); @@ -153,9 +124,6 @@ async fn handshake_cuprate_to_monerod() { #[tokio::test] async fn handshake_monerod_to_cuprate() { - let semaphore = Arc::new(Semaphore::new(10)); - let permit = semaphore.acquire_owned().await.unwrap(); - let our_basic_node_data = BasicNodeData { my_port: 18081, network_id: Network::Mainnet.network_id(), @@ -165,14 +133,7 @@ async fn handshake_monerod_to_cuprate() { rpc_credits_per_hash: 0, }; - let mut handshaker = HandShaker::::new( - DummyAddressBook, - DummyPeerSyncSvc, - DummyCoreSyncSvc, - DummyPeerRequestHandlerSvc, - |_| futures::stream::pending(), - our_basic_node_data, - ); + let mut handshaker = HandshakerBuilder::::new(our_basic_node_data).build(); let ip = "127.0.0.1".parse().unwrap(); @@ -194,8 +155,8 @@ async fn handshake_monerod_to_cuprate() { addr: InternalPeerID::KnownAddr(addr.unwrap()), // This is clear net all addresses are known. peer_stream: stream, peer_sink: sink, - direction: ConnectionDirection::InBound, - permit, + direction: ConnectionDirection::Inbound, + permit: None, }) .await .unwrap(); diff --git a/p2p/p2p-core/tests/sending_receiving.rs b/p2p/p2p-core/tests/sending_receiving.rs index b4c42e2c..e035daf8 100644 --- a/p2p/p2p-core/tests/sending_receiving.rs +++ b/p2p/p2p-core/tests/sending_receiving.rs @@ -1,27 +1,18 @@ -use std::sync::Arc; - -use tokio::sync::Semaphore; use tower::{Service, ServiceExt}; use cuprate_helper::network::Network; use cuprate_wire::{common::PeerSupportFlags, protocol::GetObjectsRequest, BasicNodeData}; use cuprate_p2p_core::{ - client::{ConnectRequest, Connector, HandShaker}, - network_zones::ClearNet, + client::{handshaker::HandshakerBuilder, ConnectRequest, Connector}, protocol::{PeerRequest, PeerResponse}, + ClearNet, ProtocolRequest, ProtocolResponse, }; use cuprate_test_utils::monerod::monerod; -mod utils; -use utils::*; - #[tokio::test] async fn get_single_block_from_monerod() { - let semaphore = Arc::new(Semaphore::new(10)); - let permit = semaphore.acquire_owned().await.unwrap(); - let monerod = monerod(["--out-peers=0"]).await; let our_basic_node_data = BasicNodeData { @@ -33,14 +24,7 @@ async fn get_single_block_from_monerod() { rpc_credits_per_hash: 0, }; - let handshaker = HandShaker::::new( - DummyAddressBook, - DummyPeerSyncSvc, - DummyCoreSyncSvc, - DummyPeerRequestHandlerSvc, - |_| futures::stream::pending(), - our_basic_node_data, - ); + let handshaker = HandshakerBuilder::::new(our_basic_node_data).build(); let mut connector = Connector::new(handshaker); @@ -50,22 +34,26 @@ async fn get_single_block_from_monerod() { .unwrap() .call(ConnectRequest { addr: monerod.p2p_addr(), - permit, + permit: None, }) .await .unwrap(); - let PeerResponse::GetObjects(obj) = connected_peer + let PeerResponse::Protocol(ProtocolResponse::GetObjects(obj)) = connected_peer .ready() .await .unwrap() - .call(PeerRequest::GetObjects(GetObjectsRequest { - blocks: hex::decode("418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3") + .call(PeerRequest::Protocol(ProtocolRequest::GetObjects( + GetObjectsRequest { + blocks: hex::decode( + "418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3", + ) .unwrap() .try_into() .unwrap(), - pruned: false, - })) + pruned: false, + }, + ))) .await .unwrap() else { diff --git a/p2p/p2p-core/tests/utils.rs b/p2p/p2p-core/tests/utils.rs deleted file mode 100644 index 9587bb58..00000000 --- a/p2p/p2p-core/tests/utils.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::{ - future::Future, - pin::Pin, - task::{Context, Poll}, -}; - -use futures::FutureExt; -use tower::Service; - -use cuprate_p2p_core::{ - services::{ - AddressBookRequest, AddressBookResponse, CoreSyncDataRequest, CoreSyncDataResponse, - PeerSyncRequest, PeerSyncResponse, - }, - NetworkZone, PeerRequest, PeerResponse, -}; - -#[derive(Clone)] -pub struct DummyAddressBook; - -impl Service> for DummyAddressBook { - type Response = AddressBookResponse; - type Error = tower::BoxError; - type Future = - Pin> + Send + 'static>>; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: AddressBookRequest) -> Self::Future { - async move { - Ok(match req { - AddressBookRequest::GetWhitePeers(_) => AddressBookResponse::Peers(vec![]), - _ => AddressBookResponse::Ok, - }) - } - .boxed() - } -} - -#[derive(Clone)] -pub struct DummyCoreSyncSvc; - -impl Service for DummyCoreSyncSvc { - type Response = CoreSyncDataResponse; - type Error = tower::BoxError; - type Future = - Pin> + Send + 'static>>; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _: CoreSyncDataRequest) -> Self::Future { - async move { - Ok(CoreSyncDataResponse(cuprate_wire::CoreSyncData { - cumulative_difficulty: 1, - cumulative_difficulty_top64: 0, - current_height: 1, - pruning_seed: 0, - top_id: hex::decode( - "418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3", - ) - .unwrap() - .try_into() - .unwrap(), - top_version: 1, - })) - } - .boxed() - } -} - -#[derive(Clone)] -pub struct DummyPeerSyncSvc; - -impl Service> for DummyPeerSyncSvc { - type Error = tower::BoxError; - type Future = - Pin> + Send + 'static>>; - - type Response = PeerSyncResponse; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _: PeerSyncRequest) -> Self::Future { - async { Ok(PeerSyncResponse::Ok) }.boxed() - } -} - -#[derive(Clone)] -pub struct DummyPeerRequestHandlerSvc; - -impl Service for DummyPeerRequestHandlerSvc { - type Response = PeerResponse; - type Error = tower::BoxError; - type Future = - Pin> + Send + 'static>>; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _: PeerRequest) -> Self::Future { - async move { Ok(PeerResponse::NA) }.boxed() - } -} diff --git a/p2p/p2p/Cargo.toml b/p2p/p2p/Cargo.toml index 507d3621..7cbbdcb1 100644 --- a/p2p/p2p/Cargo.toml +++ b/p2p/p2p/Cargo.toml @@ -13,6 +13,7 @@ cuprate-address-book = { path = "../address-book" } cuprate-pruning = { path = "../../pruning" } cuprate-helper = { path = "../../helper", features = ["asynch"], default-features = false } cuprate-async-buffer = { path = "../async-buffer" } +cuprate-types = { path = "../../types", default-features = false } monero-serai = { workspace = true, features = ["std"] } @@ -31,6 +32,7 @@ rand = { workspace = true, features = ["std", "std_rng"] } rand_distr = { workspace = true, features = ["std"] } hex = { workspace = true, features = ["std"] } tracing = { workspace = true, features = ["std", "attributes"] } +borsh = { workspace = true, features = ["derive", "std"] } [dev-dependencies] cuprate-test-utils = { path = "../../test-utils" } diff --git a/p2p/p2p/src/block_downloader.rs b/p2p/p2p/src/block_downloader.rs index e80c794d..5f530546 100644 --- a/p2p/p2p/src/block_downloader.rs +++ b/p2p/p2p/src/block_downloader.rs @@ -121,8 +121,7 @@ pub enum ChainSvcResponse { /// The response for [`ChainSvcRequest::FindFirstUnknown`]. /// /// Contains the index of the first unknown block and its expected height. - // TODO: make this a named field variant instead of a tuple - FindFirstUnknown(usize, usize), + FindFirstUnknown(Option<(usize, u64)>), /// The response for [`ChainSvcRequest::CumulativeDifficulty`]. /// /// The current cumulative difficulty of our chain. @@ -195,7 +194,7 @@ where /// - download the next batch of blocks /// - request the next chain entry /// - download an already requested batch of blocks (this might happen due to an error in the previous request -/// or because the queue of ready blocks is too large, so we need the oldest block to clear it). +/// or because the queue of ready blocks is too large, so we need the oldest block to clear it). struct BlockDownloader { /// The client pool. client_pool: Arc>, @@ -208,7 +207,7 @@ struct BlockDownloader { /// The amount of blocks to request in the next batch. amount_of_blocks_to_request: usize, /// The height at which [`Self::amount_of_blocks_to_request`] was updated. - amount_of_blocks_to_request_updated_at: usize, + amount_of_blocks_to_request_updated_at: u64, /// The amount of consecutive empty chain entries we received. /// @@ -226,12 +225,12 @@ struct BlockDownloader { /// The current inflight requests. /// /// This is a map of batch start heights to block IDs and related information of the batch. - inflight_requests: BTreeMap>, + inflight_requests: BTreeMap>, /// A queue of start heights from failed batches that should be retried. /// /// Wrapped in [`Reverse`] so we prioritize early batches. - failed_batches: BinaryHeap>, + failed_batches: BinaryHeap>, block_queue: BlockQueue, @@ -525,7 +524,7 @@ where /// Handles a response to a request to get blocks from a peer. async fn handle_download_batch_res( &mut self, - start_height: usize, + start_height: u64, res: Result<(ClientPoolDropGuard, BlockBatch), BlockDownloadError>, chain_tracker: &mut ChainTracker, pending_peers: &mut BTreeMap>>, @@ -693,19 +692,18 @@ where /// The return value from the block download tasks. struct BlockDownloadTaskResponse { /// The start height of the batch. - start_height: usize, + start_height: u64, /// A result containing the batch or an error. result: Result<(ClientPoolDropGuard, BlockBatch), BlockDownloadError>, } /// Returns if a peer has all the blocks in a range, according to its [`PruningSeed`]. -fn client_has_block_in_range( - pruning_seed: &PruningSeed, - start_height: usize, - length: usize, -) -> bool { +fn client_has_block_in_range(pruning_seed: &PruningSeed, start_height: u64, length: usize) -> bool { pruning_seed.has_full_block(start_height, CRYPTONOTE_MAX_BLOCK_HEIGHT) - && pruning_seed.has_full_block(start_height + length, CRYPTONOTE_MAX_BLOCK_HEIGHT) + && pruning_seed.has_full_block( + start_height + u64::try_from(length).unwrap(), + CRYPTONOTE_MAX_BLOCK_HEIGHT, + ) } /// Calculates the next amount of blocks to request in a batch. diff --git a/p2p/p2p/src/block_downloader/block_queue.rs b/p2p/p2p/src/block_downloader/block_queue.rs index 5aae1870..708eb3ed 100644 --- a/p2p/p2p/src/block_downloader/block_queue.rs +++ b/p2p/p2p/src/block_downloader/block_queue.rs @@ -15,7 +15,7 @@ use super::{BlockBatch, BlockDownloadError}; #[derive(Debug, Clone)] pub struct ReadyQueueBatch { /// The start height of the batch. - pub start_height: usize, + pub start_height: u64, /// The batch of blocks. pub block_batch: BlockBatch, } @@ -64,7 +64,7 @@ impl BlockQueue { } /// Returns the oldest batch that has not been put in the [`async_buffer`] yet. - pub fn oldest_ready_batch(&self) -> Option { + pub fn oldest_ready_batch(&self) -> Option { self.ready_batches.peek().map(|batch| batch.start_height) } @@ -80,13 +80,13 @@ impl BlockQueue { pub async fn add_incoming_batch( &mut self, new_batch: ReadyQueueBatch, - oldest_in_flight_start_height: Option, + oldest_in_flight_start_height: Option, ) -> Result<(), BlockDownloadError> { self.ready_batches_size += new_batch.block_batch.size; self.ready_batches.push(new_batch); // The height to stop pushing batches into the buffer. - let height_to_stop_at = oldest_in_flight_start_height.unwrap_or(usize::MAX); + let height_to_stop_at = oldest_in_flight_start_height.unwrap_or(u64::MAX); while self .ready_batches @@ -113,11 +113,10 @@ impl BlockQueue { #[cfg(test)] mod tests { - use futures::StreamExt; - use std::{collections::BTreeSet, sync::Arc}; + use std::collections::BTreeSet; + use futures::StreamExt; use proptest::{collection::vec, prelude::*}; - use tokio::sync::Semaphore; use tokio_test::block_on; use cuprate_p2p_core::handles::HandleBuilder; @@ -125,9 +124,8 @@ mod tests { use super::*; prop_compose! { - fn ready_batch_strategy()(start_height in 0_usize..500_000_000) -> ReadyQueueBatch { - // TODO: The permit will not be needed here when - let (_, peer_handle) = HandleBuilder::new().with_permit(Arc::new(Semaphore::new(1)).try_acquire_owned().unwrap()).build(); + fn ready_batch_strategy()(start_height in 0_u64..500_000_000) -> ReadyQueueBatch { + let (_, peer_handle) = HandleBuilder::new().build(); ReadyQueueBatch { start_height, @@ -142,6 +140,7 @@ mod tests { proptest! { #[test] + #[allow(clippy::mutable_key_type)] fn block_queue_returns_items_in_order(batches in vec(ready_batch_strategy(), 0..10_000)) { block_on(async move { let (buffer_tx, mut buffer_rx) = cuprate_async_buffer::new_buffer(usize::MAX); diff --git a/p2p/p2p/src/block_downloader/download_batch.rs b/p2p/p2p/src/block_downloader/download_batch.rs index c5c745ab..ea57eade 100644 --- a/p2p/p2p/src/block_downloader/download_batch.rs +++ b/p2p/p2p/src/block_downloader/download_batch.rs @@ -8,7 +8,10 @@ use tracing::instrument; use cuprate_fixed_bytes::ByteArrayVec; use cuprate_helper::asynch::rayon_spawn_async; -use cuprate_p2p_core::{handles::ConnectionHandle, NetworkZone, PeerRequest, PeerResponse}; +use cuprate_p2p_core::{ + handles::ConnectionHandle, NetworkZone, PeerRequest, PeerResponse, ProtocolRequest, + ProtocolResponse, +}; use cuprate_wire::protocol::{GetObjectsRequest, GetObjectsResponse}; use crate::{ @@ -50,16 +53,15 @@ async fn request_batch_from_peer( previous_id: [u8; 32], expected_start_height: usize, ) -> Result<(ClientPoolDropGuard, BlockBatch), BlockDownloadError> { - // Request the blocks. + let request = PeerRequest::Protocol(ProtocolRequest::GetObjects(GetObjectsRequest { + blocks: ids.clone(), + pruned: false, + })); + + // Request the blocks and add a timeout to the request let blocks_response = timeout(BLOCK_DOWNLOADER_REQUEST_TIMEOUT, async { - let PeerResponse::GetObjects(blocks_response) = client - .ready() - .await? - .call(PeerRequest::GetObjects(GetObjectsRequest { - blocks: ids.clone(), - pruned: false, - })) - .await? + let PeerResponse::Protocol(ProtocolResponse::GetObjects(blocks_response)) = + client.ready().await?.call(request).await? else { panic!("Connection task returned wrong response."); }; diff --git a/p2p/p2p/src/block_downloader/request_chain.rs b/p2p/p2p/src/block_downloader/request_chain.rs index f8b53194..4b0b47e5 100644 --- a/p2p/p2p/src/block_downloader/request_chain.rs +++ b/p2p/p2p/src/block_downloader/request_chain.rs @@ -10,7 +10,7 @@ use cuprate_p2p_core::{ client::InternalPeerID, handles::ConnectionHandle, services::{PeerSyncRequest, PeerSyncResponse}, - NetworkZone, PeerRequest, PeerResponse, PeerSyncSvc, + NetworkZone, PeerRequest, PeerResponse, PeerSyncSvc, ProtocolRequest, ProtocolResponse, }; use cuprate_wire::protocol::{ChainRequest, ChainResponse}; @@ -34,13 +34,15 @@ pub async fn request_chain_entry_from_peer( mut client: ClientPoolDropGuard, short_history: [[u8; 32]; 2], ) -> Result<(ClientPoolDropGuard, ChainEntry), BlockDownloadError> { - let PeerResponse::GetChain(chain_res) = client + let PeerResponse::Protocol(ProtocolResponse::GetChain(chain_res)) = client .ready() .await? - .call(PeerRequest::GetChain(ChainRequest { - block_ids: short_history.into(), - prune: true, - })) + .call(PeerRequest::Protocol(ProtocolRequest::GetChain( + ChainRequest { + block_ids: short_history.into(), + prune: true, + }, + ))) .await? else { panic!("Connection task returned wrong response!"); @@ -132,10 +134,10 @@ where let mut futs = JoinSet::new(); - let req = PeerRequest::GetChain(ChainRequest { + let req = PeerRequest::Protocol(ProtocolRequest::GetChain(ChainRequest { block_ids: block_ids.into(), prune: false, - }); + })); tracing::debug!("Sending requests for chain entries."); @@ -149,7 +151,7 @@ where futs.spawn(timeout( BLOCK_DOWNLOADER_REQUEST_TIMEOUT, async move { - let PeerResponse::GetChain(chain_res) = + let PeerResponse::Protocol(ProtocolResponse::GetChain(chain_res)) = next_peer.ready().await?.call(cloned_req).await? else { panic!("connection task returned wrong response!"); @@ -198,7 +200,7 @@ where tracing::debug!("Highest chin entry contained {} block Ids", hashes.len()); // Find the first unknown block in the batch. - let ChainSvcResponse::FindFirstUnknown(first_unknown, expected_height) = our_chain_svc + let ChainSvcResponse::FindFirstUnknown(first_unknown_ret) = our_chain_svc .ready() .await? .call(ChainSvcRequest::FindFirstUnknown(hashes.clone())) @@ -207,18 +209,18 @@ where panic!("chain service sent wrong response."); }; + // We know all the blocks already + // TODO: The peer could still be on a different chain, however the chain might just be too far split. + let Some((first_unknown, expected_height)) = first_unknown_ret else { + return Err(BlockDownloadError::FailedToFindAChainToFollow); + }; + // The peer must send at least one block we already know. if first_unknown == 0 { peer_handle.ban_peer(MEDIUM_BAN); return Err(BlockDownloadError::PeerSentNoOverlappingBlocks); } - // We know all the blocks already - // TODO: The peer could still be on a different chain, however the chain might just be too far split. - if first_unknown == hashes.len() { - return Err(BlockDownloadError::FailedToFindAChainToFollow); - } - let previous_id = hashes[first_unknown - 1]; let first_entry = ChainEntry { diff --git a/p2p/p2p/src/block_downloader/tests.rs b/p2p/p2p/src/block_downloader/tests.rs index 4d529730..a4148466 100644 --- a/p2p/p2p/src/block_downloader/tests.rs +++ b/p2p/p2p/src/block_downloader/tests.rs @@ -14,21 +14,19 @@ use monero_serai::{ transaction::{Input, Timelock, Transaction, TransactionPrefix}, }; use proptest::{collection::vec, prelude::*}; -use tokio::{sync::Semaphore, time::timeout}; +use tokio::time::timeout; use tower::{service_fn, Service}; use cuprate_fixed_bytes::ByteArrayVec; use cuprate_p2p_core::{ client::{mock_client, Client, InternalPeerID, PeerInformation}, - network_zones::ClearNet, services::{PeerSyncRequest, PeerSyncResponse}, - ConnectionDirection, NetworkZone, PeerRequest, PeerResponse, + ClearNet, ConnectionDirection, NetworkZone, PeerRequest, PeerResponse, ProtocolRequest, + ProtocolResponse, }; use cuprate_pruning::PruningSeed; -use cuprate_wire::{ - common::{BlockCompleteEntry, TransactionBlobs}, - protocol::{ChainResponse, GetObjectsResponse}, -}; +use cuprate_types::{BlockCompleteEntry, TransactionBlobs}; +use cuprate_wire::protocol::{ChainResponse, GetObjectsResponse}; use crate::{ block_downloader::{download_blocks, BlockDownloaderConfig, ChainSvcRequest, ChainSvcResponse}, @@ -94,7 +92,7 @@ prop_compose! { fn dummy_transaction_stragtegy(height: u64) ( extra in vec(any::(), 0..1_000), - timelock in 0_usize..50_000_000, + timelock in 1_usize..50_000_000, ) -> Transaction { Transaction::V1 { @@ -171,18 +169,15 @@ prop_compose! { } fn mock_block_downloader_client(blockchain: Arc) -> Client { - let semaphore = Arc::new(Semaphore::new(1)); - - let (connection_guard, connection_handle) = cuprate_p2p_core::handles::HandleBuilder::new() - .with_permit(semaphore.try_acquire_owned().unwrap()) - .build(); + let (connection_guard, connection_handle) = + cuprate_p2p_core::handles::HandleBuilder::new().build(); let request_handler = service_fn(move |req: PeerRequest| { let bc = blockchain.clone(); async move { match req { - PeerRequest::GetChain(chain_req) => { + PeerRequest::Protocol(ProtocolRequest::GetChain(chain_req)) => { let mut i = 0; while !bc.blocks.contains_key(&chain_req.block_ids[i]) { i += 1; @@ -204,18 +199,20 @@ fn mock_block_downloader_client(blockchain: Arc) -> Client>(); - Ok(PeerResponse::GetChain(ChainResponse { - start_height: 0, - total_height: 0, - cumulative_difficulty_low64: 1, - cumulative_difficulty_top64: 0, - m_block_ids: block_ids.into(), - m_block_weights: vec![], - first_block: Default::default(), - })) + Ok(PeerResponse::Protocol(ProtocolResponse::GetChain( + ChainResponse { + start_height: 0, + total_height: 0, + cumulative_difficulty_low64: 1, + cumulative_difficulty_top64: 0, + m_block_ids: block_ids.into(), + m_block_weights: vec![], + first_block: Default::default(), + }, + ))) } - PeerRequest::GetObjects(obj) => { + PeerRequest::Protocol(ProtocolRequest::GetObjects(obj)) => { let mut res = Vec::with_capacity(obj.blocks.len()); for i in 0..obj.blocks.len() { @@ -238,11 +235,13 @@ fn mock_block_downloader_client(blockchain: Arc) -> Client panic!(), } @@ -253,7 +252,7 @@ fn mock_block_downloader_client(blockchain: Arc) -> Client for OurChainSvc { block_ids: vec![genesis], cumulative_difficulty: 1, }, - ChainSvcRequest::FindFirstUnknown(_) => ChainSvcResponse::FindFirstUnknown(1, 1), + ChainSvcRequest::FindFirstUnknown(_) => { + ChainSvcResponse::FindFirstUnknown(Some((1, 1))) + } ChainSvcRequest::CumulativeDifficulty => ChainSvcResponse::CumulativeDifficulty(1), }) } diff --git a/p2p/p2p/src/broadcast.rs b/p2p/p2p/src/broadcast.rs index db7a41ee..5d7d61e3 100644 --- a/p2p/p2p/src/broadcast.rs +++ b/p2p/p2p/src/broadcast.rs @@ -25,10 +25,8 @@ use tower::Service; use cuprate_p2p_core::{ client::InternalPeerID, BroadcastMessage, ConnectionDirection, NetworkZone, }; -use cuprate_wire::{ - common::{BlockCompleteEntry, TransactionBlobs}, - protocol::{NewFluffyBlock, NewTransactions}, -}; +use cuprate_types::{BlockCompleteEntry, TransactionBlobs}; +use cuprate_wire::protocol::{NewFluffyBlock, NewTransactions}; use crate::constants::{ DIFFUSION_FLUSH_AVERAGE_SECONDS_INBOUND, DIFFUSION_FLUSH_AVERAGE_SECONDS_OUTBOUND, @@ -196,10 +194,10 @@ impl Service> for BroadcastSvc { // An error here means _all_ receivers were dropped which we assume will never happen. let _ = match direction { - Some(ConnectionDirection::InBound) => { + Some(ConnectionDirection::Inbound) => { self.tx_broadcast_channel_inbound.send(nex_tx_info) } - Some(ConnectionDirection::OutBound) => { + Some(ConnectionDirection::Outbound) => { self.tx_broadcast_channel_outbound.send(nex_tx_info) } None => { @@ -428,7 +426,7 @@ mod tests { .unwrap() .call(BroadcastRequest::Transaction { tx_bytes: Bytes::from_static(&[1]), - direction: Some(ConnectionDirection::OutBound), + direction: Some(ConnectionDirection::Outbound), received_from: None, }) .await @@ -440,7 +438,7 @@ mod tests { .unwrap() .call(BroadcastRequest::Transaction { tx_bytes: Bytes::from_static(&[2]), - direction: Some(ConnectionDirection::InBound), + direction: Some(ConnectionDirection::Inbound), received_from: None, }) .await diff --git a/p2p/p2p/src/client_pool.rs b/p2p/p2p/src/client_pool.rs index 711491d0..51f57e9f 100644 --- a/p2p/p2p/src/client_pool.rs +++ b/p2p/p2p/src/client_pool.rs @@ -9,7 +9,6 @@ //! //! Internally the pool is a [`DashMap`] which means care should be taken in `async` code //! as internally this uses blocking RwLocks. -//! use std::sync::Arc; use dashmap::DashMap; diff --git a/p2p/p2p/src/connection_maintainer.rs b/p2p/p2p/src/connection_maintainer.rs index 66db9de2..3dfd5e8d 100644 --- a/p2p/p2p/src/connection_maintainer.rs +++ b/p2p/p2p/src/connection_maintainer.rs @@ -106,10 +106,6 @@ where panic!("No seed nodes available to get peers from"); } - // This isn't really needed here to limit connections as the seed nodes will be dropped when we have got - // peers from them. - let semaphore = Arc::new(Semaphore::new(seeds.len())); - let mut allowed_errors = seeds.len(); let mut handshake_futs = JoinSet::new(); @@ -125,10 +121,7 @@ where .expect("Connector had an error in `poll_ready`") .call(ConnectRequest { addr: *seed, - permit: semaphore - .clone() - .try_acquire_owned() - .expect("This must have enough permits as we just set the amount."), + permit: None, }), ); // Spawn the handshake on a separate task with a timeout, so we don't get stuck connecting to a peer. @@ -157,7 +150,10 @@ where .ready() .await .expect("Connector had an error in `poll_ready`") - .call(ConnectRequest { addr, permit }); + .call(ConnectRequest { + addr, + permit: Some(permit), + }); tokio::spawn( async move { diff --git a/p2p/p2p/src/inbound_server.rs b/p2p/p2p/src/inbound_server.rs index 6bc1e6d8..aa971a51 100644 --- a/p2p/p2p/src/inbound_server.rs +++ b/p2p/p2p/src/inbound_server.rs @@ -87,8 +87,8 @@ where addr, peer_stream, peer_sink, - direction: ConnectionDirection::InBound, - permit, + direction: ConnectionDirection::Inbound, + permit: Some(permit), }); let cloned_pool = client_pool.clone(); diff --git a/p2p/p2p/src/lib.rs b/p2p/p2p/src/lib.rs index 95154ec7..be18c2a3 100644 --- a/p2p/p2p/src/lib.rs +++ b/p2p/p2p/src/lib.rs @@ -4,7 +4,6 @@ //! a certain [`NetworkZone`] use std::sync::Arc; -use cuprate_async_buffer::BufferStream; use futures::FutureExt; use tokio::{ sync::{mpsc, watch}, @@ -14,11 +13,12 @@ use tokio_stream::wrappers::WatchStream; use tower::{buffer::Buffer, util::BoxCloneService, Service, ServiceExt}; use tracing::{instrument, Instrument, Span}; +use cuprate_async_buffer::BufferStream; use cuprate_p2p_core::{ client::Connector, client::InternalPeerID, services::{AddressBookRequest, AddressBookResponse, PeerSyncRequest}, - CoreSyncSvc, NetworkZone, PeerRequestHandler, + CoreSyncSvc, NetworkZone, ProtocolRequestHandler, }; mod block_downloader; @@ -42,17 +42,18 @@ use connection_maintainer::MakeConnectionRequest; /// /// # Usage /// You must provide: -/// - A peer request handler, which is given to each connection +/// - A protocol request handler, which is given to each connection /// - A core sync service, which keeps track of the sync state of our node #[instrument(level = "debug", name = "net", skip_all, fields(zone = N::NAME))] -pub async fn initialize_network( - peer_req_handler: R, +pub async fn initialize_network( + protocol_request_handler: PR, core_sync_svc: CS, config: P2PConfig, ) -> Result, tower::BoxError> where N: NetworkZone, - R: PeerRequestHandler + Clone, + N::Addr: borsh::BorshDeserialize + borsh::BorshSerialize, + PR: ProtocolRequestHandler + Clone, CS: CoreSyncSvc + Clone, { let address_book = @@ -79,23 +80,21 @@ where basic_node_data.peer_id = 1; } - let outbound_handshaker = cuprate_p2p_core::client::HandShaker::new( - address_book.clone(), - sync_states_svc.clone(), - core_sync_svc.clone(), - peer_req_handler.clone(), - outbound_mkr, - basic_node_data.clone(), - ); + let outbound_handshaker_builder = + cuprate_p2p_core::client::HandshakerBuilder::new(basic_node_data) + .with_address_book(address_book.clone()) + .with_peer_sync_svc(sync_states_svc.clone()) + .with_core_sync_svc(core_sync_svc) + .with_protocol_request_handler(protocol_request_handler) + .with_broadcast_stream_maker(outbound_mkr) + .with_connection_parent_span(Span::current()); - let inbound_handshaker = cuprate_p2p_core::client::HandShaker::new( - address_book.clone(), - sync_states_svc.clone(), - core_sync_svc.clone(), - peer_req_handler, - inbound_mkr, - basic_node_data, - ); + let inbound_handshaker = outbound_handshaker_builder + .clone() + .with_broadcast_stream_maker(inbound_mkr) + .build(); + + let outbound_handshaker = outbound_handshaker_builder.build(); let client_pool = client_pool::ClientPool::new(); diff --git a/p2p/p2p/src/sync_states.rs b/p2p/p2p/src/sync_states.rs index ae6959aa..70ef6ca7 100644 --- a/p2p/p2p/src/sync_states.rs +++ b/p2p/p2p/src/sync_states.rs @@ -238,9 +238,6 @@ impl Service> for PeerSyncSvc { #[cfg(test)] mod tests { - use std::sync::Arc; - - use tokio::sync::Semaphore; use tower::{Service, ServiceExt}; use cuprate_p2p_core::{ @@ -255,11 +252,7 @@ mod tests { #[tokio::test] async fn top_sync_channel_updates() { - let semaphore = Arc::new(Semaphore::new(1)); - - let (_g, handle) = HandleBuilder::new() - .with_permit(semaphore.try_acquire_owned().unwrap()) - .build(); + let (_g, handle) = HandleBuilder::new().build(); let (mut svc, mut watch) = PeerSyncSvc::>::new(); @@ -336,11 +329,7 @@ mod tests { #[tokio::test] async fn peer_sync_info_updates() { - let semaphore = Arc::new(Semaphore::new(1)); - - let (_g, handle) = HandleBuilder::new() - .with_permit(semaphore.try_acquire_owned().unwrap()) - .build(); + let (_g, handle) = HandleBuilder::new().build(); let (mut svc, _watch) = PeerSyncSvc::>::new(); diff --git a/rpc/types/Cargo.toml b/rpc/types/Cargo.toml index 30e4aa95..9c996818 100644 --- a/rpc/types/Cargo.toml +++ b/rpc/types/Cargo.toml @@ -9,14 +9,22 @@ repository = "https://github.com/Cuprate/cuprate/tree/main/rpc/types" keywords = ["cuprate", "rpc", "types", "monero"] [features] -default = [] +default = ["serde", "epee"] +serde = ["dep:serde", "cuprate-fixed-bytes/serde"] +epee = ["dep:cuprate-epee-encoding"] [dependencies] -cuprate-epee-encoding = { path = "../../net/epee-encoding" } +cuprate-epee-encoding = { path = "../../net/epee-encoding", optional = true } +cuprate-fixed-bytes = { path = "../../net/fixed-bytes" } +cuprate-types = { path = "../../types" } monero-serai = { workspace = true } paste = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, optional = true } [dev-dependencies] -serde_json = { workspace = true } +cuprate-test-utils = { path = "../../test-utils" } +cuprate-json-rpc = { path = "../json-rpc" } + +serde_json = { workspace = true } +pretty_assertions = { workspace = true } \ No newline at end of file diff --git a/rpc/types/README.md b/rpc/types/README.md index 65b6d907..566cca7e 100644 --- a/rpc/types/README.md +++ b/rpc/types/README.md @@ -10,13 +10,14 @@ This crate ports the types used in Monero's RPC interface, including: # Modules This crate's types are split in the following manner: -This crate has 4 modules: -- The root module; `cuprate_rpc_types` -- [`json`] module; JSON types from the `/json_rpc` endpoint -- [`bin`] module; Binary types from the binary endpoints -- [`other`] module; Misc JSON types from other endpoints - -Miscellaneous types are found in the root module, e.g. [`crate::Status`]. +| Module | Purpose | +|--------|---------| +| The root module | Miscellaneous items, e.g. constants. +| [`json`] | Contains JSON request/response (some mixed with binary) that all share the common `/json_rpc` endpoint. | +| [`bin`] | Contains request/response types that are expected to be fully in binary (`cuprate_epee_encoding`) in `monerod` and `cuprated`'s RPC interface. These are called at a custom endpoint instead of `/json_rpc`, e.g. `/get_blocks.bin`. | +| [`other`] | Contains request/response types that are JSON, but aren't called at `/json_rpc` (e.g. [`crate::other::GetHeightRequest`]). | +| [`misc`] | Contains miscellaneous types, e.g. [`crate::misc::Status`]. Many of types here are found and used in request/response types, for example, [`crate::misc::BlockHeader`] is used in [`crate::json::GetLastBlockHeaderResponse`]. | +| [`base`] | Contains base types flattened into many request/response types. Each type in `{json,bin,other}` come in pairs and have identical names, but are suffixed with either `Request` or `Response`. e.g. [`GetBlockCountRequest`](crate::json::GetBlockCountRequest) & [`GetBlockCountResponse`](crate::json::GetBlockCountResponse). @@ -30,23 +31,21 @@ However, each type will document: # Naming The naming for types within `{json,bin,other}` follow the following scheme: -- Convert the endpoint or method name into `UpperCamelCase` -- Remove any suffix extension +1. Convert the endpoint or method name into `UpperCamelCase` +1. Remove any suffix extension +1. Add `Request/Response` suffix For example: | Endpoint/method | Crate location and name | |-----------------|-------------------------| | [`get_block_count`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_block_count) | [`json::GetBlockCountRequest`] & [`json::GetBlockCountResponse`] -| [`/get_blocks.bin`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_blockbin) | `bin::GetBlocksRequest` & `bin::GetBlocksResponse` -| [`/get_height`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_height) | `other::GetHeightRequest` & `other::GetHeightResponse` - -TODO: fix doc links when types are ready. +| [`/get_blocks.bin`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_blockbin) | [`bin::GetBlocksRequest`] & [`bin::GetBlocksResponse`] +| [`/get_height`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_height) | [`other::GetHeightRequest`] & [`other::GetHeightResponse`] # Mixed types -Note that some types within [`other`] mix JSON & binary together, i.e., -the message overall is JSON, however some fields contain binary -values inside JSON strings, for example: +Note that some types mix JSON & binary together, i.e., the message overall is JSON, +however some fields contain binary values inside JSON strings, for example: ```json { @@ -57,6 +56,50 @@ values inside JSON strings, for example: } ``` -`binary` here is (de)serialized as a normal [`String`]. In order to be clear on which fields contain binary data, the struct fields that have them will use [`crate::BinaryString`] instead of [`String`]. +`binary` here is (de)serialized as a normal [`String`]. In order to be clear on which fields contain binary data, the struct fields that have them will use [`crate::misc::BinaryString`] instead of [`String`]. -TODO: list the specific types. \ No newline at end of file +These mixed types are: +- [`crate::json::GetTransactionPoolBacklogResponse`] +- [`crate::json::GetOutputDistributionResponse`] + +TODO: we need to figure out a type that (de)serializes correctly, `String` errors with `serde_json` + +# Fixed byte containers +TODO + + + +# (De)serialization invariants +Due to how types are defined in this library internally (all through a single macro), +most types implement both `serde` and `epee`. + +However, some of the types will panic with [`unimplemented`] +or will otherwise have undefined implementation in the incorrect context. + +In other words: +- The epee (de)serialization of [`json`] & [`other`] types should **not** be relied upon +- The JSON (de)serialization of [`bin`] types should **not** be relied upon + +The invariants that can be relied upon: +- Types in [`json`] & [`other`] will implement `serde` correctly +- Types in [`bin`] will implement `epee` correctly +- Misc types will implement `serde/epee` correctly as needed + +# Feature flags +List of feature flags for `cuprate-rpc-types`. + +All are enabled by default. + +| Feature flag | Does what | +|--------------|-----------| +| `serde` | Implements `serde` on all types +| `epee` | Implements `cuprate_epee_encoding` on all types \ No newline at end of file diff --git a/rpc/types/src/base.rs b/rpc/types/src/base.rs index 6a293678..c131e41e 100644 --- a/rpc/types/src/base.rs +++ b/rpc/types/src/base.rs @@ -10,76 +10,44 @@ //! - //! - //! - +//! +//! Note that this library doesn't use [`AccessRequestBase`](https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server_commands_defs.h#L114-L122) found in `monerod` +//! as the type is practically deprecated. +//! +//! Although, [`AccessResponseBase`] still exists as to allow +//! outputting the same JSON fields as `monerod` (even if deprecated). //---------------------------------------------------------------------------------------------------- Import +#[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "epee")] use cuprate_epee_encoding::epee_object; -use crate::Status; - -//---------------------------------------------------------------------------------------------------- Macro -/// Link the original `monerod` definition for RPC base types. -macro_rules! monero_rpc_base_link { - ($start:literal..=$end:literal) => { - concat!( - "[Definition](https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server_commands_defs.h#L", - stringify!($start), - "-L", - stringify!($end), - ")." - ) - }; -} +use crate::{macros::monero_definition_link, misc::Status}; //---------------------------------------------------------------------------------------------------- Requests -/// The most common base for responses (nothing). -/// -#[doc = monero_rpc_base_link!(95..=99)] -#[derive( - Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, -)] -pub struct EmptyRequestBase; - -cuprate_epee_encoding::epee_object! { - EmptyRequestBase, -} - /// A base for RPC request types that support RPC payment. /// -#[doc = monero_rpc_base_link!(114..=122)] -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "rpc/core_rpc_server_commands_defs.h", 114..=122)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct AccessRequestBase { /// The RPC payment client. pub client: String, } -cuprate_epee_encoding::epee_object! { +#[cfg(feature = "epee")] +epee_object! { AccessRequestBase, client: String, } //---------------------------------------------------------------------------------------------------- Responses -/// An empty response base. -/// -/// This is for response types that do not contain -/// any extra fields, e.g. TODO. -// [`CalcPowResponse`](crate::json::CalcPowResponse). -#[derive( - Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, -)] -pub struct EmptyResponseBase; - -cuprate_epee_encoding::epee_object! { - EmptyResponseBase, -} - +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "rpc/core_rpc_server_commands_defs.h", 101..=112)] /// The most common base for responses. -/// -#[doc = monero_rpc_base_link!(101..=112)] -#[derive( - Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, -)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ResponseBase { /// General RPC error code. [`Status::Ok`] means everything looks good. pub status: Status, @@ -89,19 +57,78 @@ pub struct ResponseBase { pub untrusted: bool, } +impl ResponseBase { + /// `const` version of [`Default::default`]. + /// + /// ```rust + /// use cuprate_rpc_types::{misc::*, base::*}; + /// + /// let new = ResponseBase::new(); + /// assert_eq!(new, ResponseBase { + /// status: Status::Ok, + /// untrusted: false, + /// }); + /// ``` + pub const fn new() -> Self { + Self { + status: Status::Ok, + untrusted: false, + } + } + + /// Returns OK and trusted [`Self`]. + /// + /// This is the most common version of [`Self`]. + /// + /// ```rust + /// use cuprate_rpc_types::{misc::*, base::*}; + /// + /// let ok = ResponseBase::ok(); + /// assert_eq!(ok, ResponseBase { + /// status: Status::Ok, + /// untrusted: false, + /// }); + /// ``` + pub const fn ok() -> Self { + Self { + status: Status::Ok, + untrusted: false, + } + } + + /// Same as [`Self::ok`] but with [`Self::untrusted`] set to `true`. + /// + /// ```rust + /// use cuprate_rpc_types::{misc::*, base::*}; + /// + /// let ok_untrusted = ResponseBase::ok_untrusted(); + /// assert_eq!(ok_untrusted, ResponseBase { + /// status: Status::Ok, + /// untrusted: true, + /// }); + /// ``` + pub const fn ok_untrusted() -> Self { + Self { + status: Status::Ok, + untrusted: true, + } + } +} + +#[cfg(feature = "epee")] epee_object! { ResponseBase, status: Status, untrusted: bool, } +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "rpc/core_rpc_server_commands_defs.h", 124..=136)] /// A base for RPC response types that support RPC payment. -/// -#[doc = monero_rpc_base_link!(124..=136)] -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct AccessResponseBase { /// A flattened [`ResponseBase`]. - #[serde(flatten)] + #[cfg_attr(feature = "serde", serde(flatten))] pub response_base: ResponseBase, /// If payment for RPC is enabled, the number of credits /// available to the requesting client. Otherwise, `0`. @@ -111,6 +138,75 @@ pub struct AccessResponseBase { pub top_hash: String, } +impl AccessResponseBase { + /// Creates a new [`Self`] with default values. + /// + /// Since RPC payment is semi-deprecated, [`Self::credits`] + /// and [`Self::top_hash`] will always be set to the default + /// values. + /// + /// ```rust + /// use cuprate_rpc_types::{misc::*, base::*}; + /// + /// let new = AccessResponseBase::new(ResponseBase::ok()); + /// assert_eq!(new, AccessResponseBase { + /// response_base: ResponseBase::ok(), + /// credits: 0, + /// top_hash: "".into(), + /// }); + /// ``` + pub const fn new(response_base: ResponseBase) -> Self { + Self { + response_base, + credits: 0, + top_hash: String::new(), + } + } + + /// Returns OK and trusted [`Self`]. + /// + /// This is the most common version of [`Self`]. + /// + /// ```rust + /// use cuprate_rpc_types::{misc::*, base::*}; + /// + /// let ok = AccessResponseBase::ok(); + /// assert_eq!(ok, AccessResponseBase { + /// response_base: ResponseBase::ok(), + /// credits: 0, + /// top_hash: "".into(), + /// }); + /// ``` + pub const fn ok() -> Self { + Self { + response_base: ResponseBase::ok(), + credits: 0, + top_hash: String::new(), + } + } + + /// Same as [`Self::ok`] but with `untrusted` set to `true`. + /// + /// ```rust + /// use cuprate_rpc_types::{misc::*, base::*}; + /// + /// let ok_untrusted = AccessResponseBase::ok_untrusted(); + /// assert_eq!(ok_untrusted, AccessResponseBase { + /// response_base: ResponseBase::ok_untrusted(), + /// credits: 0, + /// top_hash: "".into(), + /// }); + /// ``` + pub const fn ok_untrusted() -> Self { + Self { + response_base: ResponseBase::ok_untrusted(), + credits: 0, + top_hash: String::new(), + } + } +} + +#[cfg(feature = "epee")] epee_object! { AccessResponseBase, credits: u64, diff --git a/rpc/types/src/bin.rs b/rpc/types/src/bin.rs index f327847f..c801c69e 100644 --- a/rpc/types/src/bin.rs +++ b/rpc/types/src/bin.rs @@ -1,8 +1,397 @@ -//! Binary types from [binary](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_blocksbin) endpoints. +//! Binary types from [`.bin` endpoints](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_blocksbin). +//! +//! All types are originally defined in [`rpc/core_rpc_server_commands_defs.h`](https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server_commands_defs.h). //---------------------------------------------------------------------------------------------------- Import +use cuprate_fixed_bytes::ByteArrayVec; -//---------------------------------------------------------------------------------------------------- TODO +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "epee")] +use cuprate_epee_encoding::{ + container_as_blob::ContainerAsBlob, + epee_object, error, + macros::bytes::{Buf, BufMut}, + read_epee_value, write_field, EpeeObject, EpeeObjectBuilder, EpeeValue, +}; + +use cuprate_types::BlockCompleteEntry; + +use crate::{ + base::{AccessResponseBase, ResponseBase}, + defaults::{default_false, default_height, default_string, default_vec, default_zero}, + free::{is_one, is_zero}, + macros::{define_request, define_request_and_response, define_request_and_response_doc}, + misc::{ + AuxPow, BlockHeader, BlockOutputIndices, ChainInfo, ConnectionInfo, GetBan, GetOutputsOut, + HardforkEntry, HistogramEntry, OutKeyBin, OutputDistributionData, Peer, PoolInfoExtent, + PoolTxInfo, SetBan, Span, Status, TxBacklogEntry, + }, +}; + +//---------------------------------------------------------------------------------------------------- Definitions +define_request_and_response! { + get_blocks_by_heightbin, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 264..=286, + GetBlocksByHeight, + Request { + heights: Vec, + }, + AccessResponseBase { + blocks: Vec, + } +} + +define_request_and_response! { + get_hashesbin, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 309..=338, + GetHashes, + Request { + block_ids: ByteArrayVec<32>, + start_height: u64, + }, + AccessResponseBase { + m_blocks_ids: ByteArrayVec<32>, + start_height: u64, + current_height: u64, + } +} + +#[cfg(not(feature = "epee"))] +define_request_and_response! { + get_o_indexesbin, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 487..=510, + GetOutputIndexes, + #[derive(Copy)] + Request { + txid: [u8; 32], + }, + AccessResponseBase { + o_indexes: Vec, + } +} + +#[cfg(feature = "epee")] +define_request_and_response! { + get_o_indexesbin, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 487..=510, + GetOutputIndexes, + #[derive(Copy)] + Request { + txid: [u8; 32], + }, + AccessResponseBase { + o_indexes: Vec as ContainerAsBlob, + } +} + +define_request_and_response! { + get_outsbin, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 512..=565, + GetOuts, + Request { + outputs: Vec, + get_txid: bool = default_false(), "default_false", + }, + AccessResponseBase { + outs: Vec, + } +} + +define_request_and_response! { + get_transaction_pool_hashesbin, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1593..=1613, + GetTransactionPoolHashes, + Request {}, + AccessResponseBase { + tx_hashes: ByteArrayVec<32>, + } +} + +//---------------------------------------------------------------------------------------------------- GetBlocks +define_request! { + #[doc = define_request_and_response_doc!( + "response" => GetBlocksResponse, + get_blocksbin, + cc73fe71162d564ffda8e549b79a350bca53c454, + core_rpc_server_commands_defs, h, 162, 262, + )] + GetBlocksRequest { + requested_info: u8 = default_zero::(), "default_zero", + // FIXME: This is a `std::list` in `monerod` because...? + block_ids: ByteArrayVec<32>, + start_height: u64, + prune: bool, + no_miner_tx: bool = default_false(), "default_false", + pool_info_since: u64 = default_zero::(), "default_zero", + } +} + +#[doc = define_request_and_response_doc!( + "request" => GetBlocksRequest, + get_blocksbin, + cc73fe71162d564ffda8e549b79a350bca53c454, + core_rpc_server_commands_defs, h, 162, 262, +)] +/// +/// This response's variant depends upon [`PoolInfoExtent`]. +#[allow(dead_code, missing_docs)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum GetBlocksResponse { + /// Will always serialize a [`PoolInfoExtent::None`] field. + PoolInfoNone(GetBlocksResponsePoolInfoNone), + /// Will always serialize a [`PoolInfoExtent::Incremental`] field. + PoolInfoIncremental(GetBlocksResponsePoolInfoIncremental), + /// Will always serialize a [`PoolInfoExtent::Full`] field. + PoolInfoFull(GetBlocksResponsePoolInfoFull), +} + +impl Default for GetBlocksResponse { + fn default() -> Self { + Self::PoolInfoNone(GetBlocksResponsePoolInfoNone::default()) + } +} + +/// Data within [`GetBlocksResponse::PoolInfoNone`]. +#[allow(dead_code, missing_docs)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GetBlocksResponsePoolInfoNone { + pub status: Status, + pub untrusted: bool, + pub blocks: Vec, + pub start_height: u64, + pub current_height: u64, + pub output_indices: Vec, + pub daemon_time: u64, +} + +#[cfg(feature = "epee")] +epee_object! { + GetBlocksResponsePoolInfoNone, + status: Status, + untrusted: bool, + blocks: Vec, + start_height: u64, + current_height: u64, + output_indices: Vec, + daemon_time: u64, +} + +/// Data within [`GetBlocksResponse::PoolInfoIncremental`]. +#[allow(dead_code, missing_docs)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GetBlocksResponsePoolInfoIncremental { + pub status: Status, + pub untrusted: bool, + pub blocks: Vec, + pub start_height: u64, + pub current_height: u64, + pub output_indices: Vec, + pub daemon_time: u64, + pub added_pool_txs: Vec, + pub remaining_added_pool_txids: ByteArrayVec<32>, + pub removed_pool_txids: ByteArrayVec<32>, +} + +#[cfg(feature = "epee")] +epee_object! { + GetBlocksResponsePoolInfoIncremental, + status: Status, + untrusted: bool, + blocks: Vec, + start_height: u64, + current_height: u64, + output_indices: Vec, + daemon_time: u64, + added_pool_txs: Vec, + remaining_added_pool_txids: ByteArrayVec<32>, + removed_pool_txids: ByteArrayVec<32>, +} + +/// Data within [`GetBlocksResponse::PoolInfoFull`]. +#[allow(dead_code, missing_docs)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GetBlocksResponsePoolInfoFull { + pub status: Status, + pub untrusted: bool, + pub blocks: Vec, + pub start_height: u64, + pub current_height: u64, + pub output_indices: Vec, + pub daemon_time: u64, + pub added_pool_txs: Vec, + pub remaining_added_pool_txids: ByteArrayVec<32>, +} + +#[cfg(feature = "epee")] +epee_object! { + GetBlocksResponsePoolInfoFull, + status: Status, + untrusted: bool, + blocks: Vec, + start_height: u64, + current_height: u64, + output_indices: Vec, + daemon_time: u64, + added_pool_txs: Vec, + remaining_added_pool_txids: ByteArrayVec<32>, +} + +#[cfg(feature = "epee")] +/// [`EpeeObjectBuilder`] for [`GetBlocksResponse`]. +/// +/// Not for public usage. +#[allow(dead_code, missing_docs)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct __GetBlocksResponseEpeeBuilder { + pub status: Option, + pub untrusted: Option, + pub blocks: Option>, + pub start_height: Option, + pub current_height: Option, + pub output_indices: Option>, + pub daemon_time: Option, + pub pool_info_extent: Option, + pub added_pool_txs: Option>, + pub remaining_added_pool_txids: Option>, + pub removed_pool_txids: Option>, +} + +#[cfg(feature = "epee")] +impl EpeeObjectBuilder for __GetBlocksResponseEpeeBuilder { + fn add_field(&mut self, name: &str, r: &mut B) -> error::Result { + macro_rules! read_epee_field { + ($($field:ident),*) => { + match name { + $( + stringify!($field) => { self.$field = Some(read_epee_value(r)?); }, + )* + _ => return Ok(false), + } + }; + } + + read_epee_field! { + status, + untrusted, + blocks, + start_height, + current_height, + output_indices, + daemon_time, + pool_info_extent, + added_pool_txs, + remaining_added_pool_txids, + removed_pool_txids + } + + Ok(true) + } + + fn finish(self) -> error::Result { + const ELSE: error::Error = error::Error::Format("Required field was not found!"); + + let status = self.status.ok_or(ELSE)?; + let untrusted = self.untrusted.ok_or(ELSE)?; + let blocks = self.blocks.ok_or(ELSE)?; + let start_height = self.start_height.ok_or(ELSE)?; + let current_height = self.current_height.ok_or(ELSE)?; + let output_indices = self.output_indices.ok_or(ELSE)?; + let daemon_time = self.daemon_time.ok_or(ELSE)?; + let pool_info_extent = self.pool_info_extent.ok_or(ELSE)?; + + let this = match pool_info_extent { + PoolInfoExtent::None => { + GetBlocksResponse::PoolInfoNone(GetBlocksResponsePoolInfoNone { + status, + untrusted, + blocks, + start_height, + current_height, + output_indices, + daemon_time, + }) + } + PoolInfoExtent::Incremental => { + GetBlocksResponse::PoolInfoIncremental(GetBlocksResponsePoolInfoIncremental { + status, + untrusted, + blocks, + start_height, + current_height, + output_indices, + daemon_time, + added_pool_txs: self.added_pool_txs.ok_or(ELSE)?, + remaining_added_pool_txids: self.remaining_added_pool_txids.ok_or(ELSE)?, + removed_pool_txids: self.removed_pool_txids.ok_or(ELSE)?, + }) + } + PoolInfoExtent::Full => { + GetBlocksResponse::PoolInfoFull(GetBlocksResponsePoolInfoFull { + status, + untrusted, + blocks, + start_height, + current_height, + output_indices, + daemon_time, + added_pool_txs: self.added_pool_txs.ok_or(ELSE)?, + remaining_added_pool_txids: self.remaining_added_pool_txids.ok_or(ELSE)?, + }) + } + }; + + Ok(this) + } +} + +#[cfg(feature = "epee")] +#[allow(clippy::cognitive_complexity)] +impl EpeeObject for GetBlocksResponse { + type Builder = __GetBlocksResponseEpeeBuilder; + + fn number_of_fields(&self) -> u64 { + // [`PoolInfoExtent`] + inner struct fields. + let inner_fields = match self { + Self::PoolInfoNone(s) => s.number_of_fields(), + Self::PoolInfoIncremental(s) => s.number_of_fields(), + Self::PoolInfoFull(s) => s.number_of_fields(), + }; + + 1 + inner_fields + } + + fn write_fields(self, w: &mut B) -> error::Result<()> { + match self { + Self::PoolInfoNone(s) => { + s.write_fields(w)?; + write_field(PoolInfoExtent::None.to_u8(), "pool_info_extent", w)?; + } + Self::PoolInfoIncremental(s) => { + s.write_fields(w)?; + write_field(PoolInfoExtent::Incremental.to_u8(), "pool_info_extent", w)?; + } + Self::PoolInfoFull(s) => { + s.write_fields(w)?; + write_field(PoolInfoExtent::Full.to_u8(), "pool_info_extent", w)?; + } + } + + Ok(()) + } +} //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] diff --git a/rpc/types/src/constants.rs b/rpc/types/src/constants.rs index 2d5266fd..8c6120ba 100644 --- a/rpc/types/src/constants.rs +++ b/rpc/types/src/constants.rs @@ -15,6 +15,7 @@ // What this means for Cuprate: just follow `monerod`. //---------------------------------------------------------------------------------------------------- Import +use crate::macros::monero_definition_link; //---------------------------------------------------------------------------------------------------- Status // Common RPC status strings: @@ -23,39 +24,29 @@ // Note that these are _distinct_ from the ones in ZMQ: // . -/// +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "/rpc/core_rpc_server_commands_defs.h", 78)] pub const CORE_RPC_STATUS_OK: &str = "OK"; -/// +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "/rpc/core_rpc_server_commands_defs.h", 79)] pub const CORE_RPC_STATUS_BUSY: &str = "BUSY"; -/// +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "/rpc/core_rpc_server_commands_defs.h", 80)] pub const CORE_RPC_STATUS_NOT_MINING: &str = "NOT MINING"; -/// +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "/rpc/core_rpc_server_commands_defs.h", 81)] pub const CORE_RPC_STATUS_PAYMENT_REQUIRED: &str = "PAYMENT REQUIRED"; -/// Custom `CORE_RPC_STATUS` for usage in Cuprate. -pub const CORE_RPC_STATUS_UNKNOWN: &str = "UNKNOWN"; - //---------------------------------------------------------------------------------------------------- Versions +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "/rpc/core_rpc_server_commands_defs.h", 90)] /// RPC major version. -/// -/// See: . pub const CORE_RPC_VERSION_MAJOR: u32 = 3; +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "/rpc/core_rpc_server_commands_defs.h", 91)] /// RPC miror version. -/// -/// See: . pub const CORE_RPC_VERSION_MINOR: u32 = 14; +#[doc = monero_definition_link!(cc73fe71162d564ffda8e549b79a350bca53c454, "/rpc/core_rpc_server_commands_defs.h", 92..=93)] /// RPC version. -/// -/// See: . -/// -/// ```rust -/// assert_eq!(cuprate_rpc_types::CORE_RPC_VERSION, 196_622); -/// ``` pub const CORE_RPC_VERSION: u32 = (CORE_RPC_VERSION_MAJOR << 16) | CORE_RPC_VERSION_MINOR; //---------------------------------------------------------------------------------------------------- Tests diff --git a/rpc/types/src/defaults.rs b/rpc/types/src/defaults.rs new file mode 100644 index 00000000..6addd0ab --- /dev/null +++ b/rpc/types/src/defaults.rs @@ -0,0 +1,76 @@ +//! These functions define the default values +//! of optional fields in request/response types. +//! +//! For example, [`crate::json::GetBlockRequest`] +//! has a [`crate::json::GetBlockRequest::height`] +//! field and a [`crate::json::GetBlockRequest::hash`] +//! field, when the RPC interface reads JSON without +//! `height`, it will use [`default_height`] to fill that in. + +//---------------------------------------------------------------------------------------------------- Import +use std::borrow::Cow; + +//---------------------------------------------------------------------------------------------------- TODO +/// Default [`bool`] type used in request/response types, `false`. +#[inline] +pub(crate) const fn default_false() -> bool { + false +} + +/// Default [`bool`] type used in _some_ request/response types, `true`. +#[inline] +pub(crate) const fn default_true() -> bool { + true +} + +/// Default `Cow<'static, str` type used in request/response types. +#[inline] +pub(crate) const fn default_cow_str() -> Cow<'static, str> { + Cow::Borrowed("") +} + +/// Default [`String`] type used in request/response types. +#[inline] +pub(crate) const fn default_string() -> String { + String::new() +} + +/// Default block height used in request/response types. +#[inline] +pub(crate) const fn default_height() -> u64 { + 0 +} + +/// Default [`Vec`] used in request/response types. +#[inline] +pub(crate) const fn default_vec() -> Vec { + Vec::new() +} + +/// Default `0` value used in request/response types. +#[inline] +pub(crate) fn default_zero>() -> T { + T::from(0) +} + +/// Default `1` value used in request/response types. +#[inline] +pub(crate) fn default_one>() -> T { + T::from(1) +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + use super::*; + + /// Tests that [`default_zero`] returns `0` on all unsigned numbers. + #[test] + fn zero() { + assert_eq!(default_zero::(), 0); + assert_eq!(default_zero::(), 0); + assert_eq!(default_zero::(), 0); + assert_eq!(default_zero::(), 0); + assert_eq!(default_zero::(), 0); + } +} diff --git a/rpc/types/src/free.rs b/rpc/types/src/free.rs new file mode 100644 index 00000000..043a5209 --- /dev/null +++ b/rpc/types/src/free.rs @@ -0,0 +1,18 @@ +//! Free functions. + +//---------------------------------------------------------------------------------------------------- Serde +// These are functions used for conditionally (de)serialization. + +/// Returns `true` if the input `u` is equal to `0`. +#[inline] +#[allow(clippy::trivially_copy_pass_by_ref)] // serde needs `&` +pub(crate) const fn is_zero(u: &u64) -> bool { + *u == 0 +} + +/// Returns `true` the input `u` is equal to `1`. +#[inline] +#[allow(clippy::trivially_copy_pass_by_ref)] // serde needs `&` +pub(crate) const fn is_one(u: &u64) -> bool { + *u == 1 +} diff --git a/rpc/types/src/json.rs b/rpc/types/src/json.rs index 5f5f8ff7..dd2e6483 100644 --- a/rpc/types/src/json.rs +++ b/rpc/types/src/json.rs @@ -1,14 +1,83 @@ //! JSON types from the [`/json_rpc`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#json-rpc-methods) endpoint. //! -//! . +//! All types are originally defined in [`rpc/core_rpc_server_commands_defs.h`](https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server_commands_defs.h). //---------------------------------------------------------------------------------------------------- Import use crate::{ - base::{EmptyRequestBase, EmptyResponseBase, ResponseBase}, + base::{AccessResponseBase, ResponseBase}, + defaults::{ + default_false, default_height, default_one, default_string, default_true, default_vec, + default_zero, + }, + free::{is_one, is_zero}, macros::define_request_and_response, + misc::{ + AuxPow, BlockHeader, ChainInfo, ConnectionInfo, Distribution, GetBan, + GetMinerDataTxBacklogEntry, HardforkEntry, HistogramEntry, OutputDistributionData, SetBan, + Span, Status, SyncInfoPeer, TxBacklogEntry, + }, }; -//---------------------------------------------------------------------------------------------------- Struct definitions +//---------------------------------------------------------------------------------------------------- Macro +/// Adds a (de)serialization doc-test to a type in `json.rs`. +/// +/// It expects a const string from `cuprate_test_utils::rpc::data` +/// and the expected value it should (de)serialize into/from. +/// +/// It tests that the provided const JSON string can properly +/// (de)serialize into the expected value. +/// +/// See below for example usage. This macro is only used in this file. +macro_rules! serde_doc_test { + ( + // `const` string from `cuprate_test_utils::rpc::data` + // v + $cuprate_test_utils_rpc_const:ident => $expected:expr + // ^ + // Expected value as an expression + ) => { + paste::paste! { + concat!( + "```rust\n", + "use cuprate_test_utils::rpc::data::json::*;\n", + "use cuprate_rpc_types::{misc::*, base::*, json::*};\n", + "use serde_json::{Value, from_str, from_value};\n", + "\n", + "// The expected data.\n", + "let expected = ", + stringify!($expected), + ";\n", + "\n", + "// Assert it can be turned into a JSON value.\n", + "let value = from_str::(", + stringify!($cuprate_test_utils_rpc_const), + ").unwrap();\n", + "let Value::Object(map) = value else {\n", + " panic!();\n", + "};\n", + "\n", + "// If a request...\n", + "if let Some(params) = map.get(\"params\") {\n", + " let response = from_value::<", + stringify!([<$cuprate_test_utils_rpc_const:camel>]), + ">(params.clone()).unwrap();\n", + " assert_eq!(response, expected);\n", + " return;\n", + "}\n", + "\n", + "// Else, if a response...\n", + "let result = map.get(\"result\").unwrap().clone();\n", + "let response = from_value::<", + stringify!([<$cuprate_test_utils_rpc_const:camel>]), + ">(result.clone()).unwrap();\n", + "assert_eq!(response, expected);\n", + "```\n", + ) + } + }; +} + +//---------------------------------------------------------------------------------------------------- Definitions // This generates 2 structs: // // - `GetBlockTemplateRequest` @@ -26,38 +95,99 @@ define_request_and_response! { // The base type name. GetBlockTemplate, - // The base request type. + // The request type. // - // This must be a type found in [`crate::base`]. + // If `Request {/* fields */}` is provided, a struct is generate as-is. + // + // If `Request {}` is specified here, it will create a `pub type YOUR_REQUEST_TYPE = ()` + // instead of a `struct`, see below in other macro definitions for an example. + // + // If there are any additional attributes (`/// docs` or `#[derive]`s) + // for the struct, they go here, e.g.: + // + #[doc = serde_doc_test!( + // ^ This is a macro that adds a doc-test to this type. + // It is optional but it is added to nearly all types. + // The syntax is: + // `$const` => `$expected` + // where `$const` is a `const` string from + // `cuprate_test_utils::rpc::data` and `$expected` is an + // actual expression that the string _should_ (de)serialize into/from. + GET_BLOCK_TEMPLATE_REQUEST => GetBlockTemplateRequest { + extra_nonce: String::default(), + prev_block: String::default(), + reserve_size: 60, + wallet_address: "44GBHzv6ZyQdJkjqZje6KLZ3xSyN1hBSFAnLP6EAqJtCRVzMzZmeXTC2AHKDS9aEDTRKmo6a6o9r9j86pYfhCWDkKjbtcns".into(), + } + )] + Request { + // Within the `{}` is an infinite matching pattern of: + // ``` + // $ATTRIBUTES + // $FIELD_NAME: $FIELD_TYPE, + // ``` + // The struct generated and all fields are `pub`. + + // This optional expression can be placed after + // a `field: field_type`. this indicates to the + // macro to (de)serialize this field using this + // default expression if it doesn't exist in epee. + // + // See `cuprate_epee_encoding::epee_object` for info. + // + // The default function must be specified twice: + // + // 1. As an expression + // 2. As a string literal + // + // For example: `extra_nonce: String /* = default_string(), "default_string" */,` + // + // This is a HACK since `serde`'s default attribute only takes in + // string literals and macros (stringify) within attributes do not work. + extra_nonce: String = default_string(), "default_string", + prev_block: String = default_string(), "default_string", + + // Another optional expression: + // This indicates to the macro to (de)serialize + // this field as another type in epee. + // + // See `cuprate_epee_encoding::epee_object` for info. + reserve_size: u64 /* as Type */, + + wallet_address: String, + }, + + // The response type. + // + // If `Response {/* fields */}` is used, + // this will generate a struct as-is. + // + // If a type found in [`crate::base`] is used, // It acts as a "base" that gets flattened into - // the actually request type. + // the actual request type. // // "Flatten" means the field(s) of a struct gets inlined // directly into the struct during (de)serialization, see: // . - // - // For example here, we're using [`crate::base::EmptyRequestBase`], - // which means that there is no extra fields flattened. - // - // If a request is not specified here, it will create a `type alias YOUR_REQUEST_TYPE = ()` - // instead of a `struct`, see below in other macro definitions for an example. - EmptyRequestBase { - reserve_size: u64, - wallet_address: String, - prev_block: String, - extra_nonce: String, - }, - - // The base response type. - // - // This is the same as the request base type, - // it must be a type found in [`crate::base`]. - // - // If there are any additional attributes (`/// docs` or `#[derive]`s) - // for the struct, they go here, e.g.: - // #[derive(Copy)] + #[doc = serde_doc_test!( + GET_BLOCK_TEMPLATE_RESPONSE => GetBlockTemplateResponse { + base: ResponseBase::ok(), + blockhashing_blob: "1010f4bae0b4069d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a00000000e0c20372be23d356347091025c5b5e8f2abf83ab618378565cce2b703491523401".into(), + blocktemplate_blob: "1010f4bae0b4069d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a0000000002c681c30101ff8a81c3010180e0a596bb11033b7eedf47baf878f3490cb20b696079c34bd017fe59b0d070e74d73ffabc4bb0e05f011decb630f3148d0163b3bd39690dde4078e4cfb69fecf020d6278a27bad10c58023c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".into(), + difficulty_top64: 0, + difficulty: 283305047039, + expected_reward: 600000000000, + height: 3195018, + next_seed_hash: "".into(), + prev_hash: "9d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a".into(), + reserved_offset: 131, + seed_hash: "e2aa0b7b55042cd48b02e395d78fa66a29815ccc1584e38db2d1f0e8485cd44f".into(), + seed_height: 3194880, + wide_difficulty: "0x41f64bf3ff".into(), + } + )] ResponseBase { - // This is using `crate::base::ResponseBase`, + // This is using [`crate::base::ResponseBase`], // so the type we generate will contain this field: // ``` // base: crate::base::ResponseBase, @@ -69,25 +199,18 @@ define_request_and_response! { // status: crate::Status, // untrusted: bool, // ``` - - // Within the `{}` is an infinite matching pattern of: - // ``` - // $ATTRIBUTES - // $FIELD_NAME: $FIELD_TYPE, - // ``` - // The struct generated and all fields are `pub`. - difficulty: u64, - wide_difficulty: String, - difficulty_top64: u64, - height: u64, - reserved_offset: u64, - expected_reward: u64, - prev_hash: String, - seed_height: u64, - seed_hash: String, - next_seed_hash: String, - blocktemplate_blob: String, blockhashing_blob: String, + blocktemplate_blob: String, + difficulty_top64: u64, + difficulty: u64, + expected_reward: u64, + height: u64, + next_seed_hash: String, + prev_hash: String, + reserved_offset: u64, + seed_hash: String, + seed_height: u64, + wide_difficulty: String, } } @@ -97,10 +220,17 @@ define_request_and_response! { core_rpc_server_commands_defs.h => 919..=933, GetBlockCount, - // There is no request type specified, + // There are no request fields specified, // this will cause the macro to generate a // type alias to `()` instead of a `struct`. + Request {}, + #[doc = serde_doc_test!( + GET_BLOCK_COUNT_RESPONSE => GetBlockCountResponse { + base: ResponseBase::ok(), + count: 3195019, + } + )] ResponseBase { count: u64, } @@ -110,18 +240,1300 @@ define_request_and_response! { on_get_block_hash, cc73fe71162d564ffda8e549b79a350bca53c454 => core_rpc_server_commands_defs.h => 935..=939, + OnGetBlockHash, + + #[doc = serde_doc_test!( + ON_GET_BLOCK_HASH_REQUEST => OnGetBlockHashRequest { + block_height: [912345], + } + )] + #[cfg_attr(feature = "serde", serde(transparent))] + #[repr(transparent)] #[derive(Copy)] - EmptyRequestBase { - #[serde(flatten)] - block_height: u64, + Request { + // This is `std::vector` in `monerod` but + // it must be a 1 length array or else it will error. + block_height: [u64; 1], }, - EmptyResponseBase { - #[serde(flatten)] + + #[doc = serde_doc_test!( + ON_GET_BLOCK_HASH_RESPONSE => OnGetBlockHashResponse { + block_hash: "e22cf75f39ae720e8b71b3d120a5ac03f0db50bba6379e2850975b4859190bc6".into(), + } + )] + #[cfg_attr(feature = "serde", serde(transparent))] + #[repr(transparent)] + Response { block_hash: String, } } +define_request_and_response! { + submit_block, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1114..=1128, + + SubmitBlock, + + #[doc = serde_doc_test!( + SUBMIT_BLOCK_REQUEST => SubmitBlockRequest { + block_blob: ["0707e6bdfedc053771512f1bc27c62731ae9e8f2443db64ce742f4e57f5cf8d393de28551e441a0000000002fb830a01ffbf830a018cfe88bee283060274c0aae2ef5730e680308d9c00b6da59187ad0352efe3c71d36eeeb28782f29f2501bd56b952c3ddc3e350c2631d3a5086cac172c56893831228b17de296ff4669de020200000000".into()], + } + )] + #[cfg_attr(feature = "serde", serde(transparent))] + #[repr(transparent)] + Request { + // This is `std::vector` in `monerod` but + // it must be a 1 length array or else it will error. + block_blob: [String; 1], + }, + + // FIXME: `cuprate_test_utils` only has an `error` response for this. + ResponseBase { + block_id: String, + } +} + +define_request_and_response! { + generateblocks, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1130..=1161, + + GenerateBlocks, + + #[doc = serde_doc_test!( + GENERATE_BLOCKS_REQUEST => GenerateBlocksRequest { + amount_of_blocks: 1, + prev_block: String::default(), + wallet_address: "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A".into(), + starting_nonce: 0 + } + )] + Request { + amount_of_blocks: u64, + prev_block: String = default_string(), "default_string", + starting_nonce: u32, + wallet_address: String, + }, + + #[doc = serde_doc_test!( + GENERATE_BLOCKS_RESPONSE => GenerateBlocksResponse { + base: ResponseBase::ok(), + blocks: vec!["49b712db7760e3728586f8434ee8bc8d7b3d410dac6bb6e98bf5845c83b917e4".into()], + height: 9783, + } + )] + ResponseBase { + blocks: Vec, + height: u64, + } +} + +define_request_and_response! { + get_last_block_header, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1214..=1238, + + GetLastBlockHeader, + + #[derive(Copy)] + Request { + fill_pow_hash: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + GET_LAST_BLOCK_HEADER_RESPONSE => GetLastBlockHeaderResponse { + base: AccessResponseBase::ok(), + block_header: BlockHeader { + block_size: 200419, + block_weight: 200419, + cumulative_difficulty: 366125734645190820, + cumulative_difficulty_top64: 0, + depth: 0, + difficulty: 282052561854, + difficulty_top64: 0, + hash: "57238217820195ac4c08637a144a885491da167899cf1d20e8e7ce0ae0a3434e".into(), + height: 3195020, + long_term_weight: 200419, + major_version: 16, + miner_tx_hash: "7a42667237d4f79891bb407c49c712a9299fb87fce799833a7b633a3a9377dbd".into(), + minor_version: 16, + nonce: 1885649739, + num_txes: 37, + orphan_status: false, + pow_hash: "".into(), + prev_hash: "22c72248ae9c5a2863c94735d710a3525c499f70707d1c2f395169bc5c8a0da3".into(), + reward: 615702960000, + timestamp: 1721245548, + wide_cumulative_difficulty: "0x514bd6a74a7d0a4".into(), + wide_difficulty: "0x41aba48bbe".into() + } + } + )] + AccessResponseBase { + block_header: BlockHeader, + } +} + +define_request_and_response! { + get_block_header_by_hash, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1240..=1269, + GetBlockHeaderByHash, + #[doc = serde_doc_test!( + GET_BLOCK_HEADER_BY_HASH_REQUEST => GetBlockHeaderByHashRequest { + hash: "e22cf75f39ae720e8b71b3d120a5ac03f0db50bba6379e2850975b4859190bc6".into(), + hashes: vec![], + fill_pow_hash: false, + } + )] + Request { + hash: String, + hashes: Vec = default_vec::(), "default_vec", + fill_pow_hash: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + GET_BLOCK_HEADER_BY_HASH_RESPONSE => GetBlockHeaderByHashResponse { + base: AccessResponseBase::ok(), + block_headers: vec![], + block_header: BlockHeader { + block_size: 210, + block_weight: 210, + cumulative_difficulty: 754734824984346, + cumulative_difficulty_top64: 0, + depth: 2282676, + difficulty: 815625611, + difficulty_top64: 0, + hash: "e22cf75f39ae720e8b71b3d120a5ac03f0db50bba6379e2850975b4859190bc6".into(), + height: 912345, + long_term_weight: 210, + major_version: 1, + miner_tx_hash: "c7da3965f25c19b8eb7dd8db48dcd4e7c885e2491db77e289f0609bf8e08ec30".into(), + minor_version: 2, + nonce: 1646, + num_txes: 0, + orphan_status: false, + pow_hash: "".into(), + prev_hash: "b61c58b2e0be53fad5ef9d9731a55e8a81d972b8d90ed07c04fd37ca6403ff78".into(), + reward: 7388968946286, + timestamp: 1452793716, + wide_cumulative_difficulty: "0x2ae6d65248f1a".into(), + wide_difficulty: "0x309d758b".into() + }, + } + )] + AccessResponseBase { + block_header: BlockHeader, + block_headers: Vec = default_vec::(), "default_vec", + } +} + +define_request_and_response! { + get_block_header_by_height, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1271..=1296, + + GetBlockHeaderByHeight, + + #[derive(Copy)] + #[doc = serde_doc_test!( + GET_BLOCK_HEADER_BY_HEIGHT_REQUEST => GetBlockHeaderByHeightRequest { + height: 912345, + fill_pow_hash: false, + } + )] + Request { + height: u64, + fill_pow_hash: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + GET_BLOCK_HEADER_BY_HEIGHT_RESPONSE => GetBlockHeaderByHeightResponse { + base: AccessResponseBase::ok(), + block_header: BlockHeader { + block_size: 210, + block_weight: 210, + cumulative_difficulty: 754734824984346, + cumulative_difficulty_top64: 0, + depth: 2282677, + difficulty: 815625611, + difficulty_top64: 0, + hash: "e22cf75f39ae720e8b71b3d120a5ac03f0db50bba6379e2850975b4859190bc6".into(), + height: 912345, + long_term_weight: 210, + major_version: 1, + miner_tx_hash: "c7da3965f25c19b8eb7dd8db48dcd4e7c885e2491db77e289f0609bf8e08ec30".into(), + minor_version: 2, + nonce: 1646, + num_txes: 0, + orphan_status: false, + pow_hash: "".into(), + prev_hash: "b61c58b2e0be53fad5ef9d9731a55e8a81d972b8d90ed07c04fd37ca6403ff78".into(), + reward: 7388968946286, + timestamp: 1452793716, + wide_cumulative_difficulty: "0x2ae6d65248f1a".into(), + wide_difficulty: "0x309d758b".into() + }, + } + )] + AccessResponseBase { + block_header: BlockHeader, + } +} + +define_request_and_response! { + get_block_headers_range, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1756..=1783, + + GetBlockHeadersRange, + + #[derive(Copy)] + #[doc = serde_doc_test!( + GET_BLOCK_HEADERS_RANGE_REQUEST => GetBlockHeadersRangeRequest { + start_height: 1545999, + end_height: 1546000, + fill_pow_hash: false, + } + )] + Request { + start_height: u64, + end_height: u64, + fill_pow_hash: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + GET_BLOCK_HEADERS_RANGE_RESPONSE => GetBlockHeadersRangeResponse { + base: AccessResponseBase::ok(), + headers: vec![ + BlockHeader { + block_size: 301413, + block_weight: 301413, + cumulative_difficulty: 13185267971483472, + cumulative_difficulty_top64: 0, + depth: 1649024, + difficulty: 134636057921, + difficulty_top64: 0, + hash: "86d1d20a40cefcf3dd410ff6967e0491613b77bf73ea8f1bf2e335cf9cf7d57a".into(), + height: 1545999, + long_term_weight: 301413, + major_version: 6, + miner_tx_hash: "9909c6f8a5267f043c3b2b079fb4eacc49ef9c1dee1c028eeb1a259b95e6e1d9".into(), + minor_version: 6, + nonce: 3246403956, + num_txes: 20, + orphan_status: false, + pow_hash: "".into(), + prev_hash: "0ef6e948f77b8f8806621003f5de24b1bcbea150bc0e376835aea099674a5db5".into(), + reward: 5025593029981, + timestamp: 1523002893, + wide_cumulative_difficulty: "0x2ed7ee6db56750".into(), + wide_difficulty: "0x1f58ef3541".into() + }, + BlockHeader { + block_size: 13322, + block_weight: 13322, + cumulative_difficulty: 13185402687569710, + cumulative_difficulty_top64: 0, + depth: 1649023, + difficulty: 134716086238, + difficulty_top64: 0, + hash: "b408bf4cfcd7de13e7e370c84b8314c85b24f0ba4093ca1d6eeb30b35e34e91a".into(), + height: 1546000, + long_term_weight: 13322, + major_version: 7, + miner_tx_hash: "7f749c7c64acb35ef427c7454c45e6688781fbead9bbf222cb12ad1a96a4e8f6".into(), + minor_version: 7, + nonce: 3737164176, + num_txes: 1, + orphan_status: false, + pow_hash: "".into(), + prev_hash: "86d1d20a40cefcf3dd410ff6967e0491613b77bf73ea8f1bf2e335cf9cf7d57a".into(), + reward: 4851952181070, + timestamp: 1523002931, + wide_cumulative_difficulty: "0x2ed80dcb69bf2e".into(), + wide_difficulty: "0x1f5db457de".into() + } + ], + } + )] + AccessResponseBase { + headers: Vec, + } +} + +define_request_and_response! { + get_block, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1298..=1313, + GetBlock, + + #[doc = serde_doc_test!( + GET_BLOCK_REQUEST => GetBlockRequest { + height: 2751506, + hash: String::default(), + fill_pow_hash: false, + } + )] + Request { + // `monerod` has both `hash` and `height` fields. + // In the RPC handler, if `hash.is_empty()`, it will use it, else, it uses `height`. + // + hash: String = default_string(), "default_string", + height: u64 = default_height(), "default_height", + fill_pow_hash: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + GET_BLOCK_RESPONSE => GetBlockResponse { + base: AccessResponseBase::ok(), + blob: "1010c58bab9b06b27bdecfc6cd0a46172d136c08831cf67660377ba992332363228b1b722781e7807e07f502cef8a70101ff92f8a7010180e0a596bb1103d7cbf826b665d7a532c316982dc8dbc24f285cbc18bbcc27c7164cd9b3277a85d034019f629d8b36bd16a2bfce3ea80c31dc4d8762c67165aec21845494e32b7582fe00211000000297a787a000000000000000000000000".into(), + block_header: BlockHeader { + block_size: 106, + block_weight: 106, + cumulative_difficulty: 236046001376524168, + cumulative_difficulty_top64: 0, + depth: 443517, + difficulty: 313732272488, + difficulty_top64: 0, + hash: "43bd1f2b6556dcafa413d8372974af59e4e8f37dbf74dc6b2a9b7212d0577428".into(), + height: 2751506, + long_term_weight: 176470, + major_version: 16, + miner_tx_hash: "e49b854c5f339d7410a77f2a137281d8042a0ffc7ef9ab24cd670b67139b24cd".into(), + minor_version: 16, + nonce: 4110909056, + num_txes: 0, + orphan_status: false, + pow_hash: "".into(), + prev_hash: "b27bdecfc6cd0a46172d136c08831cf67660377ba992332363228b1b722781e7".into(), + reward: 600000000000, + timestamp: 1667941829, + wide_cumulative_difficulty: "0x3469a966eb2f788".into(), + wide_difficulty: "0x490be69168".into() + }, + json: "{\n \"major_version\": 16, \n \"minor_version\": 16, \n \"timestamp\": 1667941829, \n \"prev_id\": \"b27bdecfc6cd0a46172d136c08831cf67660377ba992332363228b1b722781e7\", \n \"nonce\": 4110909056, \n \"miner_tx\": {\n \"version\": 2, \n \"unlock_time\": 2751566, \n \"vin\": [ {\n \"gen\": {\n \"height\": 2751506\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 600000000000, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"d7cbf826b665d7a532c316982dc8dbc24f285cbc18bbcc27c7164cd9b3277a85\", \n \"view_tag\": \"d0\"\n }\n }\n }\n ], \n \"extra\": [ 1, 159, 98, 157, 139, 54, 189, 22, 162, 191, 206, 62, 168, 12, 49, 220, 77, 135, 98, 198, 113, 101, 174, 194, 24, 69, 73, 78, 50, 183, 88, 47, 224, 2, 17, 0, 0, 0, 41, 122, 120, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n ], \n \"rct_signatures\": {\n \"type\": 0\n }\n }, \n \"tx_hashes\": [ ]\n}".into(), + miner_tx_hash: "e49b854c5f339d7410a77f2a137281d8042a0ffc7ef9ab24cd670b67139b24cd".into(), + tx_hashes: vec![], + } + )] + AccessResponseBase { + blob: String, + block_header: BlockHeader, + json: String, // FIXME: this should be defined in a struct, it has many fields. + miner_tx_hash: String, + tx_hashes: Vec = default_vec::(), "default_vec", + } +} + +define_request_and_response! { + get_connections, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1734..=1754, + + GetConnections, + + Request {}, + + #[doc = serde_doc_test!( + GET_CONNECTIONS_RESPONSE => GetConnectionsResponse { + base: ResponseBase::ok(), + connections: vec![ + ConnectionInfo { + address: "3evk3kezfjg44ma6tvesy7rbxwwpgpympj45xar5fo4qajrsmkoaqdqd.onion:18083".into(), + address_type: 4, + avg_download: 0, + avg_upload: 0, + connection_id: "22ef856d0f1d44cc95e84fecfd065fe2".into(), + current_download: 0, + current_upload: 0, + height: 3195026, + host: "3evk3kezfjg44ma6tvesy7rbxwwpgpympj45xar5fo4qajrsmkoaqdqd.onion".into(), + incoming: false, + ip: "".into(), + live_time: 76651, + local_ip: false, + localhost: false, + peer_id: "0000000000000001".into(), + port: "".into(), + pruning_seed: 0, + recv_count: 240328, + recv_idle_time: 34, + rpc_credits_per_hash: 0, + rpc_port: 0, + send_count: 3406572, + send_idle_time: 30, + state: "normal".into(), + support_flags: 0 + }, + ConnectionInfo { + address: "4iykytmumafy5kjahdqc7uzgcs34s2vwsadfjpk4znvsa5vmcxeup2qd.onion:18083".into(), + address_type: 4, + avg_download: 0, + avg_upload: 0, + connection_id: "c7734e15936f485a86d2b0534f87e499".into(), + current_download: 0, + current_upload: 0, + height: 3195024, + host: "4iykytmumafy5kjahdqc7uzgcs34s2vwsadfjpk4znvsa5vmcxeup2qd.onion".into(), + incoming: false, + ip: "".into(), + live_time: 76755, + local_ip: false, + localhost: false, + peer_id: "0000000000000001".into(), + port: "".into(), + pruning_seed: 389, + recv_count: 237657, + recv_idle_time: 120, + rpc_credits_per_hash: 0, + rpc_port: 0, + send_count: 3370566, + send_idle_time: 120, + state: "normal".into(), + support_flags: 0 + } + ], + } + )] + ResponseBase { + // FIXME: This is a `std::list` in `monerod` because...? + connections: Vec, + } +} + +define_request_and_response! { + get_info, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 693..=789, + GetInfo, + Request {}, + + #[doc = serde_doc_test!( + GET_INFO_RESPONSE => GetInfoResponse { + base: AccessResponseBase::ok(), + adjusted_time: 1721245289, + alt_blocks_count: 16, + block_size_limit: 600000, + block_size_median: 300000, + block_weight_limit: 600000, + block_weight_median: 300000, + bootstrap_daemon_address: "".into(), + busy_syncing: false, + cumulative_difficulty: 366127702242611947, + cumulative_difficulty_top64: 0, + database_size: 235169075200, + difficulty: 280716748706, + difficulty_top64: 0, + free_space: 30521749504, + grey_peerlist_size: 4996, + height: 3195028, + height_without_bootstrap: 3195028, + incoming_connections_count: 62, + mainnet: true, + nettype: "mainnet".into(), + offline: false, + outgoing_connections_count: 1143, + restricted: false, + rpc_connections_count: 1, + stagenet: false, + start_time: 1720462427, + synchronized: true, + target: 120, + target_height: 0, + testnet: false, + top_block_hash: "bdf06d18ed1931a8ee62654e9b6478cc459bc7072628b8e36f4524d339552946".into(), + tx_count: 43205750, + tx_pool_size: 12, + update_available: false, + version: "0.18.3.3-release".into(), + was_bootstrap_ever_used: false, + white_peerlist_size: 1000, + wide_cumulative_difficulty: "0x514bf349299d2eb".into(), + wide_difficulty: "0x415c05a7a2".into() + } + )] + AccessResponseBase { + adjusted_time: u64, + alt_blocks_count: u64, + block_size_limit: u64, + block_size_median: u64, + block_weight_limit: u64, + block_weight_median: u64, + bootstrap_daemon_address: String, + busy_syncing: bool, + cumulative_difficulty_top64: u64, + cumulative_difficulty: u64, + database_size: u64, + difficulty_top64: u64, + difficulty: u64, + free_space: u64, + grey_peerlist_size: u64, + height: u64, + height_without_bootstrap: u64, + incoming_connections_count: u64, + mainnet: bool, + nettype: String, + offline: bool, + outgoing_connections_count: u64, + restricted: bool, + rpc_connections_count: u64, + stagenet: bool, + start_time: u64, + synchronized: bool, + target_height: u64, + target: u64, + testnet: bool, + top_block_hash: String, + tx_count: u64, + tx_pool_size: u64, + update_available: bool, + version: String, + was_bootstrap_ever_used: bool, + white_peerlist_size: u64, + wide_cumulative_difficulty: String, + wide_difficulty: String, + } +} + +define_request_and_response! { + hard_fork_info, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1958..=1995, + HardForkInfo, + Request {}, + + #[doc = serde_doc_test!( + HARD_FORK_INFO_RESPONSE => HardForkInfoResponse { + base: AccessResponseBase::ok(), + earliest_height: 2689608, + enabled: true, + state: 0, + threshold: 0, + version: 16, + votes: 10080, + voting: 16, + window: 10080 + } + )] + AccessResponseBase { + earliest_height: u64, + enabled: bool, + state: u32, + threshold: u32, + version: u8, + votes: u32, + voting: u8, + window: u32, + } +} + +define_request_and_response! { + set_bans, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2032..=2067, + SetBans, + + #[doc = serde_doc_test!( + SET_BANS_REQUEST => SetBansRequest { + bans: vec![ SetBan { + host: "192.168.1.51".into(), + ip: 0, + ban: true, + seconds: 30 + }] + } + )] + Request { + bans: Vec, + }, + + #[doc = serde_doc_test!( + SET_BANS_RESPONSE => SetBansResponse { + base: ResponseBase::ok(), + } + )] + ResponseBase {} +} + +define_request_and_response! { + get_bans, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1997..=2030, + GetBans, + Request {}, + + #[doc = serde_doc_test!( + GET_BANS_RESPONSE => GetBansResponse { + base: ResponseBase::ok(), + bans: vec![ + GetBan { + host: "104.248.206.131".into(), + ip: 2211379304, + seconds: 689754 + }, + GetBan { + host: "209.222.252.0/24".into(), + ip: 0, + seconds: 689754 + } + ] + } + )] + ResponseBase { + bans: Vec, + } +} + +define_request_and_response! { + banned, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2069..=2094, + Banned, + + #[doc = serde_doc_test!( + BANNED_REQUEST => BannedRequest { + address: "95.216.203.255".into(), + } + )] + Request { + address: String, + }, + + #[doc = serde_doc_test!( + BANNED_RESPONSE => BannedResponse { + banned: true, + seconds: 689655, + status: Status::Ok, + } + )] + Response { + banned: bool, + seconds: u32, + status: Status, + } +} + +define_request_and_response! { + flush_txpool, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2096..=2116, + FlushTransactionPool, + + #[doc = serde_doc_test!( + FLUSH_TRANSACTION_POOL_REQUEST => FlushTransactionPoolRequest { + txids: vec!["dc16fa8eaffe1484ca9014ea050e13131d3acf23b419f33bb4cc0b32b6c49308".into()], + } + )] + Request { + txids: Vec = default_vec::(), "default_vec", + }, + + #[doc = serde_doc_test!( + FLUSH_TRANSACTION_POOL_RESPONSE => FlushTransactionPoolResponse { + status: Status::Ok, + } + )] + #[repr(transparent)] + Response { + status: Status, + } +} + +define_request_and_response! { + get_output_histogram, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2118..=2168, + GetOutputHistogram, + + #[doc = serde_doc_test!( + GET_OUTPUT_HISTOGRAM_REQUEST => GetOutputHistogramRequest { + amounts: vec![20000000000], + min_count: 0, + max_count: 0, + unlocked: false, + recent_cutoff: 0, + } + )] + Request { + amounts: Vec, + min_count: u64 = default_zero::(), "default_zero", + max_count: u64 = default_zero::(), "default_zero", + unlocked: bool = default_false(), "default_false", + recent_cutoff: u64 = default_zero::(), "default_zero", + }, + + #[doc = serde_doc_test!( + GET_OUTPUT_HISTOGRAM_RESPONSE => GetOutputHistogramResponse { + base: AccessResponseBase::ok(), + histogram: vec![HistogramEntry { + amount: 20000000000, + recent_instances: 0, + total_instances: 381490, + unlocked_instances: 0 + }] + } + )] + AccessResponseBase { + histogram: Vec, + } +} + +define_request_and_response! { + get_coinbase_tx_sum, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2213..=2248, + + GetCoinbaseTxSum, + + #[doc = serde_doc_test!( + GET_COINBASE_TX_SUM_REQUEST => GetCoinbaseTxSumRequest { + height: 1563078, + count: 2 + } + )] + Request { + height: u64, + count: u64, + }, + + #[doc = serde_doc_test!( + GET_COINBASE_TX_SUM_RESPONSE => GetCoinbaseTxSumResponse { + base: AccessResponseBase::ok(), + emission_amount: 9387854817320, + emission_amount_top64: 0, + fee_amount: 83981380000, + fee_amount_top64: 0, + wide_emission_amount: "0x889c7c06828".into(), + wide_fee_amount: "0x138dae29a0".into() + } + )] + AccessResponseBase { + emission_amount: u64, + emission_amount_top64: u64, + fee_amount: u64, + fee_amount_top64: u64, + wide_emission_amount: String, + wide_fee_amount: String, + } +} + +define_request_and_response! { + get_version, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2170..=2211, + + GetVersion, + Request {}, + + #[doc = serde_doc_test!( + GET_VERSION_RESPONSE => GetVersionResponse { + base: ResponseBase::ok(), + current_height: 3195051, + hard_forks: vec![ + HardforkEntry { + height: 1, + hf_version: 1 + }, + HardforkEntry { + height: 1009827, + hf_version: 2 + }, + HardforkEntry { + height: 1141317, + hf_version: 3 + }, + HardforkEntry { + height: 1220516, + hf_version: 4 + }, + HardforkEntry { + height: 1288616, + hf_version: 5 + }, + HardforkEntry { + height: 1400000, + hf_version: 6 + }, + HardforkEntry { + height: 1546000, + hf_version: 7 + }, + HardforkEntry { + height: 1685555, + hf_version: 8 + }, + HardforkEntry { + height: 1686275, + hf_version: 9 + }, + HardforkEntry { + height: 1788000, + hf_version: 10 + }, + HardforkEntry { + height: 1788720, + hf_version: 11 + }, + HardforkEntry { + height: 1978433, + hf_version: 12 + }, + HardforkEntry { + height: 2210000, + hf_version: 13 + }, + HardforkEntry { + height: 2210720, + hf_version: 14 + }, + HardforkEntry { + height: 2688888, + hf_version: 15 + }, + HardforkEntry { + height: 2689608, + hf_version: 16 + } + ], + release: true, + version: 196621, + target_height: 0, + } + )] + ResponseBase { + version: u32, + release: bool, + current_height: u64 = default_zero::(), "default_zero", + target_height: u64 = default_zero::(), "default_zero", + hard_forks: Vec = default_vec(), "default_vec", + } +} + +define_request_and_response! { + get_fee_estimate, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2250..=2277, + GetFeeEstimate, + Request {}, + + #[doc = serde_doc_test!( + GET_FEE_ESTIMATE_RESPONSE => GetFeeEstimateResponse { + base: AccessResponseBase::ok(), + fee: 20000, + fees: vec![20000,80000,320000,4000000], + quantization_mask: 10000, + } + )] + AccessResponseBase { + fee: u64, + fees: Vec, + quantization_mask: u64 = default_one::(), "default_one", + } +} + +define_request_and_response! { + get_alternate_chains, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2279..=2310, + GetAlternateChains, + Request {}, + + #[doc = serde_doc_test!( + GET_ALTERNATE_CHAINS_RESPONSE => GetAlternateChainsResponse { + base: ResponseBase::ok(), + chains: vec![ + ChainInfo { + block_hash: "4826c7d45d7cf4f02985b5c405b0e5d7f92c8d25e015492ce19aa3b209295dce".into(), + block_hashes: vec!["4826c7d45d7cf4f02985b5c405b0e5d7f92c8d25e015492ce19aa3b209295dce".into()], + difficulty: 357404825113208373, + difficulty_top64: 0, + height: 3167471, + length: 1, + main_chain_parent_block: "69b5075ea627d6ba06b1c30b7e023884eeaef5282cf58ec847dab838ddbcdd86".into(), + wide_difficulty: "0x4f5c1cb79e22635".into(), + }, + ChainInfo { + block_hash: "33ee476f5a1c5b9d889274cbbe171f5e0112df7ed69021918042525485deb401".into(), + block_hashes: vec!["33ee476f5a1c5b9d889274cbbe171f5e0112df7ed69021918042525485deb401".into()], + difficulty: 354736121711617293, + difficulty_top64: 0, + height: 3157465, + length: 1, + main_chain_parent_block: "fd522fcc4cefe5c8c0e5c5600981b3151772c285df3a4e38e5c4011cf466d2cb".into(), + wide_difficulty: "0x4ec469f8b9ee50d".into(), + } + ], + } + )] + ResponseBase { + chains: Vec, + } +} + +define_request_and_response! { + relay_tx, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2361..=2381, + + RelayTx, + + #[doc = serde_doc_test!( + RELAY_TX_REQUEST => RelayTxRequest { + txids: vec!["9fd75c429cbe52da9a52f2ffc5fbd107fe7fd2099c0d8de274dc8a67e0c98613".into()] + } + )] + Request { + txids: Vec, + }, + + #[doc = serde_doc_test!( + RELAY_TX_RESPONSE => RelayTxResponse { + status: Status::Ok, + } + )] + #[repr(transparent)] + Response { + status: Status, + } +} + +define_request_and_response! { + sync_info, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2383..=2443, + + SyncInfo, + Request {}, + + #[doc = serde_doc_test!( + SYNC_INFO_RESPONSE => SyncInfoResponse { + base: AccessResponseBase::ok(), + height: 3195157, + next_needed_pruning_seed: 0, + overview: "[]".into(), + spans: vec![], + peers: vec![ + SyncInfoPeer { + info: ConnectionInfo { + address: "142.93.128.65:44986".into(), + address_type: 1, + avg_download: 1, + avg_upload: 1, + connection_id: "a5803c4c2dac49e7b201dccdef54c862".into(), + current_download: 2, + current_upload: 1, + height: 3195157, + host: "142.93.128.65".into(), + incoming: true, + ip: "142.93.128.65".into(), + live_time: 18, + local_ip: false, + localhost: false, + peer_id: "6830e9764d3e5687".into(), + port: "44986".into(), + pruning_seed: 0, + recv_count: 20340, + recv_idle_time: 0, + rpc_credits_per_hash: 0, + rpc_port: 18089, + send_count: 32235, + send_idle_time: 6, + state: "normal".into(), + support_flags: 1 + } + }, + SyncInfoPeer { + info: ConnectionInfo { + address: "4iykytmumafy5kjahdqc7uzgcs34s2vwsadfjpk4znvsa5vmcxeup2qd.onion:18083".into(), + address_type: 4, + avg_download: 0, + avg_upload: 0, + connection_id: "277f7c821bc546878c8bd29977e780f5".into(), + current_download: 0, + current_upload: 0, + height: 3195157, + host: "4iykytmumafy5kjahdqc7uzgcs34s2vwsadfjpk4znvsa5vmcxeup2qd.onion".into(), + incoming: false, + ip: "".into(), + live_time: 2246, + local_ip: false, + localhost: false, + peer_id: "0000000000000001".into(), + port: "".into(), + pruning_seed: 389, + recv_count: 65164, + recv_idle_time: 15, + rpc_credits_per_hash: 0, + rpc_port: 0, + send_count: 99120, + send_idle_time: 15, + state: "normal".into(), + support_flags: 0 + } + } + ], + target_height: 0, + } + )] + AccessResponseBase { + height: u64, + next_needed_pruning_seed: u32, + overview: String, + // FIXME: This is a `std::list` in `monerod` because...? + peers: Vec = default_vec::(), "default_vec", + // FIXME: This is a `std::list` in `monerod` because...? + spans: Vec = default_vec::(), "default_vec", + target_height: u64, + } +} + +define_request_and_response! { + get_txpool_backlog, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1637..=1664, + GetTransactionPoolBacklog, + Request {}, + + // TODO: enable test after binary string impl. + // #[doc = serde_doc_test!( + // GET_TRANSACTION_POOL_BACKLOG_RESPONSE => GetTransactionPoolBacklogResponse { + // base: ResponseBase::ok(), + // backlog: "...Binary...".into(), + // } + // )] + ResponseBase { + // TODO: this is a [`BinaryString`]. + backlog: Vec, + } +} + +define_request_and_response! { + get_output_distribution, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2445..=2520, + + /// This type is also used in the (undocumented) + /// [`/get_output_distribution.bin`](https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server.h#L138) + /// binary endpoint. + GetOutputDistribution, + + #[doc = serde_doc_test!( + GET_OUTPUT_DISTRIBUTION_REQUEST => GetOutputDistributionRequest { + amounts: vec![628780000], + from_height: 1462078, + binary: true, + compress: false, + cumulative: false, + to_height: 0, + } + )] + Request { + amounts: Vec, + binary: bool = default_true(), "default_true", + compress: bool = default_false(), "default_false", + cumulative: bool = default_false(), "default_false", + from_height: u64 = default_zero::(), "default_zero", + to_height: u64 = default_zero::(), "default_zero", + }, + + // TODO: enable test after binary string impl. + // #[doc = serde_doc_test!( + // GET_OUTPUT_DISTRIBUTION_RESPONSE => GetOutputDistributionResponse { + // base: AccessResponseBase::ok(), + // distributions: vec![Distribution::Uncompressed(DistributionUncompressed { + // start_height: 1462078, + // base: 0, + // distribution: vec![], + // amount: 2628780000, + // binary: true, + // })], + // } + // )] + AccessResponseBase { + distributions: Vec, + } +} + +define_request_and_response! { + get_miner_data, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 996..=1044, + GetMinerData, + Request {}, + + #[doc = serde_doc_test!( + GET_MINER_DATA_RESPONSE => GetMinerDataResponse { + base: ResponseBase::ok(), + already_generated_coins: 18186022843595960691, + difficulty: "0x48afae42de".into(), + height: 2731375, + major_version: 16, + median_weight: 300000, + prev_id: "78d50c5894d187c4946d54410990ca59a75017628174a9e8c7055fa4ca5c7c6d".into(), + seed_hash: "a6b869d50eca3a43ec26fe4c369859cf36ae37ce6ecb76457d31ffeb8a6ca8a6".into(), + tx_backlog: vec![ + GetMinerDataTxBacklogEntry { + fee: 30700000, + id: "9868490d6bb9207fdd9cf17ca1f6c791b92ca97de0365855ea5c089f67c22208".into(), + weight: 1535 + }, + GetMinerDataTxBacklogEntry { + fee: 44280000, + id: "b6000b02bbec71e18ad704bcae09fb6e5ae86d897ced14a718753e76e86c0a0a".into(), + weight: 2214 + }, + ], + } + )] + ResponseBase { + major_version: u8, + height: u64, + prev_id: String, + seed_hash: String, + difficulty: String, + median_weight: u64, + already_generated_coins: u64, + tx_backlog: Vec, + } +} + +define_request_and_response! { + prune_blockchain, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2747..=2772, + + PruneBlockchain, + + #[derive(Copy)] + #[doc = serde_doc_test!( + PRUNE_BLOCKCHAIN_REQUEST => PruneBlockchainRequest { + check: true + } + )] + Request { + check: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + PRUNE_BLOCKCHAIN_RESPONSE => PruneBlockchainResponse { + base: ResponseBase::ok(), + pruned: true, + pruning_seed: 387, + } + )] + ResponseBase { + pruned: bool, + pruning_seed: u32, + } +} + +define_request_and_response! { + calc_pow, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1046..=1066, + + CalcPow, + + #[doc = serde_doc_test!( + CALC_POW_REQUEST => CalcPowRequest { + major_version: 14, + height: 2286447, + block_blob: "0e0ed286da8006ecdc1aab3033cf1716c52f13f9d8ae0051615a2453643de94643b550d543becd0000000002abc78b0101ffefc68b0101fcfcf0d4b422025014bb4a1eade6622fd781cb1063381cad396efa69719b41aa28b4fce8c7ad4b5f019ce1dc670456b24a5e03c2d9058a2df10fec779e2579753b1847b74ee644f16b023c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051399a1bc46a846474f5b33db24eae173a26393b976054ee14f9feefe99925233802867097564c9db7a36af5bb5ed33ab46e63092bd8d32cef121608c3258edd55562812e21cc7e3ac73045745a72f7d74581d9a0849d6f30e8b2923171253e864f4e9ddea3acb5bc755f1c4a878130a70c26297540bc0b7a57affb6b35c1f03d8dbd54ece8457531f8cba15bb74516779c01193e212050423020e45aa2c15dcb".into(), + seed_hash: "d432f499205150873b2572b5f033c9c6e4b7c6f3394bd2dd93822cd7085e7307".into(), + } + )] + Request { + major_version: u8, + height: u64, + block_blob: String, + seed_hash: String, + }, + + #[doc = serde_doc_test!( + CALC_POW_RESPONSE => CalcPowResponse { + pow_hash: "d0402d6834e26fb94a9ce38c6424d27d2069896a9b8b1ce685d79936bca6e0a8".into(), + } + )] + #[cfg_attr(feature = "serde", serde(transparent))] + #[repr(transparent)] + Response { + pow_hash: String, + } +} + +define_request_and_response! { + flush_cache, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2774..=2796, + + FlushCache, + + #[derive(Copy)] + #[doc = serde_doc_test!( + FLUSH_CACHE_REQUEST => FlushCacheRequest { + bad_txs: true, + bad_blocks: true + } + )] + Request { + bad_txs: bool = default_false(), "default_false", + bad_blocks: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + FLUSH_CACHE_RESPONSE => FlushCacheResponse { + base: ResponseBase::ok(), + } + )] + ResponseBase {} +} + +define_request_and_response! { + add_aux_pow, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1068..=1112, + + AddAuxPow, + + #[doc = serde_doc_test!( + ADD_AUX_POW_REQUEST => AddAuxPowRequest { + blocktemplate_blob: "1010f4bae0b4069d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a0000000002c681c30101ff8a81c3010180e0a596bb11033b7eedf47baf878f3490cb20b696079c34bd017fe59b0d070e74d73ffabc4bb0e05f011decb630f3148d0163b3bd39690dde4078e4cfb69fecf020d6278a27bad10c58023c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".into(), + aux_pow: vec![AuxPow { + id: "3200b4ea97c3b2081cd4190b58e49572b2319fed00d030ad51809dff06b5d8c8".into(), + hash: "7b35762de164b20885e15dbe656b1138db06bb402fa1796f5765a23933d8859a".into() + }] + } + )] + Request { + blocktemplate_blob: String, + aux_pow: Vec, + }, + + #[doc = serde_doc_test!( + ADD_AUX_POW_RESPONSE => AddAuxPowResponse { + base: ResponseBase::ok(), + aux_pow: vec![AuxPow { + hash: "7b35762de164b20885e15dbe656b1138db06bb402fa1796f5765a23933d8859a".into(), + id: "3200b4ea97c3b2081cd4190b58e49572b2319fed00d030ad51809dff06b5d8c8".into(), + }], + blockhashing_blob: "1010ee97e2a106e9f8ebe8887e5b609949ac8ea6143e560ed13552b110cb009b21f0cfca1eaccf00000000b2685c1283a646bc9020c758daa443be145b7370ce5a6efacb3e614117032e2c22".into(), + blocktemplate_blob: "1010f4bae0b4069d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a0000000002c681c30101ff8a81c3010180e0a596bb11033b7eedf47baf878f3490cb20b696079c34bd017fe59b0d070e74d73ffabc4bb0e05f011decb630f3148d0163b3bd39690dde4078e4cfb69fecf020d6278a27bad10c58023c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".into(), + merkle_root: "7b35762de164b20885e15dbe656b1138db06bb402fa1796f5765a23933d8859a".into(), + merkle_tree_depth: 0, + } + )] + ResponseBase { + blocktemplate_blob: String, + blockhashing_blob: String, + merkle_root: String, + merkle_tree_depth: u64, + aux_pow: Vec, + } +} + //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] mod test { diff --git a/rpc/types/src/lib.rs b/rpc/types/src/lib.rs index 780208bd..d0d1e00d 100644 --- a/rpc/types/src/lib.rs +++ b/rpc/types/src/lib.rs @@ -1,4 +1,5 @@ #![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_cfg))] //---------------------------------------------------------------------------------------------------- Lints // Forbid lints. // Our code, and code generated (e.g macros) cannot overrule these. @@ -13,7 +14,6 @@ unused_allocation, coherence_leak_check, while_true, - clippy::missing_docs_in_private_items, // Maybe can be put into `#[deny]`. unconditional_recursion, @@ -82,7 +82,15 @@ clippy::option_if_let_else, )] // Allow some lints when running in debug mode. -#![cfg_attr(debug_assertions, allow(clippy::todo, clippy::multiple_crate_versions))] +#![cfg_attr( + debug_assertions, + allow( + clippy::todo, + clippy::multiple_crate_versions, + unused_imports, + unused_variables + ) +)] // Allow some lints in tests. #![cfg_attr( test, @@ -94,23 +102,28 @@ ) )] // TODO: remove me after finishing impl -#![allow(dead_code)] +#![allow( + dead_code, + rustdoc::broken_intra_doc_links // TODO: remove after `{bin,json,other}.rs` gets merged +)] -//---------------------------------------------------------------------------------------------------- Use -mod binary_string; +//---------------------------------------------------------------------------------------------------- Mod mod constants; +mod defaults; +mod free; mod macros; -mod status; -pub use binary_string::BinaryString; -pub use constants::{ - CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, - CORE_RPC_STATUS_PAYMENT_REQUIRED, CORE_RPC_STATUS_UNKNOWN, CORE_RPC_VERSION, - CORE_RPC_VERSION_MAJOR, CORE_RPC_VERSION_MINOR, -}; -pub use status::Status; +#[cfg(feature = "serde")] +mod serde; pub mod base; pub mod bin; pub mod json; +pub mod misc; pub mod other; + +pub use constants::{ + CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, + CORE_RPC_STATUS_PAYMENT_REQUIRED, CORE_RPC_VERSION, CORE_RPC_VERSION_MAJOR, + CORE_RPC_VERSION_MINOR, +}; diff --git a/rpc/types/src/macros.rs b/rpc/types/src/macros.rs index 27288004..fa0d5188 100644 --- a/rpc/types/src/macros.rs +++ b/rpc/types/src/macros.rs @@ -1,14 +1,12 @@ //! Macros. -//---------------------------------------------------------------------------------------------------- Struct definition -/// A template for generating 2 `struct`s with a bunch of information filled out. -/// -/// These are the RPC request and response `struct`s. +//---------------------------------------------------------------------------------------------------- define_request_and_response +/// A template for generating the RPC request and response `struct`s. /// /// These `struct`s automatically implement: /// - `Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash` /// - `serde::{Serialize, Deserialize}` -/// - `epee_encoding::EpeeObject` +/// - `cuprate_epee_encoding::EpeeObject` /// /// It's best to see the output of this macro via the documentation /// of the generated structs via `cargo doc`s to see which parts @@ -17,112 +15,37 @@ /// See the [`crate::json`] module for example usage. /// /// # Macro internals -/// This macro has 2 branches with almost the same output: -/// 1. An empty `Request` type -/// 2. An `Request` type with fields +/// This macro uses: +/// - [`define_request`] +/// - [`define_response`] +/// - [`define_request_and_response_doc`] /// -/// The first branch is the same as the second with the exception -/// that if the caller of this macro provides no fields, it will -/// generate: +/// # `define_request` +/// This macro has 2 branches. If the caller provides +/// `Request {}`, i.e. no fields, it will generate: /// ``` /// pub type Request = (); /// ``` -/// instead of: +/// If they _did_ specify fields, it will generate: /// ``` /// pub struct Request {/* fields */} /// ``` -/// /// This is because having a bunch of types that are all empty structs /// means they are not compatible and it makes it cumbersome for end-users. /// Really, they semantically are empty types, so `()` is used. /// -/// Again, other than this, the 2 branches do (should) not differ. +/// # `define_response` +/// This macro has 2 branches. If the caller provides `Response` +/// it will generate a normal struct with no additional fields. /// -/// FIXME: there's probably a less painful way to branch here on input -/// without having to duplicate 80% of the macro. Sub-macros were attempted -/// but they ended up unreadable. So for now, make sure to fix the other -/// branch as well when making changes. The only de-duplicated part is -/// the doc generation with [`define_request_and_response_doc`]. +/// If the caller provides a base type from [`crate::base`], it will +/// flatten that into the request type automatically. +/// +/// E.g. `Response {/*...*/}` and `ResponseBase {/*...*/}` +/// would trigger the different branches. macro_rules! define_request_and_response { - //------------------------------------------------------------------------------ - // This version of the macro expects a `Request` type with no fields, i.e. `Request {}`. ( - // The markdown tag for Monero RPC documentation. Not necessarily the endpoint. - $monero_daemon_rpc_doc_link:ident, - - // The commit hash and `$file.$extension` in which this type is defined in - // the Monero codebase in the `rpc/` directory, followed by the specific lines. - $monero_code_commit:ident => - $monero_code_filename:ident. - $monero_code_filename_extension:ident => - $monero_code_line_start:literal..= - $monero_code_line_end:literal, - - // The base `struct` name. - $type_name:ident, - - // The response type (and any doc comments, derives, etc). - $( #[$response_type_attr:meta] )* - $response_base_type:ty { - // And any fields. - $( - $( #[$response_field_attr:meta] )* - $response_field:ident: $response_field_type:ty, - )* - } - ) => { paste::paste! { - #[doc = $crate::macros::define_request_and_response_doc!( - "response", - $monero_daemon_rpc_doc_link, - $monero_code_commit, - $monero_code_filename, - $monero_code_filename_extension, - $monero_code_line_start, - $monero_code_line_end, - [<$type_name Request>], - )] - /// - /// This request has no inputs. - pub type [<$type_name Request>] = (); - - #[allow(dead_code)] - #[allow(missing_docs)] - #[derive(serde::Serialize, serde::Deserialize)] - #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] - $( #[$response_type_attr] )* - #[doc = $crate::macros::define_request_and_response_doc!( - "request", - $monero_daemon_rpc_doc_link, - $monero_code_commit, - $monero_code_filename, - $monero_code_filename_extension, - $monero_code_line_start, - $monero_code_line_end, - [<$type_name Response>], - )] - pub struct [<$type_name Response>] { - #[serde(flatten)] - pub base: $response_base_type, - - $( - $( #[$response_field_attr] )* - pub $response_field: $response_field_type, - )* - } - - ::cuprate_epee_encoding::epee_object! { - [<$type_name Response>], - $( - $response_field: $response_field_type, - )* - !flatten: base: $response_base_type, - } - }}; - - //------------------------------------------------------------------------------ - // This version of the macro expects a `Request` type with fields. - ( - // The markdown tag for Monero RPC documentation. Not necessarily the endpoint. + // The markdown tag for Monero daemon RPC documentation. Not necessarily the endpoint. $monero_daemon_rpc_doc_link:ident, // The commit hash and `$file.$extension` in which this type is defined in @@ -134,15 +57,20 @@ macro_rules! define_request_and_response { $monero_code_line_end:literal, // The base `struct` name. + // Attributes added here will apply to _both_ + // request and response types. + $( #[$type_attr:meta] )* $type_name:ident, // The request type (and any doc comments, derives, etc). $( #[$request_type_attr:meta] )* - $request_base_type:ty { + Request { // And any fields. $( - $( #[$request_field_attr:meta] )* - $request_field:ident: $request_field_type:ty, + $( #[$request_field_attr:meta] )* // Field attribute. + $request_field:ident: $request_field_type:ty // field_name: field type + $(as $request_field_type_as:ty)? // (optional) alternative type (de)serialization + $(= $request_field_type_default:expr, $request_field_type_default_string:literal)?, // (optional) default value )* }, @@ -152,79 +80,223 @@ macro_rules! define_request_and_response { // And any fields. $( $( #[$response_field_attr:meta] )* - $response_field:ident: $response_field_type:ty, + $response_field:ident: $response_field_type:ty + $(as $response_field_type_as:ty)? + $(= $response_field_type_default:expr, $response_field_type_default_string:literal)?, )* } ) => { paste::paste! { - #[allow(dead_code)] - #[allow(missing_docs)] - #[derive(serde::Serialize, serde::Deserialize)] - #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] - $( #[$request_type_attr] )* - #[doc = $crate::macros::define_request_and_response_doc!( - "response", - $monero_daemon_rpc_doc_link, - $monero_code_commit, - $monero_code_filename, - $monero_code_filename_extension, - $monero_code_line_start, - $monero_code_line_end, - [<$type_name Request>], - )] - pub struct [<$type_name Request>] { - #[serde(flatten)] - pub base: $request_base_type, - - $( - $( #[$request_field_attr] )* - pub $request_field: $request_field_type, - )* + $crate::macros::define_request! { + #[doc = $crate::macros::define_request_and_response_doc!( + "response" => [<$type_name Response>], + $monero_daemon_rpc_doc_link, + $monero_code_commit, + $monero_code_filename, + $monero_code_filename_extension, + $monero_code_line_start, + $monero_code_line_end, + )] + /// + $( #[$type_attr] )* + /// + $( #[$request_type_attr] )* + [<$type_name Request>] { + $( + $( #[$request_field_attr] )* + $request_field: $request_field_type + $(as $request_field_type_as)? + $(= $request_field_type_default, $request_field_type_default_string)?, + )* + } } - ::cuprate_epee_encoding::epee_object! { - [<$type_name Request>], - $( - $request_field: $request_field_type, - )* - !flatten: base: $request_base_type, - } - - #[allow(dead_code)] - #[allow(missing_docs)] - #[derive(serde::Serialize, serde::Deserialize)] - #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] - $( #[$response_type_attr] )* - #[doc = $crate::macros::define_request_and_response_doc!( - "request", - $monero_daemon_rpc_doc_link, - $monero_code_commit, - $monero_code_filename, - $monero_code_filename_extension, - $monero_code_line_start, - $monero_code_line_end, - [<$type_name Response>], - )] - pub struct [<$type_name Response>] { - #[serde(flatten)] - pub base: $response_base_type, - - $( - $( #[$response_field_attr] )* - pub $response_field: $response_field_type, - )* - } - - ::cuprate_epee_encoding::epee_object! { - [<$type_name Response>], - $( - $response_field: $response_field_type, - )* - !flatten: base: $response_base_type, + $crate::macros::define_response! { + #[allow(dead_code)] + #[allow(missing_docs)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[doc = $crate::macros::define_request_and_response_doc!( + "request" => [<$type_name Request>], + $monero_daemon_rpc_doc_link, + $monero_code_commit, + $monero_code_filename, + $monero_code_filename_extension, + $monero_code_line_start, + $monero_code_line_end, + )] + /// + $( #[$type_attr] )* + /// + $( #[$response_type_attr] )* + $response_base_type => [<$type_name Response>] { + $( + $( #[$response_field_attr] )* + $response_field: $response_field_type + $(as $response_field_type_as)? + $(= $response_field_type_default, $response_field_type_default_string)?, + )* + } } }}; } pub(crate) use define_request_and_response; +//---------------------------------------------------------------------------------------------------- define_request +/// Define a request type. +/// +/// This is only used in [`define_request_and_response`], see it for docs. +macro_rules! define_request { + //------------------------------------------------------------------------------ + // This branch will generate a type alias to `()` if only given `{}` as input. + ( + // Any doc comments, derives, etc. + $( #[$attr:meta] )* + // The response type. + $t:ident {} + ) => { + $( #[$attr] )* + /// + /// This request has no inputs. + pub type $t = (); + }; + + //------------------------------------------------------------------------------ + // This branch of the macro expects fields within the `{}`, + // and will generate a `struct` + ( + // Any doc comments, derives, etc. + $( #[$attr:meta] )* + // The response type. + $t:ident { + // And any fields. + $( + $( #[$field_attr:meta] )* // field attributes + // field_name: FieldType + $field:ident: $field_type:ty + $(as $field_as:ty)? + $(= $field_default:expr, $field_default_string:literal)?, + // The $field_default is an optional extra token that represents + // a default value to pass to [`cuprate_epee_encoding::epee_object`], + // see it for usage. + )* + } + ) => { + #[allow(dead_code, missing_docs)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + $( #[$attr] )* + pub struct $t { + $( + $( #[$field_attr] )* + $(#[cfg_attr(feature = "serde", serde(default = $field_default_string))])? + pub $field: $field_type, + )* + } + + #[cfg(feature = "epee")] + ::cuprate_epee_encoding::epee_object! { + $t, + $( + $field: $field_type + $(as $field_as)? + $(= $field_default)?, + )* + } + }; +} +pub(crate) use define_request; + +//---------------------------------------------------------------------------------------------------- define_response +/// Define a response type. +/// +/// This is used in [`define_request_and_response`], see it for docs. +macro_rules! define_response { + //------------------------------------------------------------------------------ + // This version of the macro expects the literal ident + // `Response` => $response_type_name. + // + // It will create a `struct` that _doesn't_ use a base from [`crate::base`], + // for example, [`crate::json::BannedResponse`] doesn't use a base, so it + // uses this branch. + ( + // Any doc comments, derives, etc. + $( #[$attr:meta] )* + // The response type. + Response => $t:ident { + // And any fields. + // See [`define_request`] for docs, this does the same thing. + $( + $( #[$field_attr:meta] )* + $field:ident: $field_type:ty + $(as $field_as:ty)? + $(= $field_default:expr, $field_default_string:literal)?, + )* + } + ) => { + $( #[$attr] )* + pub struct $t { + $( + $( #[$field_attr] )* + $(#[cfg_attr(feature = "serde", serde(default = $field_default_string))])? + pub $field: $field_type, + )* + } + + #[cfg(feature = "epee")] + ::cuprate_epee_encoding::epee_object! { + $t, + $( + $field: $field_type + $(as $field_as)? + $(= $field_default)?, + )* + } + }; + + //------------------------------------------------------------------------------ + // This version of the macro expects a `Request` base type from [`crate::bases`]. + ( + // Any doc comments, derives, etc. + $( #[$attr:meta] )* + // The response base type => actual name of the struct + $base:ty => $t:ident { + // And any fields. + // See [`define_request`] for docs, this does the same thing. + $( + $( #[$field_attr:meta] )* + $field:ident: $field_type:ty + $(as $field_as:ty)? + $(= $field_default:expr, $field_default_string:literal)?, + )* + } + ) => { + $( #[$attr] )* + pub struct $t { + #[cfg_attr(feature = "serde", serde(flatten))] + pub base: $base, + + $( + $( #[$field_attr] )* + $(#[cfg_attr(feature = "serde", serde(default = $field_default_string))])? + pub $field: $field_type, + )* + } + + #[cfg(feature = "epee")] + ::cuprate_epee_encoding::epee_object! { + $t, + $( + $field: $field_type + $(as $field_as)? + $(= $field_default)?, + )* + !flatten: base: $base, + } + }; +} +pub(crate) use define_response; + +//---------------------------------------------------------------------------------------------------- define_request_and_response_doc /// Generate documentation for the types generated /// by the [`define_request_and_response`] macro. /// @@ -239,7 +311,7 @@ macro_rules! define_request_and_response_doc { // Remember this is linking to the _other_ type, // so if defining a `Request` type, input should // be "response". - $request_or_response:literal, + $request_or_response:literal => $request_or_response_type:ident, $monero_daemon_rpc_doc_link:ident, $monero_code_commit:ident, @@ -247,7 +319,6 @@ macro_rules! define_request_and_response_doc { $monero_code_filename_extension:ident, $monero_code_line_start:literal, $monero_code_line_end:literal, - $type_name:ident, ) => { concat!( "", @@ -269,9 +340,34 @@ macro_rules! define_request_and_response_doc { "), [", $request_or_response, "](", - stringify!($type_name), + stringify!($request_or_response_type), ")." ) }; } pub(crate) use define_request_and_response_doc; + +//---------------------------------------------------------------------------------------------------- Macro +/// Output a string link to `monerod` source code. +macro_rules! monero_definition_link { + ( + $commit:ident, // Git commit hash + $file_path:literal, // File path within `monerod`'s `src/`, e.g. `rpc/core_rpc_server_commands_defs.h` + $start:literal$(..=$end:literal)? // File lines, e.g. `0..=123` or `0` + ) => { + concat!( + "[Definition](https://github.com/monero-project/monero/blob/", + stringify!($commit), + "/src/", + $file_path, + "#L", + stringify!($start), + $( + "-L", + stringify!($end), + )? + ")." + ) + }; +} +pub(crate) use monero_definition_link; diff --git a/rpc/types/src/binary_string.rs b/rpc/types/src/misc/binary_string.rs similarity index 80% rename from rpc/types/src/binary_string.rs rename to rpc/types/src/misc/binary_string.rs index b644ad32..5c3908dd 100644 --- a/rpc/types/src/binary_string.rs +++ b/rpc/types/src/misc/binary_string.rs @@ -1,14 +1,14 @@ -//! TODO +//! JSON string containing binary data. //---------------------------------------------------------------------------------------------------- Import //---------------------------------------------------------------------------------------------------- BinaryString -/// TODO +/// TODO: we need to figure out a type that (de)serializes correctly, `String` errors with `serde_json` /// /// ```rust /// use serde::Deserialize; /// use serde_json::from_str; -/// use cuprate_rpc_types::BinaryString; +/// use cuprate_rpc_types::misc::BinaryString; /// /// #[derive(Deserialize)] /// struct Key { diff --git a/rpc/types/src/misc/distribution.rs b/rpc/types/src/misc/distribution.rs new file mode 100644 index 00000000..1a488d44 --- /dev/null +++ b/rpc/types/src/misc/distribution.rs @@ -0,0 +1,309 @@ +//! Output distributions for [`crate::json::GetOutputDistributionResponse`]. + +//---------------------------------------------------------------------------------------------------- Use +use std::mem::size_of; + +#[cfg(feature = "serde")] +use serde::{ser::SerializeStruct, Deserialize, Serialize}; + +#[cfg(feature = "epee")] +use cuprate_epee_encoding::{ + epee_object, error, + macros::bytes::{Buf, BufMut}, + read_epee_value, read_varint, write_field, write_varint, EpeeObject, EpeeObjectBuilder, + EpeeValue, Marker, +}; + +//---------------------------------------------------------------------------------------------------- Free +/// TODO: . +/// +/// Used for [`Distribution::CompressedBinary::distribution`]. +#[doc = crate::macros::monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 45..=55 +)] +#[cfg(feature = "epee")] +fn compress_integer_array(array: &[u64]) -> error::Result> { + todo!() +} + +/// TODO: . +/// +/// Used for [`Distribution::CompressedBinary::distribution`]. +#[doc = crate::macros::monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 57..=72 +)] +fn decompress_integer_array(array: &[u8]) -> Vec { + todo!() +} + +//---------------------------------------------------------------------------------------------------- Distribution +#[doc = crate::macros::monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 2468..=2508 +)] +/// Used in [`crate::json::GetOutputDistributionResponse`]. +/// +/// # Internals +/// This type's (de)serialization depends on `monerod`'s (de)serialization. +/// +/// During serialization: +/// [`Self::Uncompressed`] will emit: +/// - `compress: false` +/// +/// [`Self::CompressedBinary`] will emit: +/// - `binary: true` +/// - `compress: true` +/// +/// Upon deserialization, the presence of a `compressed_data` +/// field signifies that the [`Self::CompressedBinary`] should +/// be selected. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] +pub enum Distribution { + /// Distribution data will be (de)serialized as either JSON or binary (uncompressed). + Uncompressed(DistributionUncompressed), + /// Distribution data will be (de)serialized as compressed binary. + CompressedBinary(DistributionCompressedBinary), +} + +impl Default for Distribution { + fn default() -> Self { + Self::Uncompressed(DistributionUncompressed::default()) + } +} + +/// Data within [`Distribution::Uncompressed`]. +#[allow(dead_code, missing_docs)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DistributionUncompressed { + pub start_height: u64, + pub base: u64, + /// TODO: this is a binary JSON string if `binary == true`. + pub distribution: Vec, + pub amount: u64, + pub binary: bool, +} + +#[cfg(feature = "epee")] +epee_object! { + DistributionUncompressed, + start_height: u64, + base: u64, + distribution: Vec, + amount: u64, + binary: bool, +} + +/// Data within [`Distribution::CompressedBinary`]. +#[allow(dead_code, missing_docs)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DistributionCompressedBinary { + pub start_height: u64, + pub base: u64, + #[cfg_attr( + feature = "serde", + serde(serialize_with = "serialize_distribution_as_compressed_data") + )] + #[cfg_attr( + feature = "serde", + serde(deserialize_with = "deserialize_compressed_data_as_distribution") + )] + #[cfg_attr(feature = "serde", serde(rename = "compressed_data"))] + pub distribution: Vec, + pub amount: u64, +} + +#[cfg(feature = "epee")] +epee_object! { + DistributionCompressedBinary, + start_height: u64, + base: u64, + distribution: Vec, + amount: u64, +} + +/// Serializer function for [`DistributionCompressedBinary::distribution`]. +/// +/// 1. Compresses the distribution array +/// 2. Serializes the compressed data +#[cfg(feature = "serde")] +#[allow(clippy::ptr_arg)] +fn serialize_distribution_as_compressed_data(v: &Vec, s: S) -> Result +where + S: serde::Serializer, +{ + match compress_integer_array(v) { + Ok(compressed_data) => compressed_data.serialize(s), + Err(_) => Err(serde::ser::Error::custom( + "error compressing distribution array", + )), + } +} + +/// Deserializer function for [`DistributionCompressedBinary::distribution`]. +/// +/// 1. Deserializes as `compressed_data` field. +/// 2. Decompresses and returns the data +#[cfg(feature = "serde")] +fn deserialize_compressed_data_as_distribution<'de, D>(d: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + Vec::::deserialize(d).map(|v| decompress_integer_array(&v)) +} + +//---------------------------------------------------------------------------------------------------- Epee +#[cfg(feature = "epee")] +/// [`EpeeObjectBuilder`] for [`Distribution`]. +/// +/// Not for public usage. +#[allow(dead_code, missing_docs)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct __DistributionEpeeBuilder { + pub start_height: Option, + pub base: Option, + pub distribution: Option>, + pub amount: Option, + pub compressed_data: Option>, + pub binary: Option, + pub compress: Option, +} + +#[cfg(feature = "epee")] +impl EpeeObjectBuilder for __DistributionEpeeBuilder { + fn add_field(&mut self, name: &str, r: &mut B) -> error::Result { + macro_rules! read_epee_field { + ($($field:ident),*) => { + match name { + $( + stringify!($field) => { self.$field = Some(read_epee_value(r)?); }, + )* + _ => return Ok(false), + } + }; + } + + read_epee_field! { + start_height, + base, + amount, + binary, + compress, + compressed_data, + distribution + } + + Ok(true) + } + + fn finish(self) -> error::Result { + const ELSE: error::Error = error::Error::Format("Required field was not found!"); + + let start_height = self.start_height.ok_or(ELSE)?; + let base = self.base.ok_or(ELSE)?; + let amount = self.amount.ok_or(ELSE)?; + + let distribution = if let Some(compressed_data) = self.compressed_data { + let distribution = decompress_integer_array(&compressed_data); + Distribution::CompressedBinary(DistributionCompressedBinary { + start_height, + base, + distribution, + amount, + }) + } else if let Some(distribution) = self.distribution { + Distribution::Uncompressed(DistributionUncompressed { + binary: self.binary.ok_or(ELSE)?, + distribution, + start_height, + base, + amount, + }) + } else { + return Err(ELSE); + }; + + Ok(distribution) + } +} + +#[cfg(feature = "epee")] +impl EpeeObject for Distribution { + type Builder = __DistributionEpeeBuilder; + + fn number_of_fields(&self) -> u64 { + match self { + // Inner struct fields + `compress`. + Self::Uncompressed(s) => s.number_of_fields() + 1, + // Inner struct fields + `compress` + `binary`. + Self::CompressedBinary(s) => s.number_of_fields() + 2, + } + } + + fn write_fields(self, w: &mut B) -> error::Result<()> { + match self { + Self::Uncompressed(s) => { + s.write_fields(w)?; + write_field(false, "compress", w)?; + } + + Self::CompressedBinary(DistributionCompressedBinary { + start_height, + base, + distribution, + amount, + }) => { + let compressed_data = compress_integer_array(&distribution)?; + + start_height.write(w)?; + base.write(w)?; + compressed_data.write(w)?; + amount.write(w)?; + + write_field(true, "binary", w)?; + write_field(true, "compress", w)?; + } + } + + Ok(()) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + // TODO: re-enable tests after (de)compression functions are implemented. + + // /// Tests that [`compress_integer_array`] outputs as expected. + // #[test] + // fn compress() { + // let varints = &[16_384, 16_383, 16_382, 16_381]; + // let bytes = compress_integer_array(varints).unwrap(); + + // let expected = [2, 0, 1, 0, 253, 255, 249, 255, 245, 255]; + // assert_eq!(expected, *bytes); + // } + + // /// Tests that [`decompress_integer_array`] outputs as expected. + // #[test] + // fn decompress() { + // let bytes = &[2, 0, 1, 0, 253, 255, 249, 255, 245, 255]; + // let varints = decompress_integer_array(bytes); + + // let expected = vec![16_384, 16_383, 16_382, 16_381]; + // assert_eq!(expected, varints); + // } +} diff --git a/rpc/types/src/misc/key_image_spent_status.rs b/rpc/types/src/misc/key_image_spent_status.rs new file mode 100644 index 00000000..4b2eb535 --- /dev/null +++ b/rpc/types/src/misc/key_image_spent_status.rs @@ -0,0 +1,85 @@ +//! TODO + +//---------------------------------------------------------------------------------------------------- Use +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "epee")] +use cuprate_epee_encoding::{ + error, + macros::bytes::{Buf, BufMut}, + EpeeValue, Marker, +}; + +//---------------------------------------------------------------------------------------------------- KeyImageSpentStatus +#[doc = crate::macros::monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 456..=460 +)] +/// Used in [`crate::other::IsKeyImageSpentResponse`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[repr(u8)] +pub enum KeyImageSpentStatus { + Unspent = 0, + SpentInBlockchain = 1, + SpentInPool = 2, +} + +impl KeyImageSpentStatus { + /// Convert [`Self`] to a [`u8`]. + /// + /// ```rust + /// use cuprate_rpc_types::misc::KeyImageSpentStatus as K; + /// + /// assert_eq!(K::Unspent.to_u8(), 0); + /// assert_eq!(K::SpentInBlockchain.to_u8(), 1); + /// assert_eq!(K::SpentInPool.to_u8(), 2); + /// ``` + pub const fn to_u8(self) -> u8 { + match self { + Self::Unspent => 0, + Self::SpentInBlockchain => 1, + Self::SpentInPool => 2, + } + } + + /// Convert a [`u8`] to a [`Self`]. + /// + /// # Errors + /// This returns [`None`] if `u > 2`. + /// + /// ```rust + /// use cuprate_rpc_types::misc::KeyImageSpentStatus as K; + /// + /// assert_eq!(K::from_u8(0), Some(K::Unspent)); + /// assert_eq!(K::from_u8(1), Some(K::SpentInBlockchain)); + /// assert_eq!(K::from_u8(2), Some(K::SpentInPool)); + /// assert_eq!(K::from_u8(3), None); + /// ``` + pub const fn from_u8(u: u8) -> Option { + Some(match u { + 0 => Self::Unspent, + 1 => Self::SpentInBlockchain, + 2 => Self::SpentInPool, + _ => return None, + }) + } +} + +#[cfg(feature = "epee")] +impl EpeeValue for KeyImageSpentStatus { + const MARKER: Marker = u8::MARKER; + + fn read(r: &mut B, marker: &Marker) -> error::Result { + let u = u8::read(r, marker)?; + Self::from_u8(u).ok_or(error::Error::Format("u8 was greater than 2")) + } + + fn write(self, w: &mut B) -> error::Result<()> { + let u = self.to_u8(); + u8::write(u, w)?; + Ok(()) + } +} diff --git a/rpc/types/src/misc/misc.rs b/rpc/types/src/misc/misc.rs new file mode 100644 index 00000000..2b31cabf --- /dev/null +++ b/rpc/types/src/misc/misc.rs @@ -0,0 +1,530 @@ +//! Miscellaneous types. +//! +//! These are `struct`s that appear in request/response types. +//! For example, [`crate::json::GetConnectionsResponse`] contains +//! the [`crate::misc::ConnectionInfo`] struct defined here. + +//---------------------------------------------------------------------------------------------------- Import +use std::fmt::Display; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "epee")] +use cuprate_epee_encoding::{ + epee_object, + macros::bytes::{Buf, BufMut}, + EpeeValue, Marker, +}; + +use crate::{ + constants::{ + CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, + CORE_RPC_STATUS_PAYMENT_REQUIRED, + }, + defaults::{default_string, default_zero}, + macros::monero_definition_link, +}; + +//---------------------------------------------------------------------------------------------------- Macros +/// This macro (local to this file) defines all the misc types. +/// +/// This macro: +/// 1. Defines a `pub struct` with all `pub` fields +/// 2. Implements `serde` on the struct +/// 3. Implements `epee` on the struct +/// +/// When using, consider documenting: +/// - The original Monero definition site with [`monero_definition_link`] +/// - The request/responses where the `struct` is used +macro_rules! define_struct_and_impl_epee { + ( + // Optional `struct` attributes. + $( #[$struct_attr:meta] )* + // The `struct`'s name. + $struct_name:ident { + // And any fields. + $( + $( #[$field_attr:meta] )* // Field attributes + // Field name => the type => optional `epee_object` default value. + $field_name:ident: $field_type:ty $(= $field_default:expr)?, + )* + } + ) => { + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + $( #[$struct_attr] )* + pub struct $struct_name { + $( + $( #[$field_attr] )* + pub $field_name: $field_type, + )* + } + + #[cfg(feature = "epee")] + epee_object! { + $struct_name, + $( + $field_name: $field_type $(= $field_default)?, + )* + } + }; +} + +//---------------------------------------------------------------------------------------------------- Type Definitions +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1163..=1212 + )] + /// + /// Used in: + /// - [`crate::json::GetLastBlockHeaderResponse`] + /// - [`crate::json::GetBlockHeaderByHashResponse`] + /// - [`crate::json::GetBlockHeaderByHeightResponse`] + /// - [`crate::json::GetBlockHeadersRangeResponse`] + /// - [`crate::json::GetBlockResponse`] + BlockHeader { + block_size: u64, + block_weight: u64, + cumulative_difficulty_top64: u64, + cumulative_difficulty: u64, + depth: u64, + difficulty_top64: u64, + difficulty: u64, + hash: String, + height: u64, + long_term_weight: u64, + major_version: u8, + miner_tx_hash: String, + minor_version: u8, + nonce: u32, + num_txes: u64, + orphan_status: bool, + pow_hash: String, + prev_hash: String, + reward: u64, + timestamp: u64, + wide_cumulative_difficulty: String, + wide_difficulty: String, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "cryptonote_protocol/cryptonote_protocol_defs.h", + 47..=116 + )] + /// Used in [`crate::json::GetConnectionsResponse`]. + ConnectionInfo { + address: String, + address_type: u8, + avg_download: u64, + avg_upload: u64, + connection_id: String, + current_download: u64, + current_upload: u64, + height: u64, + host: String, + incoming: bool, + ip: String, + live_time: u64, + localhost: bool, + local_ip: bool, + peer_id: String, + port: String, + pruning_seed: u32, + recv_count: u64, + recv_idle_time: u64, + rpc_credits_per_hash: u32, + rpc_port: u16, + send_count: u64, + send_idle_time: u64, + // Exists in the original definition, but isn't + // used or (de)serialized for RPC purposes. + // ssl: bool, + state: String, + support_flags: u32, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 2034..=2047 + )] + /// Used in [`crate::json::SetBansRequest`]. + SetBan { + #[cfg_attr(feature = "serde", serde(default = "default_string"))] + host: String, + #[cfg_attr(feature = "serde", serde(default = "default_zero"))] + ip: u32, + ban: bool, + seconds: u32, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1999..=2010 + )] + /// Used in [`crate::json::GetBansResponse`]. + GetBan { + host: String, + ip: u32, + seconds: u32, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 2139..=2156 + )] + #[derive(Copy)] + /// Used in [`crate::json::GetOutputHistogramResponse`]. + HistogramEntry { + amount: u64, + total_instances: u64, + unlocked_instances: u64, + recent_instances: u64, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 2180..=2191 + )] + #[derive(Copy)] + /// Used in [`crate::json::GetVersionResponse`]. + HardforkEntry { + height: u64, + hf_version: u8, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 2289..=2310 + )] + /// Used in [`crate::json::GetAlternateChainsResponse`]. + ChainInfo { + block_hash: String, + block_hashes: Vec, + difficulty: u64, + difficulty_top64: u64, + height: u64, + length: u64, + main_chain_parent_block: String, + wide_difficulty: String, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 2393..=2400 + )] + /// Used in [`crate::json::SyncInfoResponse`]. + SyncInfoPeer { + info: ConnectionInfo, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 2402..=2421 + )] + /// Used in [`crate::json::SyncInfoResponse`]. + Span { + connection_id: String, + nblocks: u64, + rate: u32, + remote_address: String, + size: u64, + speed: u32, + start_block_height: u64, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1637..=1642 + )] + #[derive(Copy)] + /// Used in [`crate::json::GetTransactionPoolBacklogResponse`]. + TxBacklogEntry { + weight: u64, + fee: u64, + time_in_pool: u64, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/rpc_handler.h", + 45..=50 + )] + /// Used in [`crate::json::GetOutputDistributionResponse`]. + OutputDistributionData { + distribution: Vec, + start_height: u64, + base: u64, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1016..=1027 + )] + /// Used in [`crate::json::GetMinerDataResponse`]. + /// + /// Note that this is different than [`crate::misc::TxBacklogEntry`]. + GetMinerDataTxBacklogEntry { + id: String, + weight: u64, + fee: u64, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1070..=1079 + )] + /// Used in [`crate::json::AddAuxPowRequest`]. + AuxPow { + id: String, + hash: String, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 192..=199 + )] + /// Used in [`crate::bin::GetBlocksResponse`]. + TxOutputIndices { + indices: Vec, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 201..=208 + )] + /// Used in [`crate::bin::GetBlocksResponse`]. + BlockOutputIndices { + indices: Vec, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 210..=221 + )] + /// Used in [`crate::bin::GetBlocksResponse`]. + PoolTxInfo { + tx_hash: [u8; 32], + tx_blob: String, + double_spend_seen: bool, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 512..=521 + )] + #[derive(Copy)] + /// + /// Used in: + /// - [`crate::bin::GetOutsRequest`] + /// - [`crate::other::GetOutsRequest`] + GetOutputsOut { + amount: u64, + index: u64, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 538..=553 + )] + #[derive(Copy)] + /// Used in [`crate::bin::GetOutsRequest`]. + OutKeyBin { + key: [u8; 32], + mask: [u8; 32], + unlocked: bool, + height: u64, + txid: [u8; 32], + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1335..=1367 + )] + /// Used in [`crate::other::GetPeerListResponse`]. + Peer { + id: u64, + host: String, + ip: u32, + port: u16, + #[cfg_attr(feature = "serde", serde(default = "default_zero"))] + rpc_port: u16 = default_zero::(), + #[cfg_attr(feature = "serde", serde(default = "default_zero"))] + rpc_credits_per_hash: u32 = default_zero::(), + last_seen: u64, + #[cfg_attr(feature = "serde", serde(default = "default_zero"))] + pruning_seed: u32 = default_zero::(), + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1398..=1417 + )] + /// + /// Used in: + /// - [`crate::other::GetPeerListResponse`] + /// - [`crate::other::GetPublicNodesResponse`] + PublicNode { + host: String, + last_seen: u64, + rpc_port: u16, + rpc_credits_per_hash: u32, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1519..=1556 + )] + /// Used in [`crate::other::GetTransactionPoolResponse`]. + TxInfo { + blob_size: u64, + do_not_relay: bool, + double_spend_seen: bool, + fee: u64, + id_hash: String, + kept_by_block: bool, + last_failed_height: u64, + last_failed_id_hash: String, + last_relayed_time: u64, + max_used_block_height: u64, + max_used_block_id_hash: String, + receive_time: u64, + relayed: bool, + tx_blob: String, + tx_json: String, // TODO: this should be another struct + #[cfg_attr(feature = "serde", serde(default = "default_zero"))] + weight: u64 = default_zero::(), + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1558..=1567 + )] + /// Used in [`crate::other::GetTransactionPoolResponse`]. + SpentKeyImageInfo { + id_hash: String, + txs_hashes: Vec, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1666..=1675 + )] + #[derive(Copy)] + /// Used in [`crate::other::GetTransactionPoolStatsResponse`]. + TxpoolHisto { + txs: u32, + bytes: u64, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 1677..=1710 + )] + /// Used in [`crate::other::GetTransactionPoolStatsResponse`]. + TxpoolStats { + bytes_max: u32, + bytes_med: u32, + bytes_min: u32, + bytes_total: u64, + fee_total: u64, + histo_98pc: u64, + histo: Vec, + num_10m: u32, + num_double_spends: u32, + num_failing: u32, + num_not_relayed: u32, + oldest: u64, + txs_total: u32, + } +} + +define_struct_and_impl_epee! { + #[doc = monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 582..=597 + )] + /// Used in [`crate::other::GetOutsResponse`]. + OutKey { + key: String, + mask: String, + unlocked: bool, + height: u64, + txid: String, + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test {} diff --git a/rpc/types/src/misc/mod.rs b/rpc/types/src/misc/mod.rs new file mode 100644 index 00000000..bd6454dd --- /dev/null +++ b/rpc/types/src/misc/mod.rs @@ -0,0 +1,34 @@ +//! Miscellaneous types. +//! +//! These are data types that appear in request/response types. +//! +//! For example, [`crate::json::GetConnectionsResponse`] contains +//! the [`crate::misc::ConnectionInfo`] struct defined here. + +//---------------------------------------------------------------------------------------------------- Lints +#![allow( + missing_docs, // Docs are at: + clippy::struct_excessive_bools, // hey man, tell that to the people who wrote `monerod` +)] + +//---------------------------------------------------------------------------------------------------- Mod +mod binary_string; +mod distribution; +mod key_image_spent_status; +mod misc; +mod pool_info_extent; +mod status; +mod tx_entry; + +pub use binary_string::BinaryString; +pub use distribution::{Distribution, DistributionCompressedBinary, DistributionUncompressed}; +pub use key_image_spent_status::KeyImageSpentStatus; +pub use misc::{ + AuxPow, BlockHeader, BlockOutputIndices, ChainInfo, ConnectionInfo, GetBan, + GetMinerDataTxBacklogEntry, GetOutputsOut, HardforkEntry, HistogramEntry, OutKey, OutKeyBin, + OutputDistributionData, Peer, PoolTxInfo, PublicNode, SetBan, Span, SpentKeyImageInfo, + SyncInfoPeer, TxBacklogEntry, TxInfo, TxOutputIndices, TxpoolHisto, TxpoolStats, +}; +pub use pool_info_extent::PoolInfoExtent; +pub use status::Status; +pub use tx_entry::TxEntry; diff --git a/rpc/types/src/misc/pool_info_extent.rs b/rpc/types/src/misc/pool_info_extent.rs new file mode 100644 index 00000000..6372cd65 --- /dev/null +++ b/rpc/types/src/misc/pool_info_extent.rs @@ -0,0 +1,86 @@ +//! TODO + +//---------------------------------------------------------------------------------------------------- Use +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "epee")] +use cuprate_epee_encoding::{ + error, + macros::bytes::{Buf, BufMut}, + EpeeValue, Marker, +}; + +//---------------------------------------------------------------------------------------------------- PoolInfoExtent +#[doc = crate::macros::monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 223..=228 +)] +/// Used in [`crate::bin::GetBlocksResponse`]. +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[repr(u8)] +pub enum PoolInfoExtent { + #[default] + None = 0, + Incremental = 1, + Full = 2, +} + +impl PoolInfoExtent { + /// Convert [`Self`] to a [`u8`]. + /// + /// ```rust + /// use cuprate_rpc_types::misc::PoolInfoExtent as P; + /// + /// assert_eq!(P::None.to_u8(), 0); + /// assert_eq!(P::Incremental.to_u8(), 1); + /// assert_eq!(P::Full.to_u8(), 2); + /// ``` + pub const fn to_u8(self) -> u8 { + match self { + Self::None => 0, + Self::Incremental => 1, + Self::Full => 2, + } + } + + /// Convert a [`u8`] to a [`Self`]. + /// + /// # Errors + /// This returns [`None`] if `u > 2`. + /// + /// ```rust + /// use cuprate_rpc_types::misc::PoolInfoExtent as P; + /// + /// assert_eq!(P::from_u8(0), Some(P::None)); + /// assert_eq!(P::from_u8(1), Some(P::Incremental)); + /// assert_eq!(P::from_u8(2), Some(P::Full)); + /// assert_eq!(P::from_u8(3), None); + /// ``` + pub const fn from_u8(u: u8) -> Option { + Some(match u { + 0 => Self::None, + 1 => Self::Incremental, + 2 => Self::Full, + _ => return None, + }) + } +} + +#[cfg(feature = "epee")] +impl EpeeValue for PoolInfoExtent { + const MARKER: Marker = ::MARKER; + + fn read(r: &mut B, marker: &Marker) -> error::Result { + let u = u8::read(r, marker)?; + Self::from_u8(u).ok_or(error::Error::Format("u8 was greater than 2")) + } + + fn write(self, w: &mut B) -> error::Result<()> { + let u = self.to_u8(); + u8::write(u, w)?; + Ok(()) + } +} diff --git a/rpc/types/src/status.rs b/rpc/types/src/misc/status.rs similarity index 76% rename from rpc/types/src/status.rs rename to rpc/types/src/misc/status.rs index e8ac6ce9..79725cff 100644 --- a/rpc/types/src/status.rs +++ b/rpc/types/src/misc/status.rs @@ -3,8 +3,10 @@ //---------------------------------------------------------------------------------------------------- Import use std::fmt::Display; +#[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "epee")] use cuprate_epee_encoding::{ macros::bytes::{Buf, BufMut}, EpeeValue, Marker, @@ -12,88 +14,84 @@ use cuprate_epee_encoding::{ use crate::constants::{ CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, - CORE_RPC_STATUS_PAYMENT_REQUIRED, CORE_RPC_STATUS_UNKNOWN, + CORE_RPC_STATUS_PAYMENT_REQUIRED, }; //---------------------------------------------------------------------------------------------------- Status +// TODO: this type needs to expand more. +// There are a lot of RPC calls that will return a random +// string inside, which isn't compatible with [`Status`]. + /// RPC response status. /// /// This type represents `monerod`'s frequently appearing string field, `status`. /// -/// This field appears within RPC [JSON response](crate::json) types. -/// /// Reference: . /// /// ## Serialization and string formatting /// ```rust /// use cuprate_rpc_types::{ -/// Status, +/// misc::Status, /// CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, -/// CORE_RPC_STATUS_PAYMENT_REQUIRED, CORE_RPC_STATUS_UNKNOWN +/// CORE_RPC_STATUS_PAYMENT_REQUIRED /// }; /// use serde_json::to_string; /// -/// let unknown = Status::Unknown; +/// let other = Status::Other("OTHER".into()); /// /// assert_eq!(to_string(&Status::Ok).unwrap(), r#""OK""#); /// assert_eq!(to_string(&Status::Busy).unwrap(), r#""BUSY""#); /// assert_eq!(to_string(&Status::NotMining).unwrap(), r#""NOT MINING""#); /// assert_eq!(to_string(&Status::PaymentRequired).unwrap(), r#""PAYMENT REQUIRED""#); -/// assert_eq!(to_string(&unknown).unwrap(), r#""UNKNOWN""#); +/// assert_eq!(to_string(&other).unwrap(), r#""OTHER""#); /// /// assert_eq!(Status::Ok.as_ref(), CORE_RPC_STATUS_OK); /// assert_eq!(Status::Busy.as_ref(), CORE_RPC_STATUS_BUSY); /// assert_eq!(Status::NotMining.as_ref(), CORE_RPC_STATUS_NOT_MINING); /// assert_eq!(Status::PaymentRequired.as_ref(), CORE_RPC_STATUS_PAYMENT_REQUIRED); -/// assert_eq!(unknown.as_ref(), CORE_RPC_STATUS_UNKNOWN); +/// assert_eq!(other.as_ref(), "OTHER"); /// /// assert_eq!(format!("{}", Status::Ok), CORE_RPC_STATUS_OK); /// assert_eq!(format!("{}", Status::Busy), CORE_RPC_STATUS_BUSY); /// assert_eq!(format!("{}", Status::NotMining), CORE_RPC_STATUS_NOT_MINING); /// assert_eq!(format!("{}", Status::PaymentRequired), CORE_RPC_STATUS_PAYMENT_REQUIRED); -/// assert_eq!(format!("{}", unknown), CORE_RPC_STATUS_UNKNOWN); +/// assert_eq!(format!("{}", other), "OTHER"); /// /// assert_eq!(format!("{:?}", Status::Ok), "Ok"); /// assert_eq!(format!("{:?}", Status::Busy), "Busy"); /// assert_eq!(format!("{:?}", Status::NotMining), "NotMining"); /// assert_eq!(format!("{:?}", Status::PaymentRequired), "PaymentRequired"); -/// assert_eq!(format!("{:?}", unknown), "Unknown"); +/// assert_eq!(format!("{:?}", other), r#"Other("OTHER")"#); /// ``` -#[derive( - Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, -)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Status { // FIXME: // `#[serde(rename = "")]` only takes raw string literals? // We have to re-type the constants here... /// Successful RPC response, everything is OK; [`CORE_RPC_STATUS_OK`]. - #[serde(rename = "OK")] + #[cfg_attr(feature = "serde", serde(rename = "OK"))] #[default] Ok, /// The daemon is busy, try later; [`CORE_RPC_STATUS_BUSY`]. - #[serde(rename = "BUSY")] + #[cfg_attr(feature = "serde", serde(rename = "BUSY"))] Busy, /// The daemon is not mining; [`CORE_RPC_STATUS_NOT_MINING`]. - #[serde(rename = "NOT MINING")] + #[cfg_attr(feature = "serde", serde(rename = "NOT MINING"))] NotMining, /// Payment is required for RPC; [`CORE_RPC_STATUS_PAYMENT_REQUIRED`]. - #[serde(rename = "PAYMENT REQUIRED")] + #[cfg_attr(feature = "serde", serde(rename = "PAYMENT REQUIRED"))] PaymentRequired, - /// Some unknown other string; [`CORE_RPC_STATUS_UNKNOWN`]. + /// Some unknown other string. /// - /// This exists to act as a catch-all if `monerod` adds - /// a string and a Cuprate node hasn't updated yet. - /// - /// The reason this isn't `Unknown(String)` is because that - /// disallows [`Status`] to be [`Copy`], and thus other types - /// that contain it. - #[serde(other)] - #[serde(rename = "UNKNOWN")] - Unknown, + /// This exists to act as a catch-all for all of + /// `monerod`'s other strings it puts in the `status` field. + #[cfg_attr(feature = "serde", serde(rename = "OTHER"), serde(untagged))] + Other(String), } impl From for Status { @@ -103,7 +101,7 @@ impl From for Status { CORE_RPC_STATUS_BUSY => Self::Busy, CORE_RPC_STATUS_NOT_MINING => Self::NotMining, CORE_RPC_STATUS_PAYMENT_REQUIRED => Self::PaymentRequired, - _ => Self::Unknown, + _ => Self::Other(s), } } } @@ -115,7 +113,7 @@ impl AsRef for Status { Self::Busy => CORE_RPC_STATUS_BUSY, Self::NotMining => CORE_RPC_STATUS_NOT_MINING, Self::PaymentRequired => CORE_RPC_STATUS_PAYMENT_REQUIRED, - Self::Unknown => CORE_RPC_STATUS_UNKNOWN, + Self::Other(s) => s, } } } @@ -132,6 +130,7 @@ impl Display for Status { // // See below for more impl info: // . +#[cfg(feature = "epee")] impl EpeeValue for Status { const MARKER: Marker = ::MARKER; @@ -146,7 +145,7 @@ impl EpeeValue for Status { fn epee_default_value() -> Option { // - Some(Self::Unknown) + Some(Self::Other(String::new())) } fn write(self, w: &mut B) -> cuprate_epee_encoding::Result<()> { @@ -161,17 +160,18 @@ mod test { // Test epee (de)serialization works. #[test] + #[cfg(feature = "epee")] fn epee() { for status in [ Status::Ok, Status::Busy, Status::NotMining, Status::PaymentRequired, - Status::Unknown, + Status::Other(String::new()), ] { let mut buf = vec![]; - ::write(status, &mut buf).unwrap(); + ::write(status.clone(), &mut buf).unwrap(); let status2 = ::read(&mut buf.as_slice(), &::MARKER) .unwrap(); diff --git a/rpc/types/src/misc/tx_entry.rs b/rpc/types/src/misc/tx_entry.rs new file mode 100644 index 00000000..e643076d --- /dev/null +++ b/rpc/types/src/misc/tx_entry.rs @@ -0,0 +1,146 @@ +//! TODO + +//---------------------------------------------------------------------------------------------------- Use +#[cfg(feature = "serde")] +use crate::serde::{serde_false, serde_true}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "epee")] +use cuprate_epee_encoding::{ + epee_object, error, + macros::bytes::{Buf, BufMut}, + read_epee_value, write_field, EpeeObject, EpeeObjectBuilder, EpeeValue, Marker, +}; + +//---------------------------------------------------------------------------------------------------- TxEntry +#[doc = crate::macros::monero_definition_link!( + cc73fe71162d564ffda8e549b79a350bca53c454, + "rpc/core_rpc_server_commands_defs.h", + 389..=428 +)] +/// Used in [`crate::other::GetTransactionsResponse`]. +/// +/// # Epee +/// This type is only used in a JSON endpoint, so the +/// epee implementation on this type only panics. +/// +/// It is only implemented to satisfy the RPC type generator +/// macro, which requires all objects to be serde + epee. +/// +/// # Example +/// ```rust +/// use cuprate_rpc_types::misc::*; +/// use serde_json::{json, from_value}; +/// +/// let json = json!({ +/// "as_hex": String::default(), +/// "as_json": String::default(), +/// "block_height": u64::default(), +/// "block_timestamp": u64::default(), +/// "confirmations": u64::default(), +/// "double_spend_seen": bool::default(), +/// "output_indices": Vec::::default(), +/// "prunable_as_hex": String::default(), +/// "prunable_hash": String::default(), +/// "pruned_as_hex": String::default(), +/// "tx_hash": String::default(), +/// "in_pool": bool::default(), +/// }); +/// let tx_entry = from_value::(json).unwrap(); +/// assert!(matches!(tx_entry, TxEntry::InPool { .. })); +/// +/// let json = json!({ +/// "as_hex": String::default(), +/// "as_json": String::default(), +/// "double_spend_seen": bool::default(), +/// "prunable_as_hex": String::default(), +/// "prunable_hash": String::default(), +/// "pruned_as_hex": String::default(), +/// "received_timestamp": u64::default(), +/// "relayed": bool::default(), +/// "tx_hash": String::default(), +/// "in_pool": bool::default(), +/// }); +/// let tx_entry = from_value::(json).unwrap(); +/// assert!(matches!(tx_entry, TxEntry::NotInPool { .. })); +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] +pub enum TxEntry { + /// This entry exists in the transaction pool. + InPool { + as_hex: String, + as_json: String, + block_height: u64, + block_timestamp: u64, + confirmations: u64, + double_spend_seen: bool, + output_indices: Vec, + prunable_as_hex: String, + prunable_hash: String, + pruned_as_hex: String, + tx_hash: String, + #[cfg_attr(feature = "serde", serde(serialize_with = "serde_true"))] + /// Will always be serialized as `true`. + in_pool: bool, + }, + /// This entry _does not_ exist in the transaction pool. + NotInPool { + as_hex: String, + as_json: String, + double_spend_seen: bool, + prunable_as_hex: String, + prunable_hash: String, + pruned_as_hex: String, + received_timestamp: u64, + relayed: bool, + tx_hash: String, + #[cfg_attr(feature = "serde", serde(serialize_with = "serde_false"))] + /// Will always be serialized as `false`. + in_pool: bool, + }, +} + +impl Default for TxEntry { + fn default() -> Self { + Self::NotInPool { + as_hex: String::default(), + as_json: String::default(), + double_spend_seen: bool::default(), + prunable_as_hex: String::default(), + prunable_hash: String::default(), + pruned_as_hex: String::default(), + received_timestamp: u64::default(), + relayed: bool::default(), + tx_hash: String::default(), + in_pool: false, + } + } +} + +//---------------------------------------------------------------------------------------------------- Epee +#[cfg(feature = "epee")] +impl EpeeObjectBuilder for () { + fn add_field(&mut self, name: &str, r: &mut B) -> error::Result { + unreachable!() + } + + fn finish(self) -> error::Result { + unreachable!() + } +} + +#[cfg(feature = "epee")] +impl EpeeObject for TxEntry { + type Builder = (); + + fn number_of_fields(&self) -> u64 { + unreachable!() + } + + fn write_fields(self, w: &mut B) -> error::Result<()> { + unreachable!() + } +} diff --git a/rpc/types/src/other.rs b/rpc/types/src/other.rs index 22547edd..c1407778 100644 --- a/rpc/types/src/other.rs +++ b/rpc/types/src/other.rs @@ -1,19 +1,935 @@ //! JSON types from the [`other`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#other-daemon-rpc-calls) endpoints. //! -//! . +//! All types are originally defined in [`rpc/core_rpc_server_commands_defs.h`](https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server_commands_defs.h). //---------------------------------------------------------------------------------------------------- Import -use crate::{base::ResponseBase, macros::define_request_and_response}; +use crate::{ + base::{AccessResponseBase, ResponseBase}, + defaults::{default_false, default_string, default_true, default_vec, default_zero}, + macros::define_request_and_response, + misc::{ + GetOutputsOut, KeyImageSpentStatus, OutKey, Peer, PublicNode, SpentKeyImageInfo, Status, + TxEntry, TxInfo, TxpoolStats, + }, +}; + +//---------------------------------------------------------------------------------------------------- Macro +/// Adds a (de)serialization doc-test to a type in `other.rs`. +/// +/// It expects a const string from `cuprate_test_utils::rpc::data` +/// and the expected value it should (de)serialize into/from. +/// +/// It tests that the provided const JSON string can properly +/// (de)serialize into the expected value. +/// +/// See below for example usage. This macro is only used in this file. +macro_rules! serde_doc_test { + // This branch _only_ tests that the type can be deserialize + // from the string, not that any value is correct. + // + // Practically, this is used for structs that have + // many values that are complicated to test, e.g. `GET_TRANSACTIONS_RESPONSE`. + // + // HACK: + // The type itself doesn't need to be specified because it happens + // to just be the `CamelCase` version of the provided const. + ( + // `const` string from `cuprate_test_utils::rpc::data`. + $cuprate_test_utils_rpc_const:ident + ) => { + paste::paste! { + concat!( + "```rust\n", + "use cuprate_test_utils::rpc::data::other::*;\n", + "use cuprate_rpc_types::{misc::*, base::*, other::*};\n", + "use serde_json::{Value, from_str, from_value};\n", + "\n", + "let string = from_str::<", + stringify!([<$cuprate_test_utils_rpc_const:camel>]), + ">(", + stringify!($cuprate_test_utils_rpc_const), + ").unwrap();\n", + "```\n", + ) + } + }; + + // This branch tests that the type can be deserialize + // from the string AND that values are correct. + ( + // `const` string from `cuprate_test_utils::rpc::data` + // v + $cuprate_test_utils_rpc_const:ident => $expected:expr + // ^ + // Expected value as an expression + ) => { + paste::paste! { + concat!( + "```rust\n", + "use cuprate_test_utils::rpc::data::other::*;\n", + "use cuprate_rpc_types::{misc::*, base::*, other::*};\n", + "use serde_json::{Value, from_str, from_value};\n", + "\n", + "// The expected data.\n", + "let expected = ", + stringify!($expected), + ";\n", + "\n", + "let string = from_str::<", + stringify!([<$cuprate_test_utils_rpc_const:camel>]), + ">(", + stringify!($cuprate_test_utils_rpc_const), + ").unwrap();\n", + "\n", + "assert_eq!(string, expected);\n", + "```\n", + ) + } + }; +} + +//---------------------------------------------------------------------------------------------------- Definitions +define_request_and_response! { + get_height, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 138..=160, + GetHeight, + Request {}, + + #[doc = serde_doc_test!( + GET_HEIGHT_RESPONSE => GetHeightResponse { + base: ResponseBase::ok(), + hash: "68bb1a1cff8e2a44c3221e8e1aff80bc6ca45d06fa8eff4d2a3a7ac31d4efe3f".into(), + height: 3195160, + } + )] + ResponseBase { + hash: String, + height: u64, + } +} + +define_request_and_response! { + get_transactions, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 370..=451, + GetTransactions, + + #[doc = serde_doc_test!( + GET_TRANSACTIONS_REQUEST => GetTransactionsRequest { + txs_hashes: vec!["d6e48158472848e6687173a91ae6eebfa3e1d778e65252ee99d7515d63090408".into()], + decode_as_json: false, + prune: false, + split: false, + } + )] + Request { + txs_hashes: Vec, + // FIXME: this is documented as optional but it isn't serialized as an optional + // but it is set _somewhere_ to false in `monerod` + // + decode_as_json: bool = default_false(), "default_false", + prune: bool = default_false(), "default_false", + split: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!(GET_TRANSACTIONS_RESPONSE)] + AccessResponseBase { + txs_as_hex: Vec = default_vec::(), "default_vec", + txs_as_json: Vec = default_vec::(), "default_vec", + missed_tx: Vec = default_vec::(), "default_vec", + txs: Vec = default_vec::(), "default_vec", + } +} + +define_request_and_response! { + get_alt_blocks_hashes, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 288..=308, + GetAltBlocksHashes, + Request {}, + + #[doc = serde_doc_test!( + GET_ALT_BLOCKS_HASHES_RESPONSE => GetAltBlocksHashesResponse { + base: AccessResponseBase::ok(), + blks_hashes: vec!["8ee10db35b1baf943f201b303890a29e7d45437bd76c2bd4df0d2f2ee34be109".into()], + } + )] + AccessResponseBase { + blks_hashes: Vec, + } +} + +define_request_and_response! { + is_key_image_spent, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 454..=484, + + IsKeyImageSpent, + + #[doc = serde_doc_test!( + IS_KEY_IMAGE_SPENT_REQUEST => IsKeyImageSpentRequest { + key_images: vec![ + "8d1bd8181bf7d857bdb281e0153d84cd55a3fcaa57c3e570f4a49f935850b5e3".into(), + "7319134bfc50668251f5b899c66b005805ee255c136f0e1cecbb0f3a912e09d4".into() + ] + } + )] + Request { + key_images: Vec, + }, + + #[doc = serde_doc_test!( + IS_KEY_IMAGE_SPENT_RESPONSE => IsKeyImageSpentResponse { + base: AccessResponseBase::ok(), + spent_status: vec![1, 1], + } + )] + AccessResponseBase { + /// FIXME: These are [`KeyImageSpentStatus`] in [`u8`] form. + spent_status: Vec, + } +} + +define_request_and_response! { + send_raw_transaction, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 370..=451, + + SendRawTransaction, + + #[doc = serde_doc_test!( + SEND_RAW_TRANSACTION_REQUEST => SendRawTransactionRequest { + tx_as_hex: "dc16fa8eaffe1484ca9014ea050e13131d3acf23b419f33bb4cc0b32b6c49308".into(), + do_not_relay: false, + do_sanity_checks: true, + } + )] + Request { + tx_as_hex: String, + do_not_relay: bool = default_false(), "default_false", + do_sanity_checks: bool = default_true(), "default_true", + }, + + #[doc = serde_doc_test!( + SEND_RAW_TRANSACTION_RESPONSE => SendRawTransactionResponse { + base: AccessResponseBase { + response_base: ResponseBase { + status: Status::Other("Failed".into()), + untrusted: false, + }, + credits: 0, + top_hash: "".into(), + }, + double_spend: false, + fee_too_low: false, + invalid_input: false, + invalid_output: false, + low_mixin: false, + not_relayed: false, + overspend: false, + reason: "".into(), + sanity_check_failed: false, + too_big: false, + too_few_outputs: false, + tx_extra_too_big: false, + nonzero_unlock_time: false, + } + )] + AccessResponseBase { + double_spend: bool, + fee_too_low: bool, + invalid_input: bool, + invalid_output: bool, + low_mixin: bool, + nonzero_unlock_time: bool = default_false(), "default_false", + not_relayed: bool, + overspend: bool, + reason: String, + sanity_check_failed: bool, + too_big: bool, + too_few_outputs: bool, + tx_extra_too_big: bool, + } +} + +define_request_and_response! { + start_mining, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 665..=691, + + StartMining, + + #[doc = serde_doc_test!( + START_MINING_REQUEST => StartMiningRequest { + do_background_mining: false, + ignore_battery: true, + miner_address: "47xu3gQpF569au9C2ajo5SSMrWji6xnoE5vhr94EzFRaKAGw6hEGFXYAwVADKuRpzsjiU1PtmaVgcjUJF89ghGPhUXkndHc".into(), + threads_count: 1 + } + )] + Request { + miner_address: String, + threads_count: u64, + do_background_mining: bool, + ignore_battery: bool, + }, + + #[doc = serde_doc_test!( + START_MINING_RESPONSE => StartMiningResponse { + base: ResponseBase::ok(), + } + )] + ResponseBase {} +} + +define_request_and_response! { + stop_mining, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 825..=843, + StopMining, + Request {}, + + #[doc = serde_doc_test!( + STOP_MINING_RESPONSE => StopMiningResponse { + base: ResponseBase::ok(), + } + )] + ResponseBase {} +} + +define_request_and_response! { + mining_status, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 846..=895, + MiningStatus, + Request {}, + + #[doc = serde_doc_test!( + MINING_STATUS_RESPONSE => MiningStatusResponse { + base: ResponseBase::ok(), + active: false, + address: "".into(), + bg_idle_threshold: 0, + bg_ignore_battery: false, + bg_min_idle_seconds: 0, + bg_target: 0, + block_reward: 0, + block_target: 120, + difficulty: 292022797663, + difficulty_top64: 0, + is_background_mining_enabled: false, + pow_algorithm: "RandomX".into(), + speed: 0, + threads_count: 0, + wide_difficulty: "0x43fdea455f".into(), + } + )] + ResponseBase { + active: bool, + address: String, + bg_idle_threshold: u8, + bg_ignore_battery: bool, + bg_min_idle_seconds: u8, + bg_target: u8, + block_reward: u64, + block_target: u32, + difficulty: u64, + difficulty_top64: u64, + is_background_mining_enabled: bool, + pow_algorithm: String, + speed: u64, + threads_count: u32, + wide_difficulty: String, + } +} -//---------------------------------------------------------------------------------------------------- TODO define_request_and_response! { save_bc, cc73fe71162d564ffda8e549b79a350bca53c454 => core_rpc_server_commands_defs.h => 898..=916, SaveBc, + Request {}, + + #[doc = serde_doc_test!( + SAVE_BC_RESPONSE => SaveBcResponse { + base: ResponseBase::ok(), + } + )] ResponseBase {} } +define_request_and_response! { + get_peer_list, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1369..=1417, + + GetPeerList, + + #[doc = serde_doc_test!( + GET_PEER_LIST_REQUEST => GetPeerListRequest { + public_only: true, + include_blocked: false, + } + )] + Request { + public_only: bool = default_true(), "default_true", + include_blocked: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + GET_PEER_LIST_RESPONSE => GetPeerListResponse { + base: ResponseBase::ok(), + gray_list: vec![ + Peer { + host: "161.97.193.0".into(), + id: 18269586253849566614, + ip: 12673441, + last_seen: 0, + port: 18080, + rpc_port: 0, + rpc_credits_per_hash: 0, + pruning_seed: 0, + }, + Peer { + host: "193.142.4.2".into(), + id: 10865563782170056467, + ip: 33853121, + last_seen: 0, + port: 18085, + pruning_seed: 387, + rpc_port: 19085, + rpc_credits_per_hash: 0, + } + ], + white_list: vec![ + Peer { + host: "78.27.98.0".into(), + id: 11368279936682035606, + ip: 6429518, + last_seen: 1721246387, + port: 18080, + pruning_seed: 384, + rpc_port: 0, + rpc_credits_per_hash: 0, + }, + Peer { + host: "67.4.163.2".into(), + id: 16545113262826842499, + ip: 44237891, + last_seen: 1721246387, + port: 18080, + rpc_port: 0, + rpc_credits_per_hash: 0, + pruning_seed: 0, + }, + Peer { + host: "70.52.75.3".into(), + id: 3863337548778177169, + ip: 55260230, + last_seen: 1721246387, + port: 18080, + rpc_port: 18081, + rpc_credits_per_hash: 0, + pruning_seed: 0, + } + ] + } + )] + ResponseBase { + white_list: Vec, + gray_list: Vec, + } +} + +define_request_and_response! { + set_log_hash_rate, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1450..=1470, + SetLogHashRate, + + #[derive(Copy)] + #[doc = serde_doc_test!( + SET_LOG_HASH_RATE_REQUEST => SetLogHashRateRequest { + visible: true, + } + )] + Request { + visible: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + SET_LOG_HASH_RATE_RESPONSE => SetLogHashRateResponse { + base: ResponseBase::ok(), + } + )] + ResponseBase {} +} + +define_request_and_response! { + set_log_level, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1450..=1470, + + SetLogLevel, + + #[derive(Copy)] + #[doc = serde_doc_test!( + SET_LOG_LEVEL_REQUEST => SetLogLevelRequest { + level: 1 + } + )] + Request { + level: u8, + }, + + #[doc = serde_doc_test!( + SET_LOG_LEVEL_RESPONSE => SetLogLevelResponse { + base: ResponseBase::ok(), + } + )] + ResponseBase {} +} + +define_request_and_response! { + set_log_categories, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1494..=1517, + + SetLogCategories, + + #[doc = serde_doc_test!( + SET_LOG_CATEGORIES_REQUEST => SetLogCategoriesRequest { + categories: "*:INFO".into(), + } + )] + Request { + categories: String = default_string(), "default_string", + }, + + #[doc = serde_doc_test!( + SET_LOG_CATEGORIES_RESPONSE => SetLogCategoriesResponse { + base: ResponseBase::ok(), + categories: "*:INFO".into(), + } + )] + ResponseBase { + categories: String, + } +} + +define_request_and_response! { + set_bootstrap_daemon, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1785..=1812, + + SetBootstrapDaemon, + + #[doc = serde_doc_test!( + SET_BOOTSTRAP_DAEMON_REQUEST => SetBootstrapDaemonRequest { + address: "http://getmonero.org:18081".into(), + username: String::new(), + password: String::new(), + proxy: String::new(), + } + )] + Request { + address: String, + username: String = default_string(), "default_string", + password: String = default_string(), "default_string", + proxy: String = default_string(), "default_string", + }, + + #[doc = serde_doc_test!( + SET_BOOTSTRAP_DAEMON_RESPONSE => SetBootstrapDaemonResponse { + status: Status::Ok, + } + )] + Response { + status: Status, + } +} + +define_request_and_response! { + get_transaction_pool, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1569..=1591, + + GetTransactionPool, + Request {}, + + #[doc = serde_doc_test!(GET_TRANSACTION_POOL_RESPONSE)] + AccessResponseBase { + transactions: Vec, + spent_key_images: Vec, + } +} + +define_request_and_response! { + get_transaction_pool_stats, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1712..=1732, + + GetTransactionPoolStats, + Request {}, + + #[doc = serde_doc_test!( + GET_TRANSACTION_POOL_STATS_RESPONSE => GetTransactionPoolStatsResponse { + base: AccessResponseBase::ok(), + pool_stats: TxpoolStats { + bytes_max: 11843, + bytes_med: 2219, + bytes_min: 1528, + bytes_total: 144192, + fee_total: 7018100000, + histo: vec![ + TxpoolHisto { bytes: 11219, txs: 4 }, + TxpoolHisto { bytes: 9737, txs: 5 }, + TxpoolHisto { bytes: 8757, txs: 4 }, + TxpoolHisto { bytes: 14763, txs: 4 }, + TxpoolHisto { bytes: 15007, txs: 6 }, + TxpoolHisto { bytes: 15924, txs: 6 }, + TxpoolHisto { bytes: 17869, txs: 8 }, + TxpoolHisto { bytes: 10894, txs: 5 }, + TxpoolHisto { bytes: 38485, txs: 10 }, + TxpoolHisto { bytes: 1537, txs: 1 }, + ], + histo_98pc: 186, + num_10m: 0, + num_double_spends: 0, + num_failing: 0, + num_not_relayed: 0, + oldest: 1721261651, + txs_total: 53 + } + } + )] + AccessResponseBase { + pool_stats: TxpoolStats, + } +} + +define_request_and_response! { + stop_daemon, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1814..=1831, + + StopDaemon, + Request {}, + + #[doc = serde_doc_test!( + STOP_DAEMON_RESPONSE => StopDaemonResponse { + status: Status::Ok, + } + )] + Response { + status: Status, + } +} + +define_request_and_response! { + get_limit, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1852..=1874, + + GetLimit, + Request {}, + + #[doc = serde_doc_test!( + GET_LIMIT_RESPONSE => GetLimitResponse { + base: ResponseBase::ok(), + limit_down: 1280000, + limit_up: 1280000, + } + )] + ResponseBase { + limit_down: u64, + limit_up: u64, + } +} + +define_request_and_response! { + set_limit, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1876..=1903, + + SetLimit, + #[doc = serde_doc_test!( + SET_LIMIT_REQUEST => SetLimitRequest { + limit_down: 1024, + limit_up: 0, + } + )] + Request { + // FIXME: These may need to be `Option`. + limit_down: i64 = default_zero::(), "default_zero", + limit_up: i64 = default_zero::(), "default_zero", + }, + + #[doc = serde_doc_test!( + SET_LIMIT_RESPONSE => SetLimitResponse { + base: ResponseBase::ok(), + limit_down: 1024, + limit_up: 128, + } + )] + ResponseBase { + limit_down: i64, + limit_up: i64, + } +} + +define_request_and_response! { + out_peers, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1876..=1903, + + OutPeers, + + #[doc = serde_doc_test!( + OUT_PEERS_REQUEST => OutPeersRequest { + out_peers: 3232235535, + set: true, + } + )] + Request { + set: bool = default_true(), "default_true", + out_peers: u32, + }, + + #[doc = serde_doc_test!( + OUT_PEERS_RESPONSE => OutPeersResponse { + base: ResponseBase::ok(), + out_peers: 3232235535, + } + )] + ResponseBase { + out_peers: u32, + } +} + +define_request_and_response! { + get_net_stats, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 793..=822, + + GetNetStats, + Request {}, + + #[doc = serde_doc_test!( + GET_NET_STATS_RESPONSE => GetNetStatsResponse { + base: ResponseBase::ok(), + start_time: 1721251858, + total_bytes_in: 16283817214, + total_bytes_out: 34225244079, + total_packets_in: 5981922, + total_packets_out: 3627107, + } + )] + ResponseBase { + start_time: u64, + total_packets_in: u64, + total_bytes_in: u64, + total_packets_out: u64, + total_bytes_out: u64, + } +} + +define_request_and_response! { + get_outs, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 567..=609, + + GetOuts, + #[doc = serde_doc_test!( + GET_OUTS_REQUEST => GetOutsRequest { + outputs: vec![ + GetOutputsOut { amount: 1, index: 0 }, + GetOutputsOut { amount: 1, index: 1 }, + ], + get_txid: true + } + )] + Request { + outputs: Vec, + get_txid: bool, + }, + + #[doc = serde_doc_test!( + GET_OUTS_RESPONSE => GetOutsResponse { + base: ResponseBase::ok(), + outs: vec![ + OutKey { + height: 51941, + key: "08980d939ec297dd597119f498ad69fed9ca55e3a68f29f2782aae887ef0cf8e".into(), + mask: "1738eb7a677c6149228a2beaa21bea9e3370802d72a3eec790119580e02bd522".into(), + txid: "9d651903b80fb70b9935b72081cd967f543662149aed3839222511acd9100601".into(), + unlocked: true + }, + OutKey { + height: 51945, + key: "454fe46c405be77625fa7e3389a04d3be392346983f27603561ac3a3a74f4a75".into(), + mask: "1738eb7a677c6149228a2beaa21bea9e3370802d72a3eec790119580e02bd522".into(), + txid: "230bff732dc5f225df14fff82aadd1bf11b3fb7ad3a03413c396a617e843f7d0".into(), + unlocked: true + }, + ] + } + )] + ResponseBase { + outs: Vec, + } +} + +define_request_and_response! { + update, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2324..=2359, + + Update, + + #[doc = serde_doc_test!( + UPDATE_REQUEST => UpdateRequest { + command: "check".into(), + path: "".into(), + } + )] + Request { + command: String, + path: String = default_string(), "default_string", + }, + + #[doc = serde_doc_test!( + UPDATE_RESPONSE => UpdateResponse { + base: ResponseBase::ok(), + auto_uri: "".into(), + hash: "".into(), + path: "".into(), + update: false, + user_uri: "".into(), + version: "".into(), + } + )] + ResponseBase { + auto_uri: String, + hash: String, + path: String, + update: bool, + user_uri: String, + version: String, + } +} + +define_request_and_response! { + pop_blocks, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 2722..=2745, + + PopBlocks, + + #[doc = serde_doc_test!( + POP_BLOCKS_REQUEST => PopBlocksRequest { + nblocks: 6 + } + )] + Request { + nblocks: u64, + }, + + #[doc = serde_doc_test!( + POP_BLOCKS_RESPONSE => PopBlocksResponse { + base: ResponseBase::ok(), + height: 76482, + } + )] + ResponseBase { + height: u64, + } +} + +define_request_and_response! { + UNDOCUMENTED_ENDPOINT, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1615..=1635, + + GetTransactionPoolHashes, + Request {}, + + #[doc = serde_doc_test!( + GET_TRANSACTION_POOL_HASHES_RESPONSE => GetTransactionPoolHashesResponse { + base: ResponseBase::ok(), + tx_hashes: vec![ + "aa928aed888acd6152c60194d50a4df29b0b851be6169acf11b6a8e304dd6c03".into(), + "794345f321a98f3135151f3056c0fdf8188646a8dab27de971428acf3551dd11".into(), + "1e9d2ae11f2168a228942077483e70940d34e8658c972bbc3e7f7693b90edf17".into(), + "7375c928f261d00f07197775eb0bfa756e5f23319819152faa0b3c670fe54c1b".into(), + "2e4d5f8c5a45498f37fb8b6ca4ebc1efa0c371c38c901c77e66b08c072287329".into(), + "eee6d596cf855adfb10e1597d2018e3a61897ac467ef1d4a5406b8d20bfbd52f".into(), + "59c574d7ba9bb4558470f74503c7518946a85ea22c60fccfbdec108ce7d8f236".into(), + "0d57bec1e1075a9e1ac45cf3b3ced1ad95ccdf2a50ce360190111282a0178655".into(), + "60d627b2369714a40009c07d6185ebe7fa4af324fdfa8d95a37a936eb878d062".into(), + "661d7e728a901a8cb4cf851447d9cd5752462687ed0b776b605ba706f06bdc7d".into(), + "b80e1f09442b00b3fffe6db5d263be6267c7586620afff8112d5a8775a6fc58e".into(), + "974063906d1ddfa914baf85176b0f689d616d23f3d71ed4798458c8b4f9b9d8f".into(), + "d2575ae152a180be4981a9d2fc009afcd073adaa5c6d8b022c540a62d6c905bb".into(), + "3d78aa80ee50f506683bab9f02855eb10257a08adceda7cbfbdfc26b10f6b1bb".into(), + "8b5bc125bdb73b708500f734501d55088c5ac381a0879e1141634eaa72b6a4da".into(), + "11c06f4d2f00c912ca07313ed2ea5366f3cae914a762bed258731d3d9e3706df".into(), + "b3644dc7c9a3a53465fe80ad3769e516edaaeb7835e16fdd493aac110d472ae1".into(), + "ed2478ad793b923dbf652c8612c40799d764e5468897021234a14a37346bc6ee".into() + ], + } + )] + ResponseBase { + tx_hashes: Vec, + } +} + +define_request_and_response! { + UNDOCUMENTED_ENDPOINT, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 1419..=1448, + GetPublicNodes, + + #[doc = serde_doc_test!( + GET_PUBLIC_NODES_REQUEST => GetPublicNodesRequest { + gray: false, + white: true, + include_blocked: false, + } + )] + Request { + gray: bool = default_false(), "default_false", + white: bool = default_true(), "default_true", + include_blocked: bool = default_false(), "default_false", + }, + + #[doc = serde_doc_test!( + GET_PUBLIC_NODES_RESPONSE => GetPublicNodesResponse { + base: ResponseBase::ok(), + gray: vec![], + white: vec![ + PublicNode { + host: "70.52.75.3".into(), + last_seen: 1721246387, + rpc_credits_per_hash: 0, + rpc_port: 18081, + }, + PublicNode { + host: "zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion:18083".into(), + last_seen: 1720186288, + rpc_credits_per_hash: 0, + rpc_port: 18089, + } + ] + } + )] + ResponseBase { + gray: Vec = default_vec::(), "default_vec", + white: Vec = default_vec::(), "default_vec", + } +} + //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] mod test { diff --git a/rpc/types/src/serde.rs b/rpc/types/src/serde.rs new file mode 100644 index 00000000..70885e09 --- /dev/null +++ b/rpc/types/src/serde.rs @@ -0,0 +1,32 @@ +//! Custom (de)serialization functions for serde. + +//---------------------------------------------------------------------------------------------------- Lints +#![allow(clippy::trivially_copy_pass_by_ref)] // serde fn signature + +//---------------------------------------------------------------------------------------------------- Import +use serde::Serializer; + +//---------------------------------------------------------------------------------------------------- Free functions +/// Always serializes `true`. +#[inline] +pub(crate) fn serde_true(_: &bool, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_bool(true) +} + +/// Always serializes `false`. +#[inline] +pub(crate) fn serde_false(_: &bool, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_bool(false) +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + use super::*; +} diff --git a/storage/blockchain/Cargo.toml b/storage/blockchain/Cargo.toml index 26e51688..79d0dc47 100644 --- a/storage/blockchain/Cargo.toml +++ b/storage/blockchain/Cargo.toml @@ -30,7 +30,6 @@ bytemuck = { version = "1.14.3", features = ["must_cast", "derive", "min curve25519-dalek = { workspace = true } cuprate-pruning = { path = "../../pruning" } monero-serai = { workspace = true, features = ["std"] } -paste = { workspace = true } serde = { workspace = true, optional = true } # `service` feature. @@ -46,8 +45,8 @@ rayon = { workspace = true, optional = true } cuprate-helper = { path = "../../helper", features = ["thread"] } cuprate-test-utils = { path = "../../test-utils" } -bytemuck = { version = "1.14.3", features = ["must_cast", "derive", "min_const_generics", "extern_crate_alloc"] } tempfile = { version = "3.10.0" } pretty_assertions = { workspace = true } +proptest = { workspace = true } hex = { workspace = true } hex-literal = { workspace = true } diff --git a/storage/blockchain/README.md b/storage/blockchain/README.md index 8a2162c1..48005469 100644 --- a/storage/blockchain/README.md +++ b/storage/blockchain/README.md @@ -5,6 +5,10 @@ This documentation is mostly for practical usage of `cuprate_blockchain`. For a high-level overview, see the database section in [Cuprate's architecture book](https://architecture.cuprate.org). +If you're looking for a database crate, consider using the lower-level +[`cuprate-database`](https://doc.cuprate.org/cuprate_database) +crate that this crate is built on-top of. + # Purpose This crate does 3 things: 1. Uses [`cuprate_database`] as a base database layer @@ -47,11 +51,11 @@ there are some things that must be kept in mind when doing so. Failing to uphold these invariants may cause panics. 1. `LMDB` requires the user to resize the memory map resizing (see [`cuprate_database::RuntimeError::ResizeNeeded`] -1. `LMDB` has a maximum reader transaction count, currently it is set to `128` +1. `LMDB` has a maximum reader transaction count, currently, [it is set to `126`](https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/mdb.c#L794-L799) 1. `LMDB` has [maximum key/value byte size](http://www.lmdb.tech/doc/group__internal.html#gac929399f5d93cef85f874b9e9b1d09e0) which must not be exceeded # Examples -The below is an example of using `cuprate_blockchain` +The below is an example of using `cuprate_blockchain`'s lowest API, i.e. using a mix of this crate and `cuprate_database`'s traits directly - **this is NOT recommended.** @@ -67,8 +71,7 @@ use cuprate_blockchain::{ DatabaseRo, DatabaseRw, TxRo, TxRw, }, config::ConfigBuilder, - tables::{Tables, TablesMut}, - OpenTables, + tables::{Tables, TablesMut, OpenTables}, }; # fn main() -> Result<(), Box> { diff --git a/storage/blockchain/src/config/reader_threads.rs b/storage/blockchain/src/config/reader_threads.rs index 04216e3e..d4dd6ac4 100644 --- a/storage/blockchain/src/config/reader_threads.rs +++ b/storage/blockchain/src/config/reader_threads.rs @@ -20,12 +20,11 @@ use serde::{Deserialize, Serialize}; /// This controls how many reader thread `service`'s /// thread-pool will spawn to receive and send requests/responses. /// -/// It does nothing outside of `service`. -/// -/// It will always be at least 1, up until the amount of threads on the machine. -/// +/// # Invariant /// The main function used to extract an actual /// usable thread count out of this is [`ReaderThreads::as_threads`]. +/// +/// This will always return at least 1, up until the amount of threads on the machine. #[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ReaderThreads { @@ -97,30 +96,30 @@ impl ReaderThreads { /// /// # Example /// ```rust - /// use cuprate_blockchain::config::ReaderThreads as Rt; + /// use cuprate_blockchain::config::ReaderThreads as R; /// /// let total_threads: std::num::NonZeroUsize = /// cuprate_helper::thread::threads(); /// - /// assert_eq!(Rt::OnePerThread.as_threads(), total_threads); + /// assert_eq!(R::OnePerThread.as_threads(), total_threads); /// - /// assert_eq!(Rt::One.as_threads().get(), 1); + /// assert_eq!(R::One.as_threads().get(), 1); /// - /// assert_eq!(Rt::Number(0).as_threads(), total_threads); - /// assert_eq!(Rt::Number(1).as_threads().get(), 1); - /// assert_eq!(Rt::Number(usize::MAX).as_threads(), total_threads); + /// assert_eq!(R::Number(0).as_threads(), total_threads); + /// assert_eq!(R::Number(1).as_threads().get(), 1); + /// assert_eq!(R::Number(usize::MAX).as_threads(), total_threads); /// - /// assert_eq!(Rt::Percent(0.01).as_threads().get(), 1); - /// assert_eq!(Rt::Percent(0.0).as_threads(), total_threads); - /// assert_eq!(Rt::Percent(1.0).as_threads(), total_threads); - /// assert_eq!(Rt::Percent(f32::NAN).as_threads(), total_threads); - /// assert_eq!(Rt::Percent(f32::INFINITY).as_threads(), total_threads); - /// assert_eq!(Rt::Percent(f32::NEG_INFINITY).as_threads(), total_threads); + /// assert_eq!(R::Percent(0.01).as_threads().get(), 1); + /// assert_eq!(R::Percent(0.0).as_threads(), total_threads); + /// assert_eq!(R::Percent(1.0).as_threads(), total_threads); + /// assert_eq!(R::Percent(f32::NAN).as_threads(), total_threads); + /// assert_eq!(R::Percent(f32::INFINITY).as_threads(), total_threads); + /// assert_eq!(R::Percent(f32::NEG_INFINITY).as_threads(), total_threads); /// /// // Percentage only works on more than 1 thread. /// if total_threads.get() > 1 { /// assert_eq!( - /// Rt::Percent(0.5).as_threads().get(), + /// R::Percent(0.5).as_threads().get(), /// (total_threads.get() as f32 / 2.0) as usize, /// ); /// } diff --git a/storage/blockchain/src/free.rs b/storage/blockchain/src/free.rs index 255860aa..8288e65f 100644 --- a/storage/blockchain/src/free.rs +++ b/storage/blockchain/src/free.rs @@ -3,10 +3,10 @@ //---------------------------------------------------------------------------------------------------- Import use cuprate_database::{ConcreteEnv, Env, EnvInner, InitError, RuntimeError, TxRw}; -use crate::{config::Config, open_tables::OpenTables}; +use crate::{config::Config, tables::OpenTables}; //---------------------------------------------------------------------------------------------------- Free functions -/// Open the blockchain database, using the passed [`Config`]. +/// Open the blockchain database using the passed [`Config`]. /// /// This calls [`cuprate_database::Env::open`] and prepares the /// database to be ready for blockchain-related usage, e.g. @@ -50,20 +50,12 @@ pub fn open(config: Config) -> Result { // we want since it is agnostic, so we are responsible for this. { let env_inner = env.env_inner(); - let tx_rw = env_inner.tx_rw(); - let tx_rw = match tx_rw { - Ok(tx_rw) => tx_rw, - Err(e) => return Err(runtime_to_init_error(e)), - }; + let tx_rw = env_inner.tx_rw().map_err(runtime_to_init_error)?; // Create all tables. - if let Err(e) = OpenTables::create_tables(&env_inner, &tx_rw) { - return Err(runtime_to_init_error(e)); - }; + OpenTables::create_tables(&env_inner, &tx_rw).map_err(runtime_to_init_error)?; - if let Err(e) = tx_rw.commit() { - return Err(runtime_to_init_error(e)); - } + TxRw::commit(tx_rw).map_err(runtime_to_init_error)?; } Ok(env) diff --git a/storage/blockchain/src/lib.rs b/storage/blockchain/src/lib.rs index ad33e2af..9db0862a 100644 --- a/storage/blockchain/src/lib.rs +++ b/storage/blockchain/src/lib.rs @@ -114,23 +114,18 @@ compile_error!("Cuprate is only compatible with 64-bit CPUs"); // // Documentation for each module is located in the respective file. -pub mod config; - mod constants; -pub use constants::{DATABASE_CORRUPT_MSG, DATABASE_VERSION}; - -mod open_tables; -pub use open_tables::OpenTables; - mod free; + +pub use constants::{DATABASE_CORRUPT_MSG, DATABASE_VERSION}; +pub use cuprate_database; pub use free::open; +pub mod config; pub mod ops; pub mod tables; pub mod types; -pub use cuprate_database; - //---------------------------------------------------------------------------------------------------- Feature-gated #[cfg(feature = "service")] pub mod service; diff --git a/storage/blockchain/src/open_tables.rs b/storage/blockchain/src/open_tables.rs deleted file mode 100644 index b98b86b1..00000000 --- a/storage/blockchain/src/open_tables.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! TODO - -//---------------------------------------------------------------------------------------------------- Import -use cuprate_database::{EnvInner, RuntimeError, TxRo, TxRw}; - -use crate::tables::{TablesIter, TablesMut}; - -//---------------------------------------------------------------------------------------------------- Table function macro -/// `crate`-private macro for callings functions on all tables. -/// -/// This calls the function `$fn` with the optional -/// arguments `$args` on all tables - returning early -/// (within whatever scope this is called) if any -/// of the function calls error. -/// -/// Else, it evaluates to an `Ok((tuple, of, all, table, types, ...))`, -/// i.e., an `impl Table[Mut]` wrapped in `Ok`. -macro_rules! call_fn_on_all_tables_or_early_return { - ( - $($fn:ident $(::)?)* - ( - $($arg:ident),* $(,)? - ) - ) => {{ - Ok(( - $($fn ::)*<$crate::tables::BlockInfos>($($arg),*)?, - $($fn ::)*<$crate::tables::BlockBlobs>($($arg),*)?, - $($fn ::)*<$crate::tables::BlockHeights>($($arg),*)?, - $($fn ::)*<$crate::tables::KeyImages>($($arg),*)?, - $($fn ::)*<$crate::tables::NumOutputs>($($arg),*)?, - $($fn ::)*<$crate::tables::PrunedTxBlobs>($($arg),*)?, - $($fn ::)*<$crate::tables::PrunableHashes>($($arg),*)?, - $($fn ::)*<$crate::tables::Outputs>($($arg),*)?, - $($fn ::)*<$crate::tables::PrunableTxBlobs>($($arg),*)?, - $($fn ::)*<$crate::tables::RctOutputs>($($arg),*)?, - $($fn ::)*<$crate::tables::TxBlobs>($($arg),*)?, - $($fn ::)*<$crate::tables::TxIds>($($arg),*)?, - $($fn ::)*<$crate::tables::TxHeights>($($arg),*)?, - $($fn ::)*<$crate::tables::TxOutputs>($($arg),*)?, - $($fn ::)*<$crate::tables::TxUnlockTime>($($arg),*)?, - )) - }}; -} -pub(crate) use call_fn_on_all_tables_or_early_return; - -//---------------------------------------------------------------------------------------------------- OpenTables -/// Open all tables at once. -/// -/// This trait encapsulates the functionality of opening all tables at once. -/// It can be seen as the "constructor" for the [`Tables`](crate::tables::Tables) object. -/// -/// Note that this is already implemented on [`cuprate_database::EnvInner`], thus: -/// - You don't need to implement this -/// - It can be called using `env_inner.open_tables()` notation -/// -/// # Example -/// ```rust -/// use cuprate_blockchain::{ -/// cuprate_database::{Env, EnvInner}, -/// config::ConfigBuilder, -/// tables::{Tables, TablesMut}, -/// OpenTables, -/// }; -/// -/// # fn main() -> Result<(), Box> { -/// // Create a configuration for the database environment. -/// let tmp_dir = tempfile::tempdir()?; -/// let db_dir = tmp_dir.path().to_owned(); -/// let config = ConfigBuilder::new() -/// .db_directory(db_dir.into()) -/// .build(); -/// -/// // Initialize the database environment. -/// let env = cuprate_blockchain::open(config)?; -/// -/// // Open up a transaction. -/// let env_inner = env.env_inner(); -/// let tx_rw = env_inner.tx_rw()?; -/// -/// // Open _all_ tables in write mode using [`OpenTables::open_tables_mut`]. -/// // Note how this is being called on `env_inner`. -/// // | -/// // v -/// let mut tables = env_inner.open_tables_mut(&tx_rw)?; -/// # Ok(()) } -/// ``` -pub trait OpenTables<'env, Ro, Rw> -where - Self: 'env, - Ro: TxRo<'env>, - Rw: TxRw<'env>, -{ - /// Open all tables in read/iter mode. - /// - /// This calls [`EnvInner::open_db_ro`] on all database tables - /// and returns a structure that allows access to all tables. - /// - /// # Errors - /// This will only return [`RuntimeError::Io`] if it errors. - /// - /// As all tables are created upon [`crate::open`], - /// this function will never error because a table doesn't exist. - fn open_tables(&'env self, tx_ro: &Ro) -> Result; - - /// Open all tables in read-write mode. - /// - /// This calls [`EnvInner::open_db_rw`] on all database tables - /// and returns a structure that allows access to all tables. - /// - /// # Errors - /// This will only return [`RuntimeError::Io`] on errors. - fn open_tables_mut(&'env self, tx_rw: &Rw) -> Result; - - /// Create all database tables. - /// - /// This will create all the [`Table`](cuprate_database::Table)s - /// found in [`tables`](crate::tables). - /// - /// # Errors - /// This will only return [`RuntimeError::Io`] on errors. - fn create_tables(&'env self, tx_rw: &Rw) -> Result<(), RuntimeError>; -} - -impl<'env, Ei, Ro, Rw> OpenTables<'env, Ro, Rw> for Ei -where - Ei: EnvInner<'env, Ro, Rw>, - Ro: TxRo<'env>, - Rw: TxRw<'env>, -{ - fn open_tables(&'env self, tx_ro: &Ro) -> Result { - call_fn_on_all_tables_or_early_return! { - Self::open_db_ro(self, tx_ro) - } - } - - fn open_tables_mut(&'env self, tx_rw: &Rw) -> Result { - call_fn_on_all_tables_or_early_return! { - Self::open_db_rw(self, tx_rw) - } - } - - fn create_tables(&'env self, tx_rw: &Rw) -> Result<(), RuntimeError> { - match call_fn_on_all_tables_or_early_return! { - Self::create_db(self, tx_rw) - } { - Ok(_) => Ok(()), - Err(e) => Err(e), - } - } -} - -//---------------------------------------------------------------------------------------------------- Tests -#[cfg(test)] -mod test { - use std::borrow::Cow; - - use cuprate_database::{Env, EnvInner}; - - use crate::{config::ConfigBuilder, tests::tmp_concrete_env}; - - use super::*; - - /// Tests that [`crate::open`] creates all tables. - #[test] - fn test_all_tables_are_created() { - let (env, _tmp) = tmp_concrete_env(); - let env_inner = env.env_inner(); - let tx_ro = env_inner.tx_ro().unwrap(); - env_inner.open_tables(&tx_ro).unwrap(); - } - - /// Tests that directory [`cuprate_database::ConcreteEnv`] - /// usage does NOT create all tables. - #[test] - #[should_panic(expected = "`Result::unwrap()` on an `Err` value: TableNotFound")] - fn test_no_tables_are_created() { - let tempdir = tempfile::tempdir().unwrap(); - let config = ConfigBuilder::new() - .db_directory(Cow::Owned(tempdir.path().into())) - .low_power() - .build(); - let env = cuprate_database::ConcreteEnv::open(config.db_config).unwrap(); - - let env_inner = env.env_inner(); - let tx_ro = env_inner.tx_ro().unwrap(); - env_inner.open_tables(&tx_ro).unwrap(); - } -} diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index e05d2dd0..4d358f41 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -1,4 +1,4 @@ -//! Blocks functions. +//! Block functions. //---------------------------------------------------------------------------------------------------- Import use bytemuck::TransparentWrapper; @@ -271,8 +271,8 @@ mod test { use super::*; use crate::{ - open_tables::OpenTables, ops::tx::{get_tx, tx_exists}, + tables::OpenTables, tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, }; diff --git a/storage/blockchain/src/ops/blockchain.rs b/storage/blockchain/src/ops/blockchain.rs index 2b667194..3ccf4896 100644 --- a/storage/blockchain/src/ops/blockchain.rs +++ b/storage/blockchain/src/ops/blockchain.rs @@ -87,9 +87,8 @@ mod test { use super::*; use crate::{ - open_tables::OpenTables, ops::block::add_block, - tables::Tables, + tables::{OpenTables, Tables}, tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, }; diff --git a/storage/blockchain/src/ops/key_image.rs b/storage/blockchain/src/ops/key_image.rs index a518490e..19444d6b 100644 --- a/storage/blockchain/src/ops/key_image.rs +++ b/storage/blockchain/src/ops/key_image.rs @@ -52,8 +52,7 @@ mod test { use super::*; use crate::{ - open_tables::OpenTables, - tables::{Tables, TablesMut}, + tables::{OpenTables, Tables, TablesMut}, tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, }; diff --git a/storage/blockchain/src/ops/mod.rs b/storage/blockchain/src/ops/mod.rs index 58211202..2699fc82 100644 --- a/storage/blockchain/src/ops/mod.rs +++ b/storage/blockchain/src/ops/mod.rs @@ -5,14 +5,14 @@ //! database operations. //! //! # `impl Table` -//! `ops/` functions take [`Tables`](crate::tables::Tables) and +//! Functions in this module take [`Tables`](crate::tables::Tables) and //! [`TablesMut`](crate::tables::TablesMut) directly - these are //! _already opened_ database tables. //! -//! As such, the function puts the responsibility -//! of transactions, tables, etc on the caller. +//! As such, the responsibility of +//! transactions, tables, etc, are on the caller. //! -//! This does mean these functions are mostly as lean +//! Notably, this means that these functions are as lean //! as possible, so calling them in a loop should be okay. //! //! # Atomicity @@ -61,9 +61,8 @@ //! Env, EnvInner, //! DatabaseRo, DatabaseRw, TxRo, TxRw, //! }, -//! OpenTables, //! config::ConfigBuilder, -//! tables::{Tables, TablesMut}, +//! tables::{Tables, TablesMut, OpenTables}, //! ops::block::{add_block, pop_block}, //! }; //! diff --git a/storage/blockchain/src/ops/output.rs b/storage/blockchain/src/ops/output.rs index 649fcce5..f3453e46 100644 --- a/storage/blockchain/src/ops/output.rs +++ b/storage/blockchain/src/ops/output.rs @@ -254,8 +254,7 @@ mod test { use cuprate_database::{Env, EnvInner}; use crate::{ - open_tables::OpenTables, - tables::{Tables, TablesMut}, + tables::{OpenTables, Tables, TablesMut}, tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, types::OutputFlags, }; diff --git a/storage/blockchain/src/ops/tx.rs b/storage/blockchain/src/ops/tx.rs index 8e24be6a..6db71090 100644 --- a/storage/blockchain/src/ops/tx.rs +++ b/storage/blockchain/src/ops/tx.rs @@ -330,8 +330,7 @@ mod test { use cuprate_test_utils::data::{tx_v1_sig0, tx_v1_sig2, tx_v2_rct3}; use crate::{ - open_tables::OpenTables, - tables::Tables, + tables::{OpenTables, Tables}, tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, }; diff --git a/storage/blockchain/src/service/free.rs b/storage/blockchain/src/service/free.rs index 3ff8d6eb..3701f66f 100644 --- a/storage/blockchain/src/service/free.rs +++ b/storage/blockchain/src/service/free.rs @@ -33,8 +33,69 @@ pub fn init(config: Config) -> Result<(DatabaseReadHandle, DatabaseWriteHandle), Ok((readers, writer)) } -//---------------------------------------------------------------------------------------------------- Tests -#[cfg(test)] -mod test { - // use super::*; +//---------------------------------------------------------------------------------------------------- Compact history +/// Given a position in the compact history, returns the height offset that should be in that position. +/// +/// The height offset is the difference between the top block's height and the block height that should be in that position. +#[inline] +pub(super) const fn compact_history_index_to_height_offset( + i: u64, +) -> u64 { + // If the position is below the initial blocks just return the position back + if i <= INITIAL_BLOCKS { + i + } else { + // Otherwise we go with power of 2 offsets, the same as monerod. + // So (INITIAL_BLOCKS + 2), (INITIAL_BLOCKS + 2 + 4), (INITIAL_BLOCKS + 2 + 4 + 8) + // ref: + INITIAL_BLOCKS + (2 << (i - INITIAL_BLOCKS)) - 2 + } +} + +/// Returns if the genesis block was _NOT_ included when calculating the height offsets. +/// +/// The genesis must always be included in the compact history. +#[inline] +pub(super) const fn compact_history_genesis_not_included( + top_block_height: u64, +) -> bool { + // If the top block height is less than the initial blocks then it will always be included. + // Otherwise, we use the fact that to reach the genesis block this statement must be true (for a + // single `i`): + // + // `top_block_height - INITIAL_BLOCKS - 2^i + 2 == 0` + // which then means: + // `top_block_height - INITIAL_BLOCKS + 2 == 2^i` + // So if `top_block_height - INITIAL_BLOCKS + 2` is a power of 2 then the genesis block is in + // the compact history already. + top_block_height > INITIAL_BLOCKS && !(top_block_height - INITIAL_BLOCKS + 2).is_power_of_two() +} + +//---------------------------------------------------------------------------------------------------- Tests + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::*; + + proptest! { + #[test] + fn compact_history(top_height in 0_u64..500_000_000) { + let mut heights = (0..) + .map(compact_history_index_to_height_offset::<11>) + .map_while(|i| top_height.checked_sub(i)) + .collect::>(); + + if compact_history_genesis_not_included::<11>(top_height) { + heights.push(0); + } + + // Make sure the genesis and top block are always included. + assert_eq!(*heights.last().unwrap(), 0); + assert_eq!(*heights.first().unwrap(), top_height); + + heights.windows(2).for_each(|window| assert_ne!(window[0], window[1])); + } + } } diff --git a/storage/blockchain/src/service/mod.rs b/storage/blockchain/src/service/mod.rs index 1d9d10b4..bf2d8e77 100644 --- a/storage/blockchain/src/service/mod.rs +++ b/storage/blockchain/src/service/mod.rs @@ -63,7 +63,7 @@ //! use hex_literal::hex; //! use tower::{Service, ServiceExt}; //! -//! use cuprate_types::blockchain::{BCReadRequest, BCWriteRequest, BCResponse}; +//! use cuprate_types::{blockchain::{BCReadRequest, BCWriteRequest, BCResponse}, Chain}; //! use cuprate_test_utils::data::block_v16_tx0; //! //! use cuprate_blockchain::{ @@ -85,7 +85,7 @@ //! //! // Prepare a request to write block. //! let mut block = block_v16_tx0().clone(); -//! # block.height = 0 as u64; // must be 0th height or panic in `add_block()` +//! # block.height = 0_u64; // must be 0th height or panic in `add_block()` //! let request = BCWriteRequest::WriteBlock(block); //! //! // Send the request. @@ -100,7 +100,7 @@ //! //! // Now, let's try getting the block hash //! // of the block we just wrote. -//! let request = BCReadRequest::BlockHash(0); +//! let request = BCReadRequest::BlockHash(0, Chain::Main); //! let response_channel = read_handle.ready().await?.call(request); //! let response = response_channel.await?; //! assert_eq!( diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 20aebf9c..a5d51f1c 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -14,26 +14,29 @@ use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio_util::sync::PollSemaphore; use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner, RuntimeError}; -use cuprate_helper::asynch::InfallibleOneshotReceiver; +use cuprate_helper::{asynch::InfallibleOneshotReceiver, map::combine_low_high_bits_to_u128}; use cuprate_types::{ blockchain::{BCReadRequest, BCResponse}, - ExtendedBlockHeader, OutputOnChain, + Chain, ExtendedBlockHeader, OutputOnChain, }; use crate::{ config::ReaderThreads, - open_tables::OpenTables, - ops::block::block_exists, ops::{ - block::{get_block_extended_header_from_height, get_block_info}, + block::{ + block_exists, get_block_extended_header_from_height, get_block_height, get_block_info, + }, blockchain::{cumulative_generated_coins, top_block_height}, key_image::key_image_exists, output::id_to_output_on_chain, }, - service::types::{ResponseReceiver, ResponseResult, ResponseSender}, + service::{ + free::{compact_history_genesis_not_included, compact_history_index_to_height_offset}, + types::{ResponseReceiver, ResponseResult, ResponseSender}, + }, + tables::OpenTables, tables::{BlockHeights, BlockInfos, Tables}, - types::BlockHash, - types::{Amount, AmountIndex, BlockHeight, KeyImage, PreRctOutputId}, + types::{Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId}, }; //---------------------------------------------------------------------------------------------------- DatabaseReadHandle @@ -203,14 +206,19 @@ fn map_request( let response = match request { R::BlockExtendedHeader(block) => block_extended_header(env, block), - R::BlockHash(block) => block_hash(env, block), - R::FilterUnknownHashes(hashes) => filter_unknown_hahses(env, hashes), - R::BlockExtendedHeaderInRange(range) => block_extended_header_in_range(env, range), + R::BlockHash(block, chain) => block_hash(env, block, chain), + R::FindBlock(_) => todo!("Add alt blocks to DB"), + R::FilterUnknownHashes(hashes) => filter_unknown_hashes(env, hashes), + R::BlockExtendedHeaderInRange(range, chain) => { + block_extended_header_in_range(env, range, chain) + } R::ChainHeight => chain_height(env), - R::GeneratedCoins => generated_coins(env), + R::GeneratedCoins(height) => generated_coins(env, height), R::Outputs(map) => outputs(env, map), R::NumberOutputsWithAmount(vec) => number_outputs_with_amount(env, vec), R::KeyImagesSpent(set) => key_images_spent(env, set), + R::CompactChainHistory => compact_chain_history(env), + R::FindFirstUnknown(block_ids) => find_first_unknown(env, &block_ids), }; if let Err(e) = response_sender.send(response) { @@ -307,20 +315,23 @@ fn block_extended_header(env: &ConcreteEnv, block_height: BlockHeight) -> Respon /// [`BCReadRequest::BlockHash`]. #[inline] -fn block_hash(env: &ConcreteEnv, block_height: BlockHeight) -> ResponseResult { +fn block_hash(env: &ConcreteEnv, block_height: BlockHeight, chain: Chain) -> ResponseResult { // Single-threaded, no `ThreadLocal` required. let env_inner = env.env_inner(); let tx_ro = env_inner.tx_ro()?; let table_block_infos = env_inner.open_db_ro::(&tx_ro)?; - Ok(BCResponse::BlockHash( - get_block_info(&block_height, &table_block_infos)?.block_hash, - )) + let block_hash = match chain { + Chain::Main => get_block_info(&block_height, &table_block_infos)?.block_hash, + Chain::Alt(_) => todo!("Add alt blocks to DB"), + }; + + Ok(BCResponse::BlockHash(block_hash)) } /// [`BCReadRequest::FilterUnknownHashes`]. #[inline] -fn filter_unknown_hahses(env: &ConcreteEnv, mut hashes: HashSet) -> ResponseResult { +fn filter_unknown_hashes(env: &ConcreteEnv, mut hashes: HashSet) -> ResponseResult { // Single-threaded, no `ThreadLocal` required. let env_inner = env.env_inner(); let tx_ro = env_inner.tx_ro()?; @@ -351,6 +362,7 @@ fn filter_unknown_hahses(env: &ConcreteEnv, mut hashes: HashSet) -> R fn block_extended_header_in_range( env: &ConcreteEnv, range: std::ops::Range, + chain: Chain, ) -> ResponseResult { // Prepare tx/tables in `ThreadLocal`. let env_inner = env.env_inner(); @@ -358,14 +370,17 @@ fn block_extended_header_in_range( let tables = thread_local(env); // Collect results using `rayon`. - let vec = range - .into_par_iter() - .map(|block_height| { - let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; - let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); - get_block_extended_header_from_height(&block_height, tables) - }) - .collect::, RuntimeError>>()?; + let vec = match chain { + Chain::Main => range + .into_par_iter() + .map(|block_height| { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + get_block_extended_header_from_height(&block_height, tables) + }) + .collect::, RuntimeError>>()?, + Chain::Alt(_) => todo!("Add alt blocks to DB"), + }; Ok(BCResponse::BlockExtendedHeaderInRange(vec)) } @@ -388,17 +403,14 @@ fn chain_height(env: &ConcreteEnv) -> ResponseResult { /// [`BCReadRequest::GeneratedCoins`]. #[inline] -fn generated_coins(env: &ConcreteEnv) -> ResponseResult { +fn generated_coins(env: &ConcreteEnv, height: u64) -> ResponseResult { // Single-threaded, no `ThreadLocal` required. let env_inner = env.env_inner(); let tx_ro = env_inner.tx_ro()?; - let table_block_heights = env_inner.open_db_ro::(&tx_ro)?; let table_block_infos = env_inner.open_db_ro::(&tx_ro)?; - let top_height = top_block_height(&table_block_heights)?; - Ok(BCResponse::GeneratedCoins(cumulative_generated_coins( - &top_height, + &height, &table_block_infos, )?)) } @@ -525,3 +537,81 @@ fn key_images_spent(env: &ConcreteEnv, key_images: HashSet) -> Respons Some(Err(e)) => Err(e), // A database error occurred. } } + +/// [`BCReadRequest::CompactChainHistory`] +fn compact_chain_history(env: &ConcreteEnv) -> ResponseResult { + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + + let table_block_heights = env_inner.open_db_ro::(&tx_ro)?; + let table_block_infos = env_inner.open_db_ro::(&tx_ro)?; + + let top_block_height = top_block_height(&table_block_heights)?; + + let top_block_info = get_block_info(&top_block_height, &table_block_infos)?; + let cumulative_difficulty = combine_low_high_bits_to_u128( + top_block_info.cumulative_difficulty_low, + top_block_info.cumulative_difficulty_high, + ); + + /// The amount of top block IDs in the compact chain. + const INITIAL_BLOCKS: u64 = 11; + + // rayon is not used here because the amount of block IDs is expected to be small. + let mut block_ids = (0..) + .map(compact_history_index_to_height_offset::) + .map_while(|i| top_block_height.checked_sub(i)) + .map(|height| Ok(get_block_info(&height, &table_block_infos)?.block_hash)) + .collect::, RuntimeError>>()?; + + if compact_history_genesis_not_included::(top_block_height) { + block_ids.push(get_block_info(&0, &table_block_infos)?.block_hash); + } + + Ok(BCResponse::CompactChainHistory { + cumulative_difficulty, + block_ids, + }) +} + +/// [`BCReadRequest::FindFirstUnknown`] +/// +/// # Invariant +/// `block_ids` must be sorted in chronological block order, or else +/// the returned result is unspecified and meaningless, as this function +/// performs a binary search. +fn find_first_unknown(env: &ConcreteEnv, block_ids: &[BlockHash]) -> ResponseResult { + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + + let table_block_heights = env_inner.open_db_ro::(&tx_ro)?; + + let mut err = None; + + // Do a binary search to find the first unknown block in the batch. + let idx = + block_ids.partition_point( + |block_id| match block_exists(block_id, &table_block_heights) { + Ok(exists) => exists, + Err(e) => { + err.get_or_insert(e); + // if this happens the search is scrapped, just return `false` back. + false + } + }, + ); + + if let Some(e) = err { + return Err(e); + } + + Ok(if idx == block_ids.len() { + BCResponse::FindFirstUnknown(None) + } else if idx == 0 { + BCResponse::FindFirstUnknown(Some((0, 0))) + } else { + let last_known_height = get_block_height(&block_ids[idx - 1], &table_block_heights)?; + + BCResponse::FindFirstUnknown(Some((idx, last_known_height + 1))) + }) +} diff --git a/storage/blockchain/src/service/tests.rs b/storage/blockchain/src/service/tests.rs index d1634749..c00e32f3 100644 --- a/storage/blockchain/src/service/tests.rs +++ b/storage/blockchain/src/service/tests.rs @@ -19,19 +19,18 @@ use cuprate_database::{ConcreteEnv, DatabaseIter, DatabaseRo, Env, EnvInner, Run use cuprate_test_utils::data::{block_v16_tx0, block_v1_tx2, block_v9_tx3}; use cuprate_types::{ blockchain::{BCReadRequest, BCResponse, BCWriteRequest}, - OutputOnChain, VerifiedBlockInformation, + Chain, OutputOnChain, VerifiedBlockInformation, }; use crate::{ config::ConfigBuilder, - open_tables::OpenTables, ops::{ block::{get_block_extended_header_from_height, get_block_info}, blockchain::chain_height, output::id_to_output_on_chain, }, service::{init, DatabaseReadHandle, DatabaseWriteHandle}, - tables::{Tables, TablesIter}, + tables::{OpenTables, Tables, TablesIter}, tests::AssertTableLen, types::{Amount, AmountIndex, PreRctOutputId}, }; @@ -139,10 +138,15 @@ async fn test_template( Err(RuntimeError::KeyNotFound) }; + let test_chain_height = chain_height(tables.block_heights()).unwrap(); + let chain_height = { - let height = chain_height(tables.block_heights()).unwrap(); - let block_info = get_block_info(&height.saturating_sub(1), tables.block_infos()).unwrap(); - Ok(BCResponse::ChainHeight(height, block_info.block_hash)) + let block_info = + get_block_info(&test_chain_height.saturating_sub(1), tables.block_infos()).unwrap(); + Ok(BCResponse::ChainHeight( + test_chain_height, + block_info.block_hash, + )) }; let cumulative_generated_coins = Ok(BCResponse::GeneratedCoins(cumulative_generated_coins)); @@ -183,12 +187,21 @@ async fn test_template( BCReadRequest::BlockExtendedHeader(1), extended_block_header_1, ), - (BCReadRequest::BlockHash(0), block_hash_0), - (BCReadRequest::BlockHash(1), block_hash_1), - (BCReadRequest::BlockExtendedHeaderInRange(0..1), range_0_1), - (BCReadRequest::BlockExtendedHeaderInRange(0..2), range_0_2), + (BCReadRequest::BlockHash(0, Chain::Main), block_hash_0), + (BCReadRequest::BlockHash(1, Chain::Main), block_hash_1), + ( + BCReadRequest::BlockExtendedHeaderInRange(0..1, Chain::Main), + range_0_1, + ), + ( + BCReadRequest::BlockExtendedHeaderInRange(0..2, Chain::Main), + range_0_2, + ), (BCReadRequest::ChainHeight, chain_height), - (BCReadRequest::GeneratedCoins, cumulative_generated_coins), + ( + BCReadRequest::GeneratedCoins(test_chain_height), + cumulative_generated_coins, + ), (BCReadRequest::NumberOutputsWithAmount(num_req), num_resp), (BCReadRequest::KeyImagesSpent(ki_req), ki_resp), ] { diff --git a/storage/blockchain/src/service/write.rs b/storage/blockchain/src/service/write.rs index 42d96941..041ae7b6 100644 --- a/storage/blockchain/src/service/write.rs +++ b/storage/blockchain/src/service/write.rs @@ -16,8 +16,8 @@ use cuprate_types::{ }; use crate::{ - open_tables::OpenTables, service::types::{ResponseReceiver, ResponseResult, ResponseSender}, + tables::OpenTables, }; //---------------------------------------------------------------------------------------------------- Constants diff --git a/storage/blockchain/src/tables.rs b/storage/blockchain/src/tables.rs index 447faa6a..caac7873 100644 --- a/storage/blockchain/src/tables.rs +++ b/storage/blockchain/src/tables.rs @@ -4,7 +4,7 @@ //! This module contains all the table definitions used by `cuprate_blockchain`. //! //! The zero-sized structs here represents the table type; -//! they all are essentially marker types that implement [`Table`]. +//! they all are essentially marker types that implement [`cuprate_database::Table`]. //! //! Table structs are `CamelCase`, and their static string //! names used by the actual database backend are `snake_case`. @@ -14,311 +14,14 @@ //! # Traits //! This module also contains a set of traits for //! accessing _all_ tables defined here at once. -//! -//! For example, this is the object returned by [`OpenTables::open_tables`](crate::OpenTables::open_tables). //---------------------------------------------------------------------------------------------------- Import -use cuprate_database::{DatabaseIter, DatabaseRo, DatabaseRw, Table}; - use crate::types::{ Amount, AmountIndex, AmountIndices, BlockBlob, BlockHash, BlockHeight, BlockInfo, KeyImage, Output, PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RctOutput, TxBlob, TxHash, TxId, UnlockTime, }; -//---------------------------------------------------------------------------------------------------- Sealed -/// Private module, should not be accessible outside this crate. -pub(super) mod private { - /// Private sealed trait. - /// - /// Cannot be implemented outside this crate. - pub trait Sealed {} -} - -//---------------------------------------------------------------------------------------------------- `trait Tables[Mut]` -/// Creates: -/// - `pub trait Tables` -/// - `pub trait TablesIter` -/// - `pub trait TablesMut` -/// - Blanket implementation for `(tuples, containing, all, open, database, tables, ...)` -/// -/// For why this exists, see: . -macro_rules! define_trait_tables { - ($( - // The `T: Table` type The index in a tuple - // | containing all tables - // v v - $table:ident => $index:literal - ),* $(,)?) => { paste::paste! { - /// Object containing all opened [`Table`]s in read-only mode. - /// - /// This is an encapsulated object that contains all - /// available [`Table`]'s in read-only mode. - /// - /// It is a `Sealed` trait and is only implemented on a - /// `(tuple, containing, all, table, types, ...)`. - /// - /// This is used to return a _single_ object from functions like - /// [`OpenTables::open_tables`](crate::OpenTables::open_tables) rather - /// than the tuple containing the tables itself. - /// - /// To replace `tuple.0` style indexing, `field_accessor_functions()` - /// are provided on this trait, which essentially map the object to - /// fields containing the particular database table, for example: - /// ```rust,ignore - /// let tables = open_tables(); - /// - /// // The accessor function `block_infos()` returns the field - /// // containing an open database table for `BlockInfos`. - /// let _ = tables.block_infos(); - /// ``` - /// - /// See also: - /// - [`TablesMut`] - /// - [`TablesIter`] - pub trait Tables: private::Sealed { - // This expands to creating `fn field_accessor_functions()` - // for each passed `$table` type. - // - // It is essentially a mapping to the field - // containing the proper opened database table. - // - // The function name of the function is - // the table type in `snake_case`, e.g., `block_info_v1s()`. - $( - /// Access an opened - #[doc = concat!("[`", stringify!($table), "`]")] - /// database. - fn [<$table:snake>](&self) -> &impl DatabaseRo<$table>; - )* - - /// This returns `true` if all tables are empty. - /// - /// # Errors - /// This returns errors on regular database errors. - fn all_tables_empty(&self) -> Result; - } - - /// Object containing all opened [`Table`]s in read + iter mode. - /// - /// This is the same as [`Tables`] but includes `_iter()` variants. - /// - /// Note that this trait is a supertrait of `Tables`, - /// as in it can use all of its functions as well. - /// - /// See [`Tables`] for documentation - this trait exists for the same reasons. - pub trait TablesIter: private::Sealed + Tables { - $( - /// Access an opened read-only + iterable - #[doc = concat!("[`", stringify!($table), "`]")] - /// database. - fn [<$table:snake _iter>](&self) -> &(impl DatabaseRo<$table> + DatabaseIter<$table>); - )* - } - - /// Object containing all opened [`Table`]s in write mode. - /// - /// This is the same as [`Tables`] but for mutable accesses. - /// - /// Note that this trait is a supertrait of `Tables`, - /// as in it can use all of its functions as well. - /// - /// See [`Tables`] for documentation - this trait exists for the same reasons. - pub trait TablesMut: private::Sealed + Tables { - $( - /// Access an opened - #[doc = concat!("[`", stringify!($table), "`]")] - /// database. - fn [<$table:snake _mut>](&mut self) -> &mut impl DatabaseRw<$table>; - )* - } - - // Implement `Sealed` for all table types. - impl<$([<$table:upper>]),*> private::Sealed for ($([<$table:upper>]),*) {} - - // This creates a blanket-implementation for - // `(tuple, containing, all, table, types)`. - // - // There is a generic defined here _for each_ `$table` input. - // Specifically, the generic letters are just the table types in UPPERCASE. - // Concretely, this expands to something like: - // ```rust - // impl - // ``` - impl<$([<$table:upper>]),*> Tables - // We are implementing `Tables` on a tuple that - // contains all those generics specified, i.e., - // a tuple containing all open table types. - // - // Concretely, this expands to something like: - // ```rust - // (BLOCKINFOSV1S, BLOCKINFOSV2S, BLOCKINFOSV3S, [...]) - // ``` - // which is just a tuple of the generics defined above. - for ($([<$table:upper>]),*) - where - // This expands to a where bound that asserts each element - // in the tuple implements some database table type. - // - // Concretely, this expands to something like: - // ```rust - // BLOCKINFOSV1S: DatabaseRo + DatabaseIter, - // BLOCKINFOSV2S: DatabaseRo + DatabaseIter, - // [...] - // ``` - $( - [<$table:upper>]: DatabaseRo<$table>, - )* - { - $( - // The function name of the accessor function is - // the table type in `snake_case`, e.g., `block_info_v1s()`. - #[inline] - fn [<$table:snake>](&self) -> &impl DatabaseRo<$table> { - // The index of the database table in - // the tuple implements the table trait. - &self.$index - } - )* - - fn all_tables_empty(&self) -> Result { - $( - if !DatabaseRo::is_empty(&self.$index)? { - return Ok(false); - } - )* - Ok(true) - } - } - - // This is the same as the above - // `Tables`, but for `TablesIter`. - impl<$([<$table:upper>]),*> TablesIter - for ($([<$table:upper>]),*) - where - $( - [<$table:upper>]: DatabaseRo<$table> + DatabaseIter<$table>, - )* - { - $( - // The function name of the accessor function is - // the table type in `snake_case` + `_iter`, e.g., `block_info_v1s_iter()`. - #[inline] - fn [<$table:snake _iter>](&self) -> &(impl DatabaseRo<$table> + DatabaseIter<$table>) { - &self.$index - } - )* - } - - // This is the same as the above - // `Tables`, but for `TablesMut`. - impl<$([<$table:upper>]),*> TablesMut - for ($([<$table:upper>]),*) - where - $( - [<$table:upper>]: DatabaseRw<$table>, - )* - { - $( - // The function name of the mutable accessor function is - // the table type in `snake_case` + `_mut`, e.g., `block_info_v1s_mut()`. - #[inline] - fn [<$table:snake _mut>](&mut self) -> &mut impl DatabaseRw<$table> { - &mut self.$index - } - )* - } - }}; -} - -// Input format: $table_type => $index -// -// The $index: -// - Simply increments by 1 for each table -// - Must be 0.. -// - Must end at the total amount of table types - 1 -// -// Compile errors will occur if these aren't satisfied. -// -// $index is just the `tuple.$index`, as the above [`define_trait_tables`] -// macro has a blanket impl for `(all, table, types, ...)` and we must map -// each type to a tuple index explicitly. -// -// FIXME: there's definitely an automatic way to this :) -define_trait_tables! { - BlockInfos => 0, - BlockBlobs => 1, - BlockHeights => 2, - KeyImages => 3, - NumOutputs => 4, - PrunedTxBlobs => 5, - PrunableHashes => 6, - Outputs => 7, - PrunableTxBlobs => 8, - RctOutputs => 9, - TxBlobs => 10, - TxIds => 11, - TxHeights => 12, - TxOutputs => 13, - TxUnlockTime => 14, -} - -//---------------------------------------------------------------------------------------------------- Table macro -/// Create all tables, should be used _once_. -/// -/// Generating this macro once and using `$()*` is probably -/// faster for compile times than calling the macro _per_ table. -/// -/// All tables are zero-sized table structs, and implement the `Table` trait. -/// -/// Table structs are automatically `CamelCase`, -/// and their static string names are automatically `snake_case`. -macro_rules! tables { - ( - $( - $(#[$attr:meta])* // Documentation and any `derive`'s. - $table:ident, // The table name + doubles as the table struct name. - $key:ty => // Key type. - $value:ty // Value type. - ),* $(,)? - ) => { - paste::paste! { $( - // Table struct. - $(#[$attr])* - // The below test show the `snake_case` table name in cargo docs. - #[doc = concat!("- Key: [`", stringify!($key), "`]")] - #[doc = concat!("- Value: [`", stringify!($value), "`]")] - /// - /// ## Table Name - /// ```rust - /// # use cuprate_blockchain::{*,tables::*}; - /// use cuprate_database::Table; - #[doc = concat!( - "assert_eq!(", - stringify!([<$table:camel>]), - "::NAME, \"", - stringify!([<$table:snake>]), - "\");", - )] - /// ``` - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] - pub struct [<$table:camel>]; - - // Implement the `Sealed` in this file. - // Required by `Table`. - impl private::Sealed for [<$table:camel>] {} - - // Table trait impl. - impl Table for [<$table:camel>] { - const NAME: &'static str = stringify!([<$table:snake>]); - type Key = $key; - type Value = $value; - } - )* } - }; -} - //---------------------------------------------------------------------------------------------------- Tables // Notes: // - Keep this sorted A-Z (by table name) @@ -326,23 +29,23 @@ macro_rules! tables { // - If adding/changing a table also edit: // - the tests in `src/backend/tests.rs` // - `call_fn_on_all_tables_or_early_return!()` macro in `src/open_tables.rs` -tables! { +cuprate_database::define_tables! { /// Serialized block blobs (bytes). /// /// Contains the serialized version of all blocks. - BlockBlobs, + 0 => BlockBlobs, BlockHeight => BlockBlob, /// Block heights. /// /// Contains the height of all blocks. - BlockHeights, + 1 => BlockHeights, BlockHash => BlockHeight, /// Block information. /// /// Contains metadata of all blocks. - BlockInfos, + 2 => BlockInfos, BlockHeight => BlockInfo, /// Set of key images. @@ -351,38 +54,38 @@ tables! { /// /// This table has `()` as the value type, as in, /// it is a set of key images. - KeyImages, + 3 => KeyImages, KeyImage => (), /// Maps an output's amount to the number of outputs with that amount. /// /// For example, if there are 5 outputs with `amount = 123` /// then calling `get(123)` on this table will return 5. - NumOutputs, + 4 => NumOutputs, Amount => u64, /// Pre-RCT output data. - Outputs, + 5 => Outputs, PreRctOutputId => Output, /// Pruned transaction blobs (bytes). /// /// Contains the pruned portion of serialized transaction data. - PrunedTxBlobs, + 6 => PrunedTxBlobs, TxId => PrunedBlob, /// Prunable transaction blobs (bytes). /// /// Contains the prunable portion of serialized transaction data. // SOMEDAY: impl when `monero-serai` supports pruning - PrunableTxBlobs, + 7 => PrunableTxBlobs, TxId => PrunableBlob, /// Prunable transaction hashes. /// /// Contains the prunable portion of transaction hashes. // SOMEDAY: impl when `monero-serai` supports pruning - PrunableHashes, + 8 => PrunableHashes, TxId => PrunableHash, // SOMEDAY: impl a properties table: @@ -392,40 +95,40 @@ tables! { // StorableString => StorableVec, /// RCT output data. - RctOutputs, + 9 => RctOutputs, AmountIndex => RctOutput, /// Transaction blobs (bytes). /// /// Contains the serialized version of all transactions. // SOMEDAY: remove when `monero-serai` supports pruning - TxBlobs, + 10 => TxBlobs, TxId => TxBlob, /// Transaction indices. /// /// Contains the indices all transactions. - TxIds, + 11 => TxIds, TxHash => TxId, /// Transaction heights. /// /// Contains the block height associated with all transactions. - TxHeights, + 12 => TxHeights, TxId => BlockHeight, /// Transaction outputs. /// /// Contains the list of `AmountIndex`'s of the /// outputs associated with all transactions. - TxOutputs, + 13 => TxOutputs, TxId => AmountIndices, /// Transaction unlock time. /// /// Contains the unlock time of transactions IF they have one. /// Transactions without unlock times will not exist in this table. - TxUnlockTime, + 14 => TxUnlockTime, TxId => UnlockTime, } diff --git a/storage/blockchain/src/tests.rs b/storage/blockchain/src/tests.rs index ec2f18eb..65527e10 100644 --- a/storage/blockchain/src/tests.rs +++ b/storage/blockchain/src/tests.rs @@ -11,7 +11,10 @@ use pretty_assertions::assert_eq; use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner}; -use crate::{config::ConfigBuilder, open_tables::OpenTables, tables::Tables}; +use crate::{ + config::ConfigBuilder, + tables::{OpenTables, Tables}, +}; //---------------------------------------------------------------------------------------------------- Struct /// Named struct to assert the length of all tables. diff --git a/storage/blockchain/src/types.rs b/storage/blockchain/src/types.rs index ca6d06cc..08cde314 100644 --- a/storage/blockchain/src/types.rs +++ b/storage/blockchain/src/types.rs @@ -46,7 +46,7 @@ use bytemuck::{Pod, Zeroable}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use cuprate_database::StorableVec; +use cuprate_database::{Key, StorableVec}; //---------------------------------------------------------------------------------------------------- Aliases // These type aliases exist as many Monero-related types are the exact same. @@ -143,6 +143,8 @@ pub struct PreRctOutputId { pub amount_index: AmountIndex, } +impl Key for PreRctOutputId {} + //---------------------------------------------------------------------------------------------------- BlockInfoV3 /// Block information. /// diff --git a/storage/database/Cargo.toml b/storage/database/Cargo.toml index 887f1b60..a70457f5 100644 --- a/storage/database/Cargo.toml +++ b/storage/database/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/Cuprate/cuprate/tree/main/storage/database" keywords = ["cuprate", "database"] [features] -default = ["heed"] +# default = ["heed"] # default = ["redb"] # default = ["redb-memory"] heed = ["dep:heed"] @@ -21,6 +21,7 @@ bytemuck = { version = "1.14.3", features = ["must_cast", "derive", "min_const_ bytes = { workspace = true } cfg-if = { workspace = true } page_size = { version = "0.6.0" } # Needed for database resizes, they must be a multiple of the OS page size. +paste = { workspace = true } thiserror = { workspace = true } # Optional features. diff --git a/storage/database/README.md b/storage/database/README.md index d7a9b92f..aed738eb 100644 --- a/storage/database/README.md +++ b/storage/database/README.md @@ -6,10 +6,10 @@ For a high-level overview, see the database section in [Cuprate's architecture book](https://architecture.cuprate.org). If you need blockchain specific capabilities, consider using the higher-level -`cuprate-blockchain` crate which builds upon this one. +[`cuprate-blockchain`](https://doc.cuprate.org/cuprate_blockchain) crate which builds upon this one. # Purpose -This crate abstracts various database backends with traits. The databases are: +This crate abstracts various database backends with traits. All backends have the following attributes: - [Embedded](https://en.wikipedia.org/wiki/Embedded_database) @@ -19,6 +19,10 @@ All backends have the following attributes: - Are table oriented (`"table_name" -> (key, value)`) - Allows concurrent readers +The currently implemented backends are: +- [`heed`](https://github.com/meilisearch/heed) (LMDB) +- [`redb`](https://github.com/cberner/redb) + # Terminology To be more clear on some terms used in this crate: @@ -26,17 +30,17 @@ To be more clear on some terms used in this crate: |------------------|--------------------------------------| | `Env` | The 1 database environment, the "whole" thing | `DatabaseR{o,w}` | A _actively open_ readable/writable `key/value` store -| `Table` | Solely the metadata of a `cuprate_database` (the `key` and `value` types, and the name) +| `Table` | Solely the metadata of a `Database` (the `key` and `value` types, and the name) | `TxR{o,w}` | A read/write transaction -| `Storable` | A data that type can be stored in the database +| `Storable` | A data type that can be stored in the database -The dataflow is `Env` -> `Tx` -> `cuprate_database` +The flow is `Env` -> `Tx` -> `Database` Which reads as: 1. You have a database `Environment` 1. You open up a `Transaction` -1. You open a particular `Table` from that `Environment`, getting a `cuprate_database` -1. You can now read/write data from/to that `cuprate_database` +1. You open a particular `Table` from that `Environment`, getting a `Database` +1. You can now read/write data from/to that `Database` # Concrete types You should _not_ rely on the concrete type of any abstracted backend. @@ -80,6 +84,15 @@ and use `` everywhere it is stored instead. This would allow generic-backed dynamic runtime selection of the database backend, i.e. the user can select which database backend they use. --> +# Defining tables +Most likely, your crate building on-top of `cuprate_database` will +want to define all tables used at compile time. + +If this is the case, consider using the [`define_tables`] macro +to bulk generate zero-sized marker types that implement [`Table`]. + +This macro also generates other convenient traits specific to _your_ tables. + # Feature flags Different database backends are enabled by the feature flags: - `heed` (LMDB) diff --git a/storage/database/src/backend/heed/env.rs b/storage/database/src/backend/heed/env.rs index 14f9777d..0c2847fb 100644 --- a/storage/database/src/backend/heed/env.rs +++ b/storage/database/src/backend/heed/env.rs @@ -7,26 +7,23 @@ use std::{ sync::{RwLock, RwLockReadGuard}, }; -use heed::{EnvFlags, EnvOpenOptions}; +use heed::{DatabaseFlags, EnvFlags, EnvOpenOptions}; use crate::{ backend::heed::{ database::{HeedTableRo, HeedTableRw}, + storable::StorableHeed, types::HeedDb, }, config::{Config, SyncMode}, database::{DatabaseIter, DatabaseRo, DatabaseRw}, env::{Env, EnvInner}, error::{InitError, RuntimeError}, + key::{Key, KeyCompare}, resize::ResizeAlgorithm, table::Table, }; -//---------------------------------------------------------------------------------------------------- Consts -/// Panic message when there's a table missing. -const PANIC_MSG_MISSING_TABLE: &str = - "cuprate_database::Env should uphold the invariant that all tables are already created"; - //---------------------------------------------------------------------------------------------------- ConcreteEnv /// A strongly typed, concrete database environment, backed by `heed`. pub struct ConcreteEnv { @@ -247,27 +244,34 @@ impl Env for ConcreteEnv { } //---------------------------------------------------------------------------------------------------- EnvInner Impl -impl<'env> EnvInner<'env, heed::RoTxn<'env>, RefCell>> - for RwLockReadGuard<'env, heed::Env> +impl<'env> EnvInner<'env> for RwLockReadGuard<'env, heed::Env> where Self: 'env, { + type Ro<'a> = heed::RoTxn<'a>; + + type Rw<'a> = RefCell>; + #[inline] - fn tx_ro(&'env self) -> Result, RuntimeError> { + fn tx_ro(&self) -> Result, RuntimeError> { Ok(self.read_txn()?) } #[inline] - fn tx_rw(&'env self) -> Result>, RuntimeError> { + fn tx_rw(&self) -> Result, RuntimeError> { Ok(RefCell::new(self.write_txn()?)) } #[inline] fn open_db_ro( &self, - tx_ro: &heed::RoTxn<'env>, + tx_ro: &Self::Ro<'_>, ) -> Result + DatabaseIter, RuntimeError> { // Open up a read-only database using our table's const metadata. + // + // INVARIANT: LMDB caches the ordering / comparison function from [`EnvInner::create_db`], + // and we're relying on that since we aren't setting that here. + // Ok(HeedTableRo { db: self .open_database(tx_ro, Some(T::NAME))? @@ -279,40 +283,66 @@ where #[inline] fn open_db_rw( &self, - tx_rw: &RefCell>, + tx_rw: &Self::Rw<'_>, ) -> Result, RuntimeError> { // Open up a read/write database using our table's const metadata. + // + // INVARIANT: LMDB caches the ordering / comparison function from [`EnvInner::create_db`], + // and we're relying on that since we aren't setting that here. + // Ok(HeedTableRw { db: self.create_database(&mut tx_rw.borrow_mut(), Some(T::NAME))?, tx_rw, }) } - fn create_db(&self, tx_rw: &RefCell>) -> Result<(), RuntimeError> { - // INVARIANT: `heed` creates tables with `open_database` if they don't exist. - self.open_db_rw::(tx_rw)?; + fn create_db(&self, tx_rw: &Self::Rw<'_>) -> Result<(), RuntimeError> { + // Create a database using our: + // - [`Table`]'s const metadata. + // - (potentially) our [`Key`] comparison function + let mut tx_rw = tx_rw.borrow_mut(); + let mut db = self.database_options(); + db.name(T::NAME); + + // Set the key comparison behavior. + match ::KEY_COMPARE { + // Use LMDB's default comparison function. + KeyCompare::Default => { + db.create(&mut tx_rw)?; + } + + // Instead of setting a custom [`heed::Comparator`], + // use this LMDB flag; it is ~10% faster. + KeyCompare::Number => { + db.flags(DatabaseFlags::INTEGER_KEY).create(&mut tx_rw)?; + } + + // Use a custom comparison function if specified. + KeyCompare::Custom(_) => { + db.key_comparator::>() + .create(&mut tx_rw)?; + } + } + Ok(()) } #[inline] - fn clear_db( - &self, - tx_rw: &mut RefCell>, - ) -> Result<(), RuntimeError> { + fn clear_db(&self, tx_rw: &mut Self::Rw<'_>) -> Result<(), RuntimeError> { let tx_rw = tx_rw.get_mut(); - // Open the table first... + // Open the table. We don't care about flags or key + // comparison behavior since we're clearing it anyway. let db: HeedDb = self .open_database(tx_rw, Some(T::NAME))? - .expect(PANIC_MSG_MISSING_TABLE); + .ok_or(RuntimeError::TableNotFound)?; - // ...then clear it. - Ok(db.clear(tx_rw)?) + db.clear(tx_rw)?; + + Ok(()) } } //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] -mod test { - // use super::*; -} +mod tests {} diff --git a/storage/database/src/backend/heed/storable.rs b/storage/database/src/backend/heed/storable.rs index 83442212..3566e88f 100644 --- a/storage/database/src/backend/heed/storable.rs +++ b/storage/database/src/backend/heed/storable.rs @@ -1,11 +1,11 @@ //! `cuprate_database::Storable` <-> `heed` serde trait compatibility layer. //---------------------------------------------------------------------------------------------------- Use -use std::{borrow::Cow, marker::PhantomData}; +use std::{borrow::Cow, cmp::Ordering, marker::PhantomData}; use heed::{BoxedError, BytesDecode, BytesEncode}; -use crate::storable::Storable; +use crate::{storable::Storable, Key}; //---------------------------------------------------------------------------------------------------- StorableHeed /// The glue struct that implements `heed`'s (de)serialization @@ -16,7 +16,19 @@ pub(super) struct StorableHeed(PhantomData) where T: Storable + ?Sized; -//---------------------------------------------------------------------------------------------------- BytesDecode +//---------------------------------------------------------------------------------------------------- Key +// If `Key` is also implemented, this can act as the comparison function. +impl heed::Comparator for StorableHeed +where + T: Key, +{ + #[inline] + fn compare(a: &[u8], b: &[u8]) -> Ordering { + ::KEY_COMPARE.as_compare_fn::()(a, b) + } +} + +//---------------------------------------------------------------------------------------------------- BytesDecode/Encode impl<'a, T> BytesDecode<'a> for StorableHeed where T: Storable + 'static, @@ -30,7 +42,6 @@ where } } -//---------------------------------------------------------------------------------------------------- BytesEncode impl<'a, T> BytesEncode<'a> for StorableHeed where T: Storable + ?Sized + 'a, @@ -57,6 +68,42 @@ mod test { // - simplify trait bounds // - make sure the right function is being called + #[test] + /// Assert key comparison behavior is correct. + fn compare() { + fn test(left: T, right: T, expected: Ordering) + where + T: Key + Ord + 'static, + { + println!("left: {left:?}, right: {right:?}, expected: {expected:?}"); + assert_eq!( + as heed::Comparator>::compare( + & as heed::BytesEncode>::bytes_encode(&left).unwrap(), + & as heed::BytesEncode>::bytes_encode(&right).unwrap() + ), + expected + ); + } + + // Value comparison + test::(0, 255, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + + // Byte comparison + test::<[u8; 2]>([1, 1], [1, 0], Ordering::Greater); + test::<[u8; 3]>([1, 2, 3], [1, 2, 3], Ordering::Equal); + } + #[test] /// Assert `BytesEncode::bytes_encode` is accurate. fn bytes_encode() { diff --git a/storage/database/src/backend/heed/types.rs b/storage/database/src/backend/heed/types.rs index 6a99d0df..10f57e67 100644 --- a/storage/database/src/backend/heed/types.rs +++ b/storage/database/src/backend/heed/types.rs @@ -5,4 +5,7 @@ use crate::backend::heed::storable::StorableHeed; //---------------------------------------------------------------------------------------------------- Types /// The concrete database type for `heed`, usable for reads and writes. +// +// Key type Value type +// v v pub(super) type HeedDb = heed::Database, StorableHeed>; diff --git a/storage/database/src/backend/redb/env.rs b/storage/database/src/backend/redb/env.rs index 3ff195c1..a405ea72 100644 --- a/storage/database/src/backend/redb/env.rs +++ b/storage/database/src/backend/redb/env.rs @@ -56,8 +56,9 @@ impl Env for ConcreteEnv { // // should we use that instead of Immediate? SyncMode::Safe => redb::Durability::Immediate, - SyncMode::Async => redb::Durability::Eventual, - SyncMode::Fast => redb::Durability::None, + // FIXME: `Fast` maps to `Eventual` instead of `None` because of: + // + SyncMode::Async | SyncMode::Fast => redb::Durability::Eventual, // SOMEDAY: dynamic syncs are not implemented. SyncMode::FastThenSafe | SyncMode::Threshold(_) => unimplemented!(), }; @@ -118,18 +119,20 @@ impl Env for ConcreteEnv { } //---------------------------------------------------------------------------------------------------- EnvInner Impl -impl<'env> EnvInner<'env, redb::ReadTransaction, redb::WriteTransaction> - for (&'env redb::Database, redb::Durability) +impl<'env> EnvInner<'env> for (&'env redb::Database, redb::Durability) where Self: 'env, { + type Ro<'a> = redb::ReadTransaction; + type Rw<'a> = redb::WriteTransaction; + #[inline] - fn tx_ro(&'env self) -> Result { + fn tx_ro(&self) -> Result { Ok(self.0.begin_read()?) } #[inline] - fn tx_rw(&'env self) -> Result { + fn tx_rw(&self) -> Result { // `redb` has sync modes on the TX level, unlike heed, // which sets it at the Environment level. // @@ -142,7 +145,7 @@ where #[inline] fn open_db_ro( &self, - tx_ro: &redb::ReadTransaction, + tx_ro: &Self::Ro<'_>, ) -> Result + DatabaseIter, RuntimeError> { // Open up a read-only database using our `T: Table`'s const metadata. let table: redb::TableDefinition<'static, StorableRedb, StorableRedb> = @@ -154,7 +157,7 @@ where #[inline] fn open_db_rw( &self, - tx_rw: &redb::WriteTransaction, + tx_rw: &Self::Rw<'_>, ) -> Result, RuntimeError> { // Open up a read/write database using our `T: Table`'s const metadata. let table: redb::TableDefinition<'static, StorableRedb, StorableRedb> = @@ -189,7 +192,10 @@ where // 3. So it's not being used to open a table since that needs `&tx_rw` // // Reader-open tables do not affect this, if they're open the below is still OK. - redb::WriteTransaction::delete_table(tx_rw, table)?; + if !redb::WriteTransaction::delete_table(tx_rw, table)? { + return Err(RuntimeError::TableNotFound); + } + // Re-create the table. // `redb` creates tables if they don't exist, so this should never panic. redb::WriteTransaction::open_table(tx_rw, table)?; @@ -200,6 +206,4 @@ where //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] -mod test { - // use super::*; -} +mod tests {} diff --git a/storage/database/src/backend/redb/storable.rs b/storage/database/src/backend/redb/storable.rs index 6735fec0..abf2e71b 100644 --- a/storage/database/src/backend/redb/storable.rs +++ b/storage/database/src/backend/redb/storable.rs @@ -25,7 +25,7 @@ where { #[inline] fn compare(left: &[u8], right: &[u8]) -> Ordering { - ::compare(left, right) + ::KEY_COMPARE.as_compare_fn::()(left, right) } } @@ -93,8 +93,21 @@ mod test { ); } - test::(-1, 2, Ordering::Greater); // bytes are greater, not the value - test::(0, 1, Ordering::Less); + // Value comparison + test::(0, 255, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(0, 256, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + test::(-1, 2, Ordering::Less); + + // Byte comparison test::<[u8; 2]>([1, 1], [1, 0], Ordering::Greater); test::<[u8; 3]>([1, 2, 3], [1, 2, 3], Ordering::Equal); } diff --git a/storage/database/src/backend/tests.rs b/storage/database/src/backend/tests.rs index df80b631..ac6b5927 100644 --- a/storage/database/src/backend/tests.rs +++ b/storage/database/src/backend/tests.rs @@ -156,6 +156,20 @@ fn non_manual_resize_2() { env.current_map_size(); } +/// Tests that [`EnvInner::clear_db`] will return +/// [`RuntimeError::TableNotFound`] if the table doesn't exist. +#[test] +fn clear_db_table_not_found() { + let (env, _tmpdir) = tmp_concrete_env(); + let env_inner = env.env_inner(); + let mut tx_rw = env_inner.tx_rw().unwrap(); + let err = env_inner.clear_db::(&mut tx_rw).unwrap_err(); + assert!(matches!(err, RuntimeError::TableNotFound)); + + env_inner.create_db::(&tx_rw).unwrap(); + env_inner.clear_db::(&mut tx_rw).unwrap(); +} + /// Test all `DatabaseR{o,w}` operations. #[test] fn db_read_write() { @@ -165,11 +179,11 @@ fn db_read_write() { let mut table = env_inner.open_db_rw::(&tx_rw).unwrap(); /// The (1st) key. - const KEY: u8 = 0; + const KEY: u32 = 0; /// The expected value. const VALUE: u64 = 0; /// How many `(key, value)` pairs will be inserted. - const N: u8 = 100; + const N: u32 = 100; /// Assert a u64 is the same as `VALUE`. fn assert_value(value: u64) { @@ -323,19 +337,35 @@ fn db_read_write() { /// Assert that `key`'s in database tables are sorted in /// an ordered B-Tree fashion, i.e. `min_value -> max_value`. +/// +/// And that it is true for integers, e.g. `0` -> `10`. #[test] fn tables_are_sorted() { let (env, _tmp) = tmp_concrete_env(); let env_inner = env.env_inner(); + + /// Range of keys to insert, `{0, 1, 2 ... 256}`. + const RANGE: std::ops::Range = 0..257; + + // Create tables and set flags / comparison flags. + { + let tx_rw = env_inner.tx_rw().unwrap(); + env_inner.create_db::(&tx_rw).unwrap(); + TxRw::commit(tx_rw).unwrap(); + } + let tx_rw = env_inner.tx_rw().unwrap(); let mut table = env_inner.open_db_rw::(&tx_rw).unwrap(); - // Insert `{5, 4, 3, 2, 1, 0}`, assert each new - // number inserted is the minimum `first()` value. - for key in (0..6).rev() { - table.put(&key, &123).unwrap(); + // Insert range, assert each new + // number inserted is the minimum `last()` value. + for key in RANGE { + table.put(&key, &0).unwrap(); + table.contains(&key).unwrap(); let (first, _) = table.first().unwrap(); - assert_eq!(first, key); + let (last, _) = table.last().unwrap(); + println!("first: {first}, last: {last}, key: {key}"); + assert_eq!(last, key); } drop(table); @@ -348,7 +378,7 @@ fn tables_are_sorted() { let table = env_inner.open_db_ro::(&tx_ro).unwrap(); let iter = table.iter().unwrap(); let keys = table.keys().unwrap(); - for ((i, iter), key) in (0..6).zip(iter).zip(keys) { + for ((i, iter), key) in RANGE.zip(iter).zip(keys) { let (iter, _) = iter.unwrap(); let key = key.unwrap(); assert_eq!(i, iter); @@ -359,14 +389,14 @@ fn tables_are_sorted() { let mut table = env_inner.open_db_rw::(&tx_rw).unwrap(); // Assert the `first()` values are the minimum, i.e. `{0, 1, 2}` - for key in 0..3 { + for key in [0, 1, 2] { let (first, _) = table.first().unwrap(); assert_eq!(first, key); table.delete(&key).unwrap(); } - // Assert the `last()` values are the maximum, i.e. `{5, 4, 3}` - for key in (3..6).rev() { + // Assert the `last()` values are the maximum, i.e. `{256, 255, 254}` + for key in [256, 255, 254] { let (last, _) = table.last().unwrap(); assert_eq!(last, key); table.delete(&key).unwrap(); diff --git a/storage/database/src/config/config.rs b/storage/database/src/config/config.rs index a5ecbb23..8a4ddbf2 100644 --- a/storage/database/src/config/config.rs +++ b/storage/database/src/config/config.rs @@ -160,7 +160,7 @@ pub struct Config { /// Set the number of slots in the reader table. /// /// This is only used in LMDB, see - /// . + /// [here](https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/mdb.c#L794-L799). /// /// By default, this value is [`READER_THREADS_DEFAULT`]. pub reader_threads: NonZeroUsize, diff --git a/storage/database/src/config/sync_mode.rs b/storage/database/src/config/sync_mode.rs index 1d203396..5a0cba52 100644 --- a/storage/database/src/config/sync_mode.rs +++ b/storage/database/src/config/sync_mode.rs @@ -84,7 +84,7 @@ pub enum SyncMode { /// /// This is expected to be very slow. /// - /// This matches: + /// This maps to: /// - LMDB without any special sync flags /// - [`redb::Durability::Immediate`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Immediate) Safe, @@ -96,7 +96,7 @@ pub enum SyncMode { /// each transaction commit will sync to disk, /// but only eventually, not necessarily immediately. /// - /// This matches: + /// This maps to: /// - [`MDB_MAPASYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#gab034ed0d8e5938090aef5ee0997f7e94) /// - [`redb::Durability::Eventual`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Eventual) Async, @@ -115,17 +115,25 @@ pub enum SyncMode { /// This is the fastest, yet unsafest option. /// /// It will cause the database to never _actively_ sync, - /// letting the OS decide when to flush data to disk. + /// letting the OS decide when to flush data to disk[^1]. /// - /// This matches: + /// This maps to: /// - [`MDB_NOSYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#ga5791dd1adb09123f82dd1f331209e12e) + [`MDB_MAPASYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#gab034ed0d8e5938090aef5ee0997f7e94) - /// - [`redb::Durability::None`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.None) + /// - [`redb::Durability::Eventual`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Eventual) /// - /// `monerod` reference: + /// [`monerod` reference](https://github.com/monero-project/monero/blob/7b7958bbd9d76375c47dc418b4adabba0f0b1785/src/blockchain_db/lmdb/db_lmdb.cpp#L1380-L1381). /// /// # Corruption /// In the case of a system crash, the database /// may become corrupted when using this option. + /// + /// + /// [^1]: Semantically, this variant would actually map to + /// [`redb::Durability::None`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.None), + /// however due to [`#149`](https://github.com/Cuprate/cuprate/issues/149), + /// this is not possible. As such, when using the `redb` backend, + /// transaction writes "should be persistent some time after `WriteTransaction::commit` returns." + /// Thus, [`SyncMode::Async`] will map to the same `redb::Durability::Eventual` as [`SyncMode::Fast`]. // // FIXME: we could call this `unsafe` // and use that terminology in the config file diff --git a/storage/database/src/env.rs b/storage/database/src/env.rs index 8491f58c..cae49733 100644 --- a/storage/database/src/env.rs +++ b/storage/database/src/env.rs @@ -24,15 +24,14 @@ use crate::{ /// /// # Lifetimes /// The lifetimes associated with `Env` have a sequential flow: -/// 1. `ConcreteEnv` -/// 2. `'env` -/// 3. `'tx` -/// 4. `'db` +/// ```text +/// Env -> Tx -> Database +/// ``` /// /// As in: /// - open database tables only live as long as... /// - transactions which only live as long as the... -/// - environment ([`EnvInner`]) +/// - database environment pub trait Env: Sized { //------------------------------------------------ Constants /// Does the database backend need to be manually @@ -62,17 +61,17 @@ pub trait Env: Sized { // For `heed`, this is just `heed::Env`, for `redb` this is // `(redb::Database, redb::Durability)` as each transaction // needs the sync mode set during creation. - type EnvInner<'env>: EnvInner<'env, Self::TxRo<'env>, Self::TxRw<'env>> + type EnvInner<'env>: EnvInner<'env> where Self: 'env; /// The read-only transaction type of the backend. - type TxRo<'env>: TxRo<'env> + 'env + type TxRo<'env>: TxRo<'env> where Self: 'env; /// The read/write transaction type of the backend. - type TxRw<'env>: TxRw<'env> + 'env + type TxRw<'env>: TxRw<'env> where Self: 'env; @@ -175,18 +174,16 @@ pub trait Env: Sized { } //---------------------------------------------------------------------------------------------------- DatabaseRo -/// Document errors when opening tables in [`EnvInner`]. -macro_rules! doc_table_error { +/// Document the INVARIANT that the `heed` backend +/// must use [`EnvInner::create_db`] when initially +/// opening/creating tables. +macro_rules! doc_heed_create_db_invariant { () => { - r"# Errors -This will only return [`RuntimeError::Io`] on normal errors. + r#"The first time you open/create tables, you _must_ use [`EnvInner::create_db`] +to set the proper flags / [`Key`](crate::Key) comparison for the `heed` backend. -If the specified table is not created upon before this function is called, -this will return an error. - -Implementation detail you should NOT rely on: -- This only panics on `heed` -- `redb` will create the table if it does not exist" +Subsequent table opens will follow the flags/ordering, but only if +[`EnvInner::create_db`] was the _first_ function to open/create it."# }; } @@ -204,24 +201,31 @@ Implementation detail you should NOT rely on: /// Note that when opening tables with [`EnvInner::open_db_ro`], /// they must be created first or else it will return error. /// -/// See [`EnvInner::open_db_rw`] and [`EnvInner::create_db`] for creating tables. -pub trait EnvInner<'env, Ro, Rw> -where - Self: 'env, - Ro: TxRo<'env>, - Rw: TxRw<'env>, -{ +/// See [`EnvInner::create_db`] for creating tables. +/// +/// # Invariant +#[doc = doc_heed_create_db_invariant!()] +pub trait EnvInner<'env> { + /// The read-only transaction type of the backend. + /// + /// `'tx` is the lifetime of the transaction itself. + type Ro<'tx>: TxRo<'tx>; + /// The read-write transaction type of the backend. + /// + /// `'tx` is the lifetime of the transaction itself. + type Rw<'tx>: TxRw<'tx>; + /// Create a read-only transaction. /// /// # Errors /// This will only return [`RuntimeError::Io`] if it errors. - fn tx_ro(&'env self) -> Result; + fn tx_ro(&self) -> Result, RuntimeError>; /// Create a read/write transaction. /// /// # Errors /// This will only return [`RuntimeError::Io`] if it errors. - fn tx_rw(&'env self) -> Result; + fn tx_rw(&self) -> Result, RuntimeError>; /// Open a database in read-only mode. /// @@ -231,11 +235,37 @@ where /// This will open the database [`Table`] /// passed as a generic to this function. /// - /// ```rust,ignore - /// let db = env.open_db_ro::(&tx_ro); - /// // ^ ^ - /// // database table table metadata - /// // (name, key/value type) + /// ```rust + /// # use cuprate_database::{ + /// # ConcreteEnv, + /// # config::ConfigBuilder, + /// # Env, EnvInner, + /// # DatabaseRo, DatabaseRw, TxRo, TxRw, + /// # }; + /// # fn main() -> Result<(), Box> { + /// # let tmp_dir = tempfile::tempdir()?; + /// # let db_dir = tmp_dir.path().to_owned(); + /// # let config = ConfigBuilder::new(db_dir.into()).build(); + /// # let env = ConcreteEnv::open(config)?; + /// # + /// # struct Table; + /// # impl cuprate_database::Table for Table { + /// # const NAME: &'static str = "table"; + /// # type Key = u8; + /// # type Value = u64; + /// # } + /// # + /// # let env_inner = env.env_inner(); + /// # let tx_rw = env_inner.tx_rw()?; + /// # env_inner.create_db::
(&tx_rw)?; + /// # TxRw::commit(tx_rw); + /// # + /// # let tx_ro = env_inner.tx_ro()?; + /// let db = env_inner.open_db_ro::
(&tx_ro); + /// // ^ ^ + /// // database table table metadata + /// // (name, key/value type) + /// # Ok(()) } /// ``` /// /// # Errors @@ -243,9 +273,12 @@ where /// /// If the specified table is not created upon before this function is called, /// this will return [`RuntimeError::TableNotFound`]. + /// + /// # Invariant + #[doc = doc_heed_create_db_invariant!()] fn open_db_ro( &self, - tx_ro: &Ro, + tx_ro: &Self::Ro<'_>, ) -> Result + DatabaseIter, RuntimeError>; /// Open a database in read/write mode. @@ -262,19 +295,23 @@ where /// # Errors /// This will only return [`RuntimeError::Io`] on errors. /// - /// Implementation details: Both `heed` & `redb` backends create - /// the table with this function if it does not already exist. For safety and - /// clear intent, you should still consider using [`EnvInner::create_db`] instead. - fn open_db_rw(&self, tx_rw: &Rw) -> Result, RuntimeError>; + /// # Invariant + #[doc = doc_heed_create_db_invariant!()] + fn open_db_rw( + &self, + tx_rw: &Self::Rw<'_>, + ) -> Result, RuntimeError>; /// Create a database table. /// - /// This will create the database [`Table`] - /// passed as a generic to this function. + /// This will create the database [`Table`] passed as a generic to this function. /// /// # Errors /// This will only return [`RuntimeError::Io`] on errors. - fn create_db(&self, tx_rw: &Rw) -> Result<(), RuntimeError>; + /// + /// # Invariant + #[doc = doc_heed_create_db_invariant!()] + fn create_db(&self, tx_rw: &Self::Rw<'_>) -> Result<(), RuntimeError>; /// Clear all `(key, value)`'s from a database table. /// @@ -284,6 +321,10 @@ where /// Note that this operation is tied to `tx_rw`, as such this /// function's effects can be aborted using [`TxRw::abort`]. /// - #[doc = doc_table_error!()] - fn clear_db(&self, tx_rw: &mut Rw) -> Result<(), RuntimeError>; + /// # Errors + /// This will return [`RuntimeError::Io`] on normal errors. + /// + /// If the specified table is not created upon before this function is called, + /// this will return [`RuntimeError::TableNotFound`]. + fn clear_db(&self, tx_rw: &mut Self::Rw<'_>) -> Result<(), RuntimeError>; } diff --git a/storage/database/src/error.rs b/storage/database/src/error.rs index 386091d9..3471ac74 100644 --- a/storage/database/src/error.rs +++ b/storage/database/src/error.rs @@ -59,18 +59,16 @@ pub enum InitError { } //---------------------------------------------------------------------------------------------------- RuntimeError -/// Errors that occur _after_ successful ([`Env::open`](crate::env::Env::open)). +/// Errors that occur _after_ successful [`Env::open`](crate::env::Env::open). /// /// There are no errors for: /// 1. Missing tables /// 2. (De)serialization -/// 3. Shutdown errors /// /// as `cuprate_database` upholds the invariant that: /// /// 1. All tables exist /// 2. (De)serialization never fails -/// 3. The database (thread-pool) only shuts down when all channels are dropped #[derive(thiserror::Error, Debug)] pub enum RuntimeError { /// The given key already existed in the database. diff --git a/storage/database/src/key.rs b/storage/database/src/key.rs index 13f7cede..3273d4ed 100644 --- a/storage/database/src/key.rs +++ b/storage/database/src/key.rs @@ -1,54 +1,177 @@ //! Database key abstraction; `trait Key`. //---------------------------------------------------------------------------------------------------- Import -use std::cmp::Ordering; +use std::{cmp::Ordering, fmt::Debug}; -use crate::storable::Storable; +use crate::{storable::Storable, StorableBytes, StorableStr, StorableVec}; //---------------------------------------------------------------------------------------------------- Table /// Database [`Table`](crate::table::Table) key metadata. /// /// Purely compile time information for database table keys. -// -// FIXME: this doesn't need to exist right now but -// may be used if we implement getting values using ranges. -// -pub trait Key: Storable + Sized { - /// The primary key type. - type Primary: Storable; - +/// +/// ## Comparison +/// There are 2 differences between [`Key`] and [`Storable`]: +/// 1. [`Key`] must be [`Sized`] +/// 2. [`Key`] represents a [`Storable`] type that defines a comparison function +/// +/// The database backends will use [`Key::KEY_COMPARE`] +/// to sort the keys within database tables. +/// +/// [`Key::KEY_COMPARE`] is pre-implemented as a straight byte comparison. +/// +/// This default is overridden for numbers, which use a number comparison. +/// For example, [`u64`] keys are sorted as `{0, 1, 2 ... 999_998, 999_999, 1_000_000}`. +/// +/// If you would like to re-define this for number types, consider; +/// 1. Creating a wrapper type around primitives like a `struct SortU8(pub u8)` +/// 2. Implement [`Key`] on that wrapper +/// 3. Define a custom [`Key::KEY_COMPARE`] +pub trait Key: Storable + Sized + Ord { /// Compare 2 [`Key`]'s against each other. /// - /// By default, this does a straight _byte_ comparison, - /// not a comparison of the key's value. + /// # Defaults for types + /// For arrays and vectors that contain a `T: Storable`, + /// this does a straight _byte_ comparison, not a comparison of the key's value. /// + /// For [`StorableStr`], this will use [`str::cmp`], i.e. it is the same as the default behavior; it is a + /// [lexicographical comparison](https://doc.rust-lang.org/std/cmp/trait.Ord.html#lexicographical-comparison) + /// + /// For all primitive number types ([`u8`], [`i128`], etc), this will + /// convert the bytes to the number using [`Storable::from_bytes`], + /// then do a number comparison. + /// + /// # Example /// ```rust /// # use cuprate_database::*; + /// // Normal byte comparison. + /// let vec1 = StorableVec(vec![0, 1]); + /// let vec2 = StorableVec(vec![255, 0]); /// assert_eq!( - /// ::compare([0].as_slice(), [1].as_slice()), + /// as Key>::KEY_COMPARE + /// .as_compare_fn::>()(&vec1, &vec2), /// std::cmp::Ordering::Less, /// ); + /// + /// // Integer comparison. + /// let byte1 = [0, 1]; // 256 + /// let byte2 = [255, 0]; // 255 + /// let num1 = u16::from_le_bytes(byte1); + /// let num2 = u16::from_le_bytes(byte2); + /// assert_eq!(num1, 256); + /// assert_eq!(num2, 255); /// assert_eq!( - /// ::compare([1].as_slice(), [1].as_slice()), - /// std::cmp::Ordering::Equal, - /// ); - /// assert_eq!( - /// ::compare([2].as_slice(), [1].as_slice()), + /// // 256 > 255 + /// ::KEY_COMPARE.as_compare_fn::()(&byte1, &byte2), /// std::cmp::Ordering::Greater, /// ); /// ``` - #[inline] - fn compare(left: &[u8], right: &[u8]) -> Ordering { - left.cmp(right) - } + const KEY_COMPARE: KeyCompare = KeyCompare::Default; } //---------------------------------------------------------------------------------------------------- Impl -impl Key for T -where - T: Storable + Sized, -{ - type Primary = Self; +/// [`Ord`] comparison for arrays/vectors. +impl Key for [T; N] where T: Key + Storable + Sized + bytemuck::Pod {} +impl Key for StorableVec {} + +/// [`Ord`] comparison for misc types. +/// +/// This is not a blanket implementation because +/// it allows outer crates to define their own +/// comparison functions for their `T: Storable` types. +impl Key for () {} +impl Key for StorableBytes {} +impl Key for StorableStr {} + +/// Number comparison. +/// +/// # Invariant +/// This must _only_ be implemented for [`u32`], [`u64`] (and maybe [`usize`]). +/// +/// This is because: +/// 1. We use LMDB's `INTEGER_KEY` flag when this enum variant is used +/// 2. LMDB only supports these types when using that flag +/// +/// See: +/// +/// Other numbers will still have the same behavior, but they use +/// [`impl_custom_numbers_key`] and essentially pass LMDB a "custom" +/// number compare function. +macro_rules! impl_number_key { + ($($t:ident),* $(,)?) => { + $( + impl Key for $t { + const KEY_COMPARE: KeyCompare = KeyCompare::Number; + } + )* + }; +} + +impl_number_key!(u32, u64, usize); +#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))] +compile_error!("`cuprate_database`: `usize` must be equal to `u32` or `u64` for LMDB's `usize` key sorting to function correctly"); + +/// Custom number comparison for other numbers. +macro_rules! impl_custom_numbers_key { + ($($t:ident),* $(,)?) => { + $( + impl Key for $t { + // Just forward the the number comparison function. + const KEY_COMPARE: KeyCompare = KeyCompare::Custom(|left, right| { + KeyCompare::Number.as_compare_fn::<$t>()(left, right) + }); + } + )* + }; +} + +impl_custom_numbers_key!(u8, u16, u128, i8, i16, i32, i64, i128, isize); + +//---------------------------------------------------------------------------------------------------- KeyCompare +/// Comparison behavior for [`Key`]s. +/// +/// This determines how the database sorts [`Key`]s inside a database [`Table`](crate::Table). +/// +/// See [`Key`] for more info. +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum KeyCompare { + /// Use the default comparison behavior of the backend. + /// + /// Currently, both `heed` and `redb` use + /// [lexicographical comparison](https://doc.rust-lang.org/1.79.0/std/cmp/trait.Ord.html#lexicographical-comparison) + /// by default, i.e. a straight byte comparison. + #[default] + Default, + + /// A by-value number comparison, i.e. `255 < 256`. + /// + /// This _behavior_ is implemented as the default for all number primitives, + /// although some implementations on numbers use [`KeyCompare::Custom`] due + /// to internal implementation details of LMDB. + Number, + + /// A custom sorting function. + /// + /// The input of the function is 2 [`Key`]s in byte form. + Custom(fn(&[u8], &[u8]) -> Ordering), +} + +impl KeyCompare { + /// Return [`Self`] as a pure comparison function. + /// + /// The returned function expects 2 [`Key`]s in byte form as input. + #[inline] + pub const fn as_compare_fn(self) -> fn(&[u8], &[u8]) -> Ordering { + match self { + Self::Default => std::cmp::Ord::cmp, + Self::Number => |left, right| { + let left = ::from_bytes(left); + let right = ::from_bytes(right); + std::cmp::Ord::cmp(&left, &right) + }, + Self::Custom(f) => f, + } + } } //---------------------------------------------------------------------------------------------------- Tests diff --git a/storage/database/src/lib.rs b/storage/database/src/lib.rs index 1e15b584..da36b0d5 100644 --- a/storage/database/src/lib.rs +++ b/storage/database/src/lib.rs @@ -78,6 +78,8 @@ clippy::module_inception, clippy::redundant_pub_crate, clippy::option_if_let_else, + + // unused_crate_dependencies, // false-positive with `paste` )] // Allow some lints when running in debug mode. #![cfg_attr( @@ -105,42 +107,39 @@ // Documentation for each module is located in the respective file. mod backend; -pub use backend::ConcreteEnv; +mod constants; +mod database; +mod env; +mod error; +mod key; +mod storable; +mod table; +mod tables; +mod transaction; pub mod config; +pub mod resize; -mod constants; +pub use backend::ConcreteEnv; pub use constants::{ DATABASE_BACKEND, DATABASE_CORRUPT_MSG, DATABASE_DATA_FILENAME, DATABASE_LOCK_FILENAME, }; - -mod database; pub use database::{DatabaseIter, DatabaseRo, DatabaseRw}; - -mod env; pub use env::{Env, EnvInner}; - -mod error; pub use error::{InitError, RuntimeError}; - -pub mod resize; - -mod key; -pub use key::Key; - -mod storable; -pub use storable::{Storable, StorableBytes, StorableVec}; - -mod table; +pub use key::{Key, KeyCompare}; +pub use storable::{Storable, StorableBytes, StorableStr, StorableVec}; pub use table::Table; - -mod transaction; pub use transaction::{TxRo, TxRw}; //---------------------------------------------------------------------------------------------------- Private #[cfg(test)] pub(crate) mod tests; +// Used inside public facing macros. +#[doc(hidden)] +pub use paste; + //---------------------------------------------------------------------------------------------------- // HACK: needed to satisfy the `unused_crate_dependencies` lint. cfg_if::cfg_if! { diff --git a/storage/database/src/storable.rs b/storage/database/src/storable.rs index b5fa2f8a..100ed44f 100644 --- a/storage/database/src/storable.rs +++ b/storage/database/src/storable.rs @@ -1,7 +1,10 @@ //! (De)serialization for table keys & values. //---------------------------------------------------------------------------------------------------- Import -use std::{borrow::Borrow, fmt::Debug}; +use std::{ + borrow::{Borrow, Cow}, + fmt::Debug, +}; use bytemuck::Pod; use bytes::Bytes; @@ -126,7 +129,7 @@ where /// /// Slice types are owned both: /// - when returned from the database -/// - in `put()` +/// - in [`crate::DatabaseRw::put()`] /// /// This is needed as `impl Storable for Vec` runs into impl conflicts. /// @@ -194,6 +197,66 @@ impl Borrow<[T]> for StorableVec { } } +//---------------------------------------------------------------------------------------------------- StorableVec +/// A [`Storable`] string. +/// +/// This is a wrapper around a `Cow<'static, str>` +/// that can be stored in the database. +/// +/// # Invariant +/// [`StorableStr::from_bytes`] will panic +/// if the bytes are not UTF-8. This should normally +/// not be possible in database operations, although technically +/// you can call this function yourself and input bad data. +/// +/// # Example +/// ```rust +/// # use cuprate_database::*; +/// # use std::borrow::Cow; +/// let string: StorableStr = StorableStr(Cow::Borrowed("a")); +/// +/// // Into bytes. +/// let into = Storable::as_bytes(&string); +/// assert_eq!(into, &[97]); +/// +/// // From bytes. +/// let from: StorableStr = Storable::from_bytes(&into); +/// assert_eq!(from, string); +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, bytemuck::TransparentWrapper)] +#[repr(transparent)] +pub struct StorableStr(pub Cow<'static, str>); + +impl Storable for StorableStr { + const BYTE_LENGTH: Option = None; + + /// [`String::as_bytes`]. + #[inline] + fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + #[inline] + fn from_bytes(bytes: &[u8]) -> Self { + Self(Cow::Owned(std::str::from_utf8(bytes).unwrap().to_string())) + } +} + +impl std::ops::Deref for StorableStr { + type Target = Cow<'static, str>; + #[inline] + fn deref(&self) -> &Cow<'static, str> { + &self.0 + } +} + +impl Borrow> for StorableStr { + #[inline] + fn borrow(&self) -> &Cow<'static, str> { + &self.0 + } +} + //---------------------------------------------------------------------------------------------------- StorableBytes /// A [`Storable`] version of [`Bytes`]. /// diff --git a/storage/database/src/table.rs b/storage/database/src/table.rs index 56e84ddd..3ad0e793 100644 --- a/storage/database/src/table.rs +++ b/storage/database/src/table.rs @@ -8,6 +8,8 @@ use crate::{key::Key, storable::Storable}; /// Database table metadata. /// /// Purely compile time information for database tables. +/// +/// See [`crate::define_tables`] for bulk table generation. pub trait Table: 'static { /// Name of the database table. const NAME: &'static str; diff --git a/storage/database/src/tables.rs b/storage/database/src/tables.rs new file mode 100644 index 00000000..83a00e16 --- /dev/null +++ b/storage/database/src/tables.rs @@ -0,0 +1,429 @@ +//! Database table definition macro. + +//---------------------------------------------------------------------------------------------------- Import + +//---------------------------------------------------------------------------------------------------- Table macro +/// Define all table types. +/// +/// # Purpose +/// This macro allows you to define all database tables in one place. +/// +/// A by-product of this macro is that it defines some +/// convenient traits specific to _your_ tables +/// (see [Output](#output)). +/// +/// # Inputs +/// This macro expects a list of tables, and their key/value types. +/// +/// This syntax is as follows: +/// +/// ```rust +/// cuprate_database::define_tables! { +/// /// Any extra attributes you'd like to add to +/// /// this table type, e.g. docs or derives. +/// +/// 0 => TableName, +/// // ▲ ▲ +/// // │ └─ Table struct name. The macro generates this for you. +/// // │ +/// // Incrementing index. This must start at 0 +/// // and increment by 1 per table added. +/// +/// u8 => u64, +/// // ▲ ▲ +/// // │ └─ Table value type. +/// // │ +/// // Table key type. +/// +/// // Another table. +/// 1 => TableName2, +/// i8 => i64, +/// } +/// ``` +/// +/// An example: +/// ```rust +/// use cuprate_database::{ +/// ConcreteEnv, Table, +/// config::ConfigBuilder, +/// Env, EnvInner, +/// DatabaseRo, DatabaseRw, TxRo, TxRw, +/// }; +/// +/// // This generates `pub struct Table{1,2,3}` +/// // where all those implement `Table` with +/// // the defined name and key/value types. +/// // +/// // It also generate traits specific to our tables. +/// cuprate_database::define_tables! { +/// 0 => Table1, +/// u32 => i32, +/// +/// /// This one has extra docs. +/// 1 => Table2, +/// u64 => (), +/// +/// 2 => Table3, +/// i32 => i32, +/// } +/// +/// # fn main() -> Result<(), Box> { +/// # let tmp_dir = tempfile::tempdir()?; +/// # let db_dir = tmp_dir.path().to_owned(); +/// # let config = ConfigBuilder::new(db_dir.into()).build(); +/// // Open the database. +/// let env = ConcreteEnv::open(config)?; +/// let env_inner = env.env_inner(); +/// +/// // Open the table we just defined. +/// { +/// let tx_rw = env_inner.tx_rw()?; +/// env_inner.create_db::(&tx_rw)?; +/// let mut table = env_inner.open_db_rw::(&tx_rw)?; +/// +/// // Write data to the table. +/// table.put(&0, &1)?; +/// +/// drop(table); +/// TxRw::commit(tx_rw)?; +/// } +/// +/// // Read the data, assert it is correct. +/// { +/// let tx_ro = env_inner.tx_ro()?; +/// let table = env_inner.open_db_ro::(&tx_ro)?; +/// assert_eq!(table.first()?, (0, 1)); +/// } +/// +/// // Create all tables at once using the +/// // `OpenTables` trait generated with the +/// // macro above. +/// { +/// let tx_rw = env_inner.tx_rw()?; +/// env_inner.create_tables(&tx_rw)?; +/// TxRw::commit(tx_rw)?; +/// } +/// +/// // Open all tables at once. +/// { +/// let tx_ro = env_inner.tx_ro()?; +/// let all_tables = env_inner.open_tables(&tx_ro)?; +/// } +/// # Ok(()) } +/// ``` +/// +/// # Output +/// This macro: +/// 1. Implements [`Table`](crate::Table) on all your table types +/// 1. Creates a `pub trait Tables` trait (in scope) +/// 1. Creates a `pub trait TablesIter` trait (in scope) +/// 1. Creates a `pub trait TablesMut` trait (in scope) +/// 1. Blanket implements a `(tuples, containing, all, open, database, tables, ...)` for the above traits +/// 1. Creates a `pub trait OpenTables` trait (in scope) +/// +/// All table types are zero-sized structs that implement the `Table` trait. +/// +/// Table structs are automatically `CamelCase`, and their +/// static string names are automatically `snake_case`. +/// +/// For why the table traits + blanket implementation on the tuple exists, see: +/// . +/// +/// The `OpenTables` trait lets you open all tables you've defined, at once. +/// +/// # Example +/// For examples of usage & output, see +/// [`cuprate_blockchain::tables`](https://github.com/Cuprate/cuprate/blob/main/storage/blockchain/src/tables.rs). +#[macro_export] +macro_rules! define_tables { + ( + $( + // Documentation and any `derive`'s. + $(#[$attr:meta])* + + // The table name + doubles as the table struct name. + $index:literal => $table:ident, + + // Key type => Value type. + $key:ty => $value:ty + ),* $(,)? + ) => { $crate::paste::paste! { + $( + // Table struct. + $(#[$attr])* + #[doc = concat!("- Key: [`", stringify!($key), "`]")] + #[doc = concat!("- Value: [`", stringify!($value), "`]")] + #[doc = concat!("- Name: `", stringify!([<$table:snake>]), "`")] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] + pub struct [<$table:camel>]; + + // Table trait impl. + impl $crate::Table for [<$table:camel>] { + const NAME: &'static str = stringify!([<$table:snake>]); + type Key = $key; + type Value = $value; + } + )* + + /// Object containing all opened [`Table`](cuprate_database::Table)s in read-only mode. + /// + /// This is an encapsulated object that contains all + /// available `Table`'s in read-only mode. + /// + /// It is a `Sealed` trait and is only implemented on a + /// `(tuple, containing, all, table, types, ...)`. + /// + /// This is used to return a _single_ object from functions like + /// [`OpenTables::open_tables`] rather than the tuple containing the tables itself. + /// + /// To replace `tuple.0` style indexing, `field_accessor_functions()` + /// are provided on this trait, which essentially map the object to + /// fields containing the particular database table, for example: + /// ```rust,ignore + /// let tables = open_tables(); + /// + /// // The accessor function `block_infos()` returns the field + /// // containing an open database table for `BlockInfos`. + /// let _ = tables.block_infos(); + /// ``` + /// + /// See also: + /// - [`TablesMut`] + /// - [`TablesIter`] + pub trait Tables { + // This expands to creating `fn field_accessor_functions()` + // for each passed `$table` type. + // + // It is essentially a mapping to the field + // containing the proper opened database table. + // + // The function name of the function is + // the table type in `snake_case`, e.g., `block_info_v1s()`. + $( + /// Access an opened + #[doc = concat!("[`", stringify!($table), "`]")] + /// database. + fn [<$table:snake>](&self) -> &impl $crate::DatabaseRo<$table>; + )* + + /// This returns `true` if all tables are empty. + /// + /// # Errors + /// This returns errors on regular database errors. + fn all_tables_empty(&self) -> Result; + } + + /// Object containing all opened [`Table`](cuprate_database::Table)s in read + iter mode. + /// + /// This is the same as [`Tables`] but includes `_iter()` variants. + /// + /// Note that this trait is a supertrait of `Tables`, + /// as in it can use all of its functions as well. + /// + /// See [`Tables`] for documentation - this trait exists for the same reasons. + pub trait TablesIter: Tables { + $( + /// Access an opened read-only + iterable + #[doc = concat!("[`", stringify!($table), "`]")] + /// database. + fn [<$table:snake _iter>](&self) -> &(impl $crate::DatabaseRo<$table> + $crate::DatabaseIter<$table>); + )* + } + + /// Object containing all opened [`Table`](cuprate_database::Table)s in write mode. + /// + /// This is the same as [`Tables`] but for mutable accesses. + /// + /// Note that this trait is a supertrait of `Tables`, + /// as in it can use all of its functions as well. + /// + /// See [`Tables`] for documentation - this trait exists for the same reasons. + pub trait TablesMut: Tables { + $( + /// Access an opened + #[doc = concat!("[`", stringify!($table), "`]")] + /// database. + fn [<$table:snake _mut>](&mut self) -> &mut impl $crate::DatabaseRw<$table>; + )* + } + + // This creates a blanket-implementation for + // `(tuple, containing, all, table, types)`. + // + // There is a generic defined here _for each_ `$table` input. + // Specifically, the generic letters are just the table types in UPPERCASE. + // Concretely, this expands to something like: + // ```rust + // impl + // ``` + impl<$([<$table:upper>]),*> Tables + // We are implementing `Tables` on a tuple that + // contains all those generics specified, i.e., + // a tuple containing all open table types. + // + // Concretely, this expands to something like: + // ```rust + // (BLOCKINFOSV1S, BLOCKINFOSV2S, BLOCKINFOSV3S, [...]) + // ``` + // which is just a tuple of the generics defined above. + for ($([<$table:upper>]),*) + where + // This expands to a where bound that asserts each element + // in the tuple implements some database table type. + // + // Concretely, this expands to something like: + // ```rust + // BLOCKINFOSV1S: DatabaseRo + DatabaseIter, + // BLOCKINFOSV2S: DatabaseRo + DatabaseIter, + // [...] + // ``` + $( + [<$table:upper>]: $crate::DatabaseRo<$table>, + )* + { + $( + // The function name of the accessor function is + // the table type in `snake_case`, e.g., `block_info_v1s()`. + #[inline] + fn [<$table:snake>](&self) -> &impl $crate::DatabaseRo<$table> { + // The index of the database table in + // the tuple implements the table trait. + &self.$index + } + )* + + fn all_tables_empty(&self) -> Result { + $( + if !$crate::DatabaseRo::is_empty(&self.$index)? { + return Ok(false); + } + )* + Ok(true) + } + } + + // This is the same as the above + // `Tables`, but for `TablesIter`. + impl<$([<$table:upper>]),*> TablesIter + for ($([<$table:upper>]),*) + where + $( + [<$table:upper>]: $crate::DatabaseRo<$table> + $crate::DatabaseIter<$table>, + )* + { + $( + // The function name of the accessor function is + // the table type in `snake_case` + `_iter`, e.g., `block_info_v1s_iter()`. + #[inline] + fn [<$table:snake _iter>](&self) -> &(impl $crate::DatabaseRo<$table> + $crate::DatabaseIter<$table>) { + &self.$index + } + )* + } + + // This is the same as the above + // `Tables`, but for `TablesMut`. + impl<$([<$table:upper>]),*> TablesMut + for ($([<$table:upper>]),*) + where + $( + [<$table:upper>]: $crate::DatabaseRw<$table>, + )* + { + $( + // The function name of the mutable accessor function is + // the table type in `snake_case` + `_mut`, e.g., `block_info_v1s_mut()`. + #[inline] + fn [<$table:snake _mut>](&mut self) -> &mut impl $crate::DatabaseRw<$table> { + &mut self.$index + } + )* + } + + /// Open all tables at once. + /// + /// This trait encapsulates the functionality of opening all tables at once. + /// It can be seen as the "constructor" for the [`Tables`] object. + /// + /// Note that this is already implemented on [`cuprate_database::EnvInner`], thus: + /// - You don't need to implement this + /// - It can be called using `env_inner.open_tables()` notation + /// + /// # Creation before opening + /// As [`cuprate_database::EnvInner`] documentation states, + /// tables must be created before they are opened. + /// + /// I.e. [`OpenTables::create_tables`] must be called before + /// [`OpenTables::open_tables`] or else panics may occur. + pub trait OpenTables<'env> { + /// The read-only transaction type of the backend. + type Ro<'tx>; + /// The read-write transaction type of the backend. + type Rw<'tx>; + + /// Open all tables in read/iter mode. + /// + /// This calls [`cuprate_database::EnvInner::open_db_ro`] on all database tables + /// and returns a structure that allows access to all tables. + /// + /// # Errors + /// This will only return [`cuprate_database::RuntimeError::Io`] if it errors. + fn open_tables(&self, tx_ro: &Self::Ro<'_>) -> Result; + + /// Open all tables in read-write mode. + /// + /// This calls [`cuprate_database::EnvInner::open_db_rw`] on all database tables + /// and returns a structure that allows access to all tables. + /// + /// # Errors + /// This will only return [`cuprate_database::RuntimeError::Io`] on errors. + fn open_tables_mut(&self, tx_rw: &Self::Rw<'_>) -> Result; + + /// Create all database tables. + /// + /// This will create all the defined [`Table`](cuprate_database::Table)s. + /// + /// # Errors + /// This will only return [`cuprate_database::RuntimeError::Io`] on errors. + fn create_tables(&self, tx_rw: &Self::Rw<'_>) -> Result<(), $crate::RuntimeError>; + } + + impl<'env, Ei> OpenTables<'env> for Ei + where + Ei: $crate::EnvInner<'env>, + { + type Ro<'tx> = >::Ro<'tx>; + type Rw<'tx> = >::Rw<'tx>; + + fn open_tables(&self, tx_ro: &Self::Ro<'_>) -> Result { + Ok(($( + Self::open_db_ro::<[<$table:camel>]>(self, tx_ro)?, + )*)) + } + + fn open_tables_mut(&self, tx_rw: &Self::Rw<'_>) -> Result { + Ok(($( + Self::open_db_rw::<[<$table:camel>]>(self, tx_rw)?, + )*)) + } + + fn create_tables(&self, tx_rw: &Self::Rw<'_>) -> Result<(), $crate::RuntimeError> { + let result = Ok(($( + Self::create_db::<[<$table:camel>]>(self, tx_rw), + )*)); + + match result { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + } + }}; +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/tests.rs b/storage/database/src/tests.rs index 81561073..9c9317d2 100644 --- a/storage/database/src/tests.rs +++ b/storage/database/src/tests.rs @@ -15,7 +15,7 @@ pub(crate) struct TestTable; impl Table for TestTable { const NAME: &'static str = "test_table"; - type Key = u8; + type Key = u32; type Value = u64; } diff --git a/storage/database/src/transaction.rs b/storage/database/src/transaction.rs index e4c310a0..8f33983d 100644 --- a/storage/database/src/transaction.rs +++ b/storage/database/src/transaction.rs @@ -11,7 +11,7 @@ use crate::error::RuntimeError; /// # Commit /// It's recommended but may not be necessary to call [`TxRo::commit`] in certain cases: /// - -pub trait TxRo<'env> { +pub trait TxRo<'tx> { /// Commit the read-only transaction. /// /// # Errors @@ -23,7 +23,7 @@ pub trait TxRo<'env> { /// Read/write database transaction. /// /// Returned from [`EnvInner::tx_rw`](crate::EnvInner::tx_rw). -pub trait TxRw<'env> { +pub trait TxRw<'tx> { /// Commit the read/write transaction. /// /// Note that this doesn't necessarily sync the database caches to disk. diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml index 76b4cd97..dd24fd59 100644 --- a/test-utils/Cargo.toml +++ b/test-utils/Cargo.toml @@ -1,32 +1,30 @@ [package] -name = "cuprate-test-utils" +name = "cuprate-test-utils" version = "0.1.0" edition = "2021" license = "MIT" authors = ["Boog900", "hinto-janai"] [dependencies] -cuprate-types = { path = "../types" } -cuprate-helper = { path = "../helper", features = ["map"] } -cuprate-wire = { path = "../net/wire" } +cuprate-types = { path = "../types" } +cuprate-helper = { path = "../helper", features = ["map"] } +cuprate-wire = { path = "../net/wire" } cuprate-p2p-core = { path = "../p2p/p2p-core", features = ["borsh"] } -hex = { workspace = true } -hex-literal = { workspace = true } -monero-serai = { workspace = true, features = ["std"] } -monero-simple-request-rpc = { workspace = true } -monero-rpc = { workspace = true } -futures = { workspace = true, features = ["std"] } -async-trait = { workspace = true } -tokio = { workspace = true, features = ["full"] } -tokio-util = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -bytes = { workspace = true, features = ["std"] } -tempfile = { workspace = true } - -borsh = { workspace = true, features = ["derive"]} +hex = { workspace = true } +hex-literal = { workspace = true } +monero-serai = { workspace = true, features = ["std", "http-rpc"] } +futures = { workspace = true, features = ["std"] } +async-trait = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tokio-util = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +bytes = { workspace = true, features = ["std"] } +tempfile = { workspace = true } +paste = { workspace = true } +borsh = { workspace = true, features = ["derive"]} [dev-dependencies] -hex = { workspace = true } +hex = { workspace = true } pretty_assertions = { workspace = true } \ No newline at end of file diff --git a/test-utils/README.md b/test-utils/README.md index c210686a..3c71c0a3 100644 --- a/test-utils/README.md +++ b/test-utils/README.md @@ -7,3 +7,4 @@ It currently contains: - Code to spawn monerod instances and a testing network zone - Real raw and typed Monero data, e.g. `Block, Transaction` - An RPC client to generate types from `cuprate_types` +- Raw RPC request/response strings and binary data \ No newline at end of file diff --git a/test-utils/src/data/free.rs b/test-utils/src/data/free.rs index df66dd11..71a4dc51 100644 --- a/test-utils/src/data/free.rs +++ b/test-utils/src/data/free.rs @@ -293,7 +293,7 @@ mod tests { use pretty_assertions::assert_eq; - use crate::rpc::HttpRpcClient; + use crate::rpc::client::HttpRpcClient; /// Assert the defined blocks are the same compared to ones received from a local RPC call. #[ignore] // FIXME: doesn't work in CI, we need a real unrestricted node diff --git a/test-utils/src/rpc/client.rs b/test-utils/src/rpc/client.rs index ab4481a2..09aa3738 100644 --- a/test-utils/src/rpc/client.rs +++ b/test-utils/src/rpc/client.rs @@ -11,7 +11,9 @@ use monero_simple_request_rpc::SimpleRequestRpc; use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; -use crate::rpc::constants::LOCALHOST_RPC_URL; +//---------------------------------------------------------------------------------------------------- Constants +/// The default URL used for Monero RPC connections. +pub const LOCALHOST_RPC_URL: &str = "http://127.0.0.1:18081"; //---------------------------------------------------------------------------------------------------- HttpRpcClient /// An HTTP RPC client for Monero. diff --git a/test-utils/src/rpc/constants.rs b/test-utils/src/rpc/constants.rs deleted file mode 100644 index ce44a88b..00000000 --- a/test-utils/src/rpc/constants.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! RPC-related Constants. - -//---------------------------------------------------------------------------------------------------- Use - -//---------------------------------------------------------------------------------------------------- Constants -/// The default URL used for Monero RPC connections. -pub const LOCALHOST_RPC_URL: &str = "http://127.0.0.1:18081"; diff --git a/test-utils/src/rpc/data/bin.rs b/test-utils/src/rpc/data/bin.rs new file mode 100644 index 00000000..cf98a4aa --- /dev/null +++ b/test-utils/src/rpc/data/bin.rs @@ -0,0 +1,55 @@ +//! Binary data from [`.bin` endpoints](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_blocksbin). +//! +//! TODO: Not implemented yet. + +//---------------------------------------------------------------------------------------------------- Import +use crate::rpc::data::macros::define_request_and_response; + +//---------------------------------------------------------------------------------------------------- TODO +define_request_and_response! { + get_blocksbin, + GET_BLOCKS: &[u8], + Request = &[]; + Response = &[]; +} + +define_request_and_response! { + get_blocks_by_heightbin, + GET_BLOCKS_BY_HEIGHT: &[u8], + Request = &[]; + Response = &[]; +} + +define_request_and_response! { + get_hashesbin, + GET_HASHES: &[u8], + Request = &[]; + Response = &[]; +} + +define_request_and_response! { + get_o_indexesbin, + GET_O_INDEXES: &[u8], + Request = &[]; + Response = &[]; +} + +define_request_and_response! { + get_outsbin, + GET_OUTS: &[u8], + Request = &[]; + Response = &[]; +} + +define_request_and_response! { + get_transaction_pool_hashesbin, + GET_TRANSACTION_POOL_HASHES: &[u8], + Request = &[]; + Response = &[]; +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/test-utils/src/rpc/data/json.rs b/test-utils/src/rpc/data/json.rs new file mode 100644 index 00000000..a05af670 --- /dev/null +++ b/test-utils/src/rpc/data/json.rs @@ -0,0 +1,1296 @@ +//! JSON data from the [`/json_rpc`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#json-rpc-methods) endpoint. + +//---------------------------------------------------------------------------------------------------- Import +use crate::rpc::data::macros::define_request_and_response; + +//---------------------------------------------------------------------------------------------------- Struct definitions +// This generates 2 const strings: +// +// - `const GET_BLOCK_TEMPLATE_REQUEST: &str = "..."` +// - `const GET_BLOCK_TEMPLATE_RESPONSE: &str = "..."` +// +// with some interconnected documentation. +define_request_and_response! { + // The markdown tag for Monero RPC documentation. Not necessarily the endpoint (json). + // + // Adding `(json_rpc)` after this will trigger the macro to automatically + // add a `serde_json` test for the request/response data. + get_block_template (json_rpc), + + // The base const name: the type of the request/response. + GET_BLOCK_TEMPLATE: &str, + + // The request data. + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_block_template", + "params": { + "wallet_address": "44GBHzv6ZyQdJkjqZje6KLZ3xSyN1hBSFAnLP6EAqJtCRVzMzZmeXTC2AHKDS9aEDTRKmo6a6o9r9j86pYfhCWDkKjbtcns", + "reserve_size": 60 + } +}"#; + + // The response data. + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "blockhashing_blob": "1010f4bae0b4069d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a00000000e0c20372be23d356347091025c5b5e8f2abf83ab618378565cce2b703491523401", + "blocktemplate_blob": "1010f4bae0b4069d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a0000000002c681c30101ff8a81c3010180e0a596bb11033b7eedf47baf878f3490cb20b696079c34bd017fe59b0d070e74d73ffabc4bb0e05f011decb630f3148d0163b3bd39690dde4078e4cfb69fecf020d6278a27bad10c58023c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "difficulty": 283305047039, + "difficulty_top64": 0, + "expected_reward": 600000000000, + "height": 3195018, + "next_seed_hash": "", + "prev_hash": "9d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a", + "reserved_offset": 131, + "seed_hash": "e2aa0b7b55042cd48b02e395d78fa66a29815ccc1584e38db2d1f0e8485cd44f", + "seed_height": 3194880, + "status": "OK", + "untrusted": false, + "wide_difficulty": "0x41f64bf3ff" + } +}"#; +} + +define_request_and_response! { + get_block_count (json_rpc), + GET_BLOCK_COUNT: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_block_count" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "count": 3195019, + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + on_get_block_hash (json_rpc), + ON_GET_BLOCK_HASH: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "on_get_block_hash", + "params": [912345] +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": "e22cf75f39ae720e8b71b3d120a5ac03f0db50bba6379e2850975b4859190bc6" +}"#; +} + +define_request_and_response! { + submit_block (json_rpc), + SUBMIT_BLOCK: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "submit_block", + "params": ["0707e6bdfedc053771512f1bc27c62731ae9e8f2443db64ce742f4e57f5cf8d393de28551e441a0000000002fb830a01ffbf830a018cfe88bee283060274c0aae2ef5730e680308d9c00b6da59187ad0352efe3c71d36eeeb28782f29f2501bd56b952c3ddc3e350c2631d3a5086cac172c56893831228b17de296ff4669de020200000000"] +}"#; + Response = +r#"{ + "error": { + "code": -7, + "message": "Block not accepted" + }, + "id": "0", + "jsonrpc": "2.0" +}"#; +} + +define_request_and_response! { + generateblocks (json_rpc), + GENERATE_BLOCKS: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "generateblocks", + "params": { + "amount_of_blocks": 1, + "wallet_address": "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A", + "starting_nonce": 0 + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "blocks": ["49b712db7760e3728586f8434ee8bc8d7b3d410dac6bb6e98bf5845c83b917e4"], + "height": 9783, + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_last_block_header (json_rpc), + GET_LAST_BLOCK_HEADER: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_last_block_header" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "block_header": { + "block_size": 200419, + "block_weight": 200419, + "cumulative_difficulty": 366125734645190820, + "cumulative_difficulty_top64": 0, + "depth": 0, + "difficulty": 282052561854, + "difficulty_top64": 0, + "hash": "57238217820195ac4c08637a144a885491da167899cf1d20e8e7ce0ae0a3434e", + "height": 3195020, + "long_term_weight": 200419, + "major_version": 16, + "miner_tx_hash": "7a42667237d4f79891bb407c49c712a9299fb87fce799833a7b633a3a9377dbd", + "minor_version": 16, + "nonce": 1885649739, + "num_txes": 37, + "orphan_status": false, + "pow_hash": "", + "prev_hash": "22c72248ae9c5a2863c94735d710a3525c499f70707d1c2f395169bc5c8a0da3", + "reward": 615702960000, + "timestamp": 1721245548, + "wide_cumulative_difficulty": "0x514bd6a74a7d0a4", + "wide_difficulty": "0x41aba48bbe" + }, + "credits": 0, + "status": "OK", + "top_hash": "", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_block_header_by_hash (json_rpc), + GET_BLOCK_HEADER_BY_HASH: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_block_header_by_hash", + "params": { + "hash": "e22cf75f39ae720e8b71b3d120a5ac03f0db50bba6379e2850975b4859190bc6" + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "block_header": { + "block_size": 210, + "block_weight": 210, + "cumulative_difficulty": 754734824984346, + "cumulative_difficulty_top64": 0, + "depth": 2282676, + "difficulty": 815625611, + "difficulty_top64": 0, + "hash": "e22cf75f39ae720e8b71b3d120a5ac03f0db50bba6379e2850975b4859190bc6", + "height": 912345, + "long_term_weight": 210, + "major_version": 1, + "miner_tx_hash": "c7da3965f25c19b8eb7dd8db48dcd4e7c885e2491db77e289f0609bf8e08ec30", + "minor_version": 2, + "nonce": 1646, + "num_txes": 0, + "orphan_status": false, + "pow_hash": "", + "prev_hash": "b61c58b2e0be53fad5ef9d9731a55e8a81d972b8d90ed07c04fd37ca6403ff78", + "reward": 7388968946286, + "timestamp": 1452793716, + "wide_cumulative_difficulty": "0x2ae6d65248f1a", + "wide_difficulty": "0x309d758b" + }, + "credits": 0, + "status": "OK", + "top_hash": "", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_block_header_by_height (json_rpc), + GET_BLOCK_HEADER_BY_HEIGHT: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_block_header_by_height", + "params": { + "height": 912345 + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "block_header": { + "block_size": 210, + "block_weight": 210, + "cumulative_difficulty": 754734824984346, + "cumulative_difficulty_top64": 0, + "depth": 2282677, + "difficulty": 815625611, + "difficulty_top64": 0, + "hash": "e22cf75f39ae720e8b71b3d120a5ac03f0db50bba6379e2850975b4859190bc6", + "height": 912345, + "long_term_weight": 210, + "major_version": 1, + "miner_tx_hash": "c7da3965f25c19b8eb7dd8db48dcd4e7c885e2491db77e289f0609bf8e08ec30", + "minor_version": 2, + "nonce": 1646, + "num_txes": 0, + "orphan_status": false, + "pow_hash": "", + "prev_hash": "b61c58b2e0be53fad5ef9d9731a55e8a81d972b8d90ed07c04fd37ca6403ff78", + "reward": 7388968946286, + "timestamp": 1452793716, + "wide_cumulative_difficulty": "0x2ae6d65248f1a", + "wide_difficulty": "0x309d758b" + }, + "credits": 0, + "status": "OK", + "top_hash": "", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_block_headers_range (json_rpc), + GET_BLOCK_HEADERS_RANGE: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_block_headers_range", + "params": { + "start_height": 1545999, + "end_height": 1546000 + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "credits": 0, + "headers": [{ + "block_size": 301413, + "block_weight": 301413, + "cumulative_difficulty": 13185267971483472, + "cumulative_difficulty_top64": 0, + "depth": 1649024, + "difficulty": 134636057921, + "difficulty_top64": 0, + "hash": "86d1d20a40cefcf3dd410ff6967e0491613b77bf73ea8f1bf2e335cf9cf7d57a", + "height": 1545999, + "long_term_weight": 301413, + "major_version": 6, + "miner_tx_hash": "9909c6f8a5267f043c3b2b079fb4eacc49ef9c1dee1c028eeb1a259b95e6e1d9", + "minor_version": 6, + "nonce": 3246403956, + "num_txes": 20, + "orphan_status": false, + "pow_hash": "", + "prev_hash": "0ef6e948f77b8f8806621003f5de24b1bcbea150bc0e376835aea099674a5db5", + "reward": 5025593029981, + "timestamp": 1523002893, + "wide_cumulative_difficulty": "0x2ed7ee6db56750", + "wide_difficulty": "0x1f58ef3541" + },{ + "block_size": 13322, + "block_weight": 13322, + "cumulative_difficulty": 13185402687569710, + "cumulative_difficulty_top64": 0, + "depth": 1649023, + "difficulty": 134716086238, + "difficulty_top64": 0, + "hash": "b408bf4cfcd7de13e7e370c84b8314c85b24f0ba4093ca1d6eeb30b35e34e91a", + "height": 1546000, + "long_term_weight": 13322, + "major_version": 7, + "miner_tx_hash": "7f749c7c64acb35ef427c7454c45e6688781fbead9bbf222cb12ad1a96a4e8f6", + "minor_version": 7, + "nonce": 3737164176, + "num_txes": 1, + "orphan_status": false, + "pow_hash": "", + "prev_hash": "86d1d20a40cefcf3dd410ff6967e0491613b77bf73ea8f1bf2e335cf9cf7d57a", + "reward": 4851952181070, + "timestamp": 1523002931, + "wide_cumulative_difficulty": "0x2ed80dcb69bf2e", + "wide_difficulty": "0x1f5db457de" + }], + "status": "OK", + "top_hash": "", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_block (json_rpc), + GET_BLOCK: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_block", + "params": { + "height": 2751506 + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "blob": "1010c58bab9b06b27bdecfc6cd0a46172d136c08831cf67660377ba992332363228b1b722781e7807e07f502cef8a70101ff92f8a7010180e0a596bb1103d7cbf826b665d7a532c316982dc8dbc24f285cbc18bbcc27c7164cd9b3277a85d034019f629d8b36bd16a2bfce3ea80c31dc4d8762c67165aec21845494e32b7582fe00211000000297a787a000000000000000000000000", + "block_header": { + "block_size": 106, + "block_weight": 106, + "cumulative_difficulty": 236046001376524168, + "cumulative_difficulty_top64": 0, + "depth": 443517, + "difficulty": 313732272488, + "difficulty_top64": 0, + "hash": "43bd1f2b6556dcafa413d8372974af59e4e8f37dbf74dc6b2a9b7212d0577428", + "height": 2751506, + "long_term_weight": 176470, + "major_version": 16, + "miner_tx_hash": "e49b854c5f339d7410a77f2a137281d8042a0ffc7ef9ab24cd670b67139b24cd", + "minor_version": 16, + "nonce": 4110909056, + "num_txes": 0, + "orphan_status": false, + "pow_hash": "", + "prev_hash": "b27bdecfc6cd0a46172d136c08831cf67660377ba992332363228b1b722781e7", + "reward": 600000000000, + "timestamp": 1667941829, + "wide_cumulative_difficulty": "0x3469a966eb2f788", + "wide_difficulty": "0x490be69168" + }, + "credits": 0, + "json": "{\n \"major_version\": 16, \n \"minor_version\": 16, \n \"timestamp\": 1667941829, \n \"prev_id\": \"b27bdecfc6cd0a46172d136c08831cf67660377ba992332363228b1b722781e7\", \n \"nonce\": 4110909056, \n \"miner_tx\": {\n \"version\": 2, \n \"unlock_time\": 2751566, \n \"vin\": [ {\n \"gen\": {\n \"height\": 2751506\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 600000000000, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"d7cbf826b665d7a532c316982dc8dbc24f285cbc18bbcc27c7164cd9b3277a85\", \n \"view_tag\": \"d0\"\n }\n }\n }\n ], \n \"extra\": [ 1, 159, 98, 157, 139, 54, 189, 22, 162, 191, 206, 62, 168, 12, 49, 220, 77, 135, 98, 198, 113, 101, 174, 194, 24, 69, 73, 78, 50, 183, 88, 47, 224, 2, 17, 0, 0, 0, 41, 122, 120, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n ], \n \"rct_signatures\": {\n \"type\": 0\n }\n }, \n \"tx_hashes\": [ ]\n}", + "miner_tx_hash": "e49b854c5f339d7410a77f2a137281d8042a0ffc7ef9ab24cd670b67139b24cd", + "status": "OK", + "top_hash": "", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_block (json_rpc), + /// This is the same as [`GET_BLOCK_REQUEST`] and + /// [`GET_BLOCK_RESPONSE`] but it uses the `hash` parameter. + GET_BLOCK_HASH: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_block", + "params": { + "hash": "86d421322b700166dde2d7eba1cc8600925ef640abf6c0a2cc8ce0d6dd90abfd" + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "blob": "1010d8faa89b06f8a36d0dbe4d27d2f52160000563896048d71067c31e99a3869bf9b7142227bb5328010b02a6f6a70101ffeaf5a70101a08bc8b3bb11036d6713f5aa552a1aaf33baed7591f795b86daf339e51029a9062dfe09f0f909b312b0124d6023d591c4d434000e5e31c6db718a1e96e865939930e90a7042a1cd4cbd202083786a78452fdfc000002a89e380a44d8dfc64b551baa171447a0f9c9262255be6e8f8ef10896e36e2bf90c4d343e416e394ad9cc10b7d2df7b2f39370a554730f75dfcb04944bd62c299", + "block_header": { + "block_size": 3166, + "block_weight": 3166, + "cumulative_difficulty": 235954020187853162, + "cumulative_difficulty_top64": 0, + "depth": 443814, + "difficulty": 312527777859, + "difficulty_top64": 0, + "hash": "86d421322b700166dde2d7eba1cc8600925ef640abf6c0a2cc8ce0d6dd90abfd", + "height": 2751210, + "long_term_weight": 176470, + "major_version": 16, + "miner_tx_hash": "dabe07900d3123ed895612f4a151adb3e39681b145f0f85bfee23ea1fe47acf2", + "minor_version": 16, + "nonce": 184625235, + "num_txes": 2, + "orphan_status": false, + "pow_hash": "", + "prev_hash": "f8a36d0dbe4d27d2f52160000563896048d71067c31e99a3869bf9b7142227bb", + "reward": 600061380000, + "timestamp": 1667906904, + "wide_cumulative_difficulty": "0x34646ee649f516a", + "wide_difficulty": "0x48c41b7043" + }, + "credits": 0, + "json": "{\n \"major_version\": 16, \n \"minor_version\": 16, \n \"timestamp\": 1667906904, \n \"prev_id\": \"f8a36d0dbe4d27d2f52160000563896048d71067c31e99a3869bf9b7142227bb\", \n \"nonce\": 184625235, \n \"miner_tx\": {\n \"version\": 2, \n \"unlock_time\": 2751270, \n \"vin\": [ {\n \"gen\": {\n \"height\": 2751210\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 600061380000, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"6d6713f5aa552a1aaf33baed7591f795b86daf339e51029a9062dfe09f0f909b\", \n \"view_tag\": \"31\"\n }\n }\n }\n ], \n \"extra\": [ 1, 36, 214, 2, 61, 89, 28, 77, 67, 64, 0, 229, 227, 28, 109, 183, 24, 161, 233, 110, 134, 89, 57, 147, 14, 144, 167, 4, 42, 28, 212, 203, 210, 2, 8, 55, 134, 167, 132, 82, 253, 252, 0\n ], \n \"rct_signatures\": {\n \"type\": 0\n }\n }, \n \"tx_hashes\": [ \"a89e380a44d8dfc64b551baa171447a0f9c9262255be6e8f8ef10896e36e2bf9\", \"0c4d343e416e394ad9cc10b7d2df7b2f39370a554730f75dfcb04944bd62c299\"\n ]\n}", + "miner_tx_hash": "dabe07900d3123ed895612f4a151adb3e39681b145f0f85bfee23ea1fe47acf2", + "status": "OK", + "top_hash": "", + "tx_hashes": ["a89e380a44d8dfc64b551baa171447a0f9c9262255be6e8f8ef10896e36e2bf9","0c4d343e416e394ad9cc10b7d2df7b2f39370a554730f75dfcb04944bd62c299"], + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_connections (json_rpc), + GET_CONNECTIONS: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_connections" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "connections": [{ + "address": "3evk3kezfjg44ma6tvesy7rbxwwpgpympj45xar5fo4qajrsmkoaqdqd.onion:18083", + "address_type": 4, + "avg_download": 0, + "avg_upload": 0, + "connection_id": "22ef856d0f1d44cc95e84fecfd065fe2", + "current_download": 0, + "current_upload": 0, + "height": 3195026, + "host": "3evk3kezfjg44ma6tvesy7rbxwwpgpympj45xar5fo4qajrsmkoaqdqd.onion", + "incoming": false, + "ip": "", + "live_time": 76651, + "local_ip": false, + "localhost": false, + "peer_id": "0000000000000001", + "port": "", + "pruning_seed": 0, + "recv_count": 240328, + "recv_idle_time": 34, + "rpc_credits_per_hash": 0, + "rpc_port": 0, + "send_count": 3406572, + "send_idle_time": 30, + "state": "normal", + "support_flags": 0 + },{ + "address": "4iykytmumafy5kjahdqc7uzgcs34s2vwsadfjpk4znvsa5vmcxeup2qd.onion:18083", + "address_type": 4, + "avg_download": 0, + "avg_upload": 0, + "connection_id": "c7734e15936f485a86d2b0534f87e499", + "current_download": 0, + "current_upload": 0, + "height": 3195024, + "host": "4iykytmumafy5kjahdqc7uzgcs34s2vwsadfjpk4znvsa5vmcxeup2qd.onion", + "incoming": false, + "ip": "", + "live_time": 76755, + "local_ip": false, + "localhost": false, + "peer_id": "0000000000000001", + "port": "", + "pruning_seed": 389, + "recv_count": 237657, + "recv_idle_time": 120, + "rpc_credits_per_hash": 0, + "rpc_port": 0, + "send_count": 3370566, + "send_idle_time": 120, + "state": "normal", + "support_flags": 0 + }], + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_info (json_rpc), + GET_INFO: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_info" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "adjusted_time": 1721245289, + "alt_blocks_count": 16, + "block_size_limit": 600000, + "block_size_median": 300000, + "block_weight_limit": 600000, + "block_weight_median": 300000, + "bootstrap_daemon_address": "", + "busy_syncing": false, + "credits": 0, + "cumulative_difficulty": 366127702242611947, + "cumulative_difficulty_top64": 0, + "database_size": 235169075200, + "difficulty": 280716748706, + "difficulty_top64": 0, + "free_space": 30521749504, + "grey_peerlist_size": 4996, + "height": 3195028, + "height_without_bootstrap": 3195028, + "incoming_connections_count": 62, + "mainnet": true, + "nettype": "mainnet", + "offline": false, + "outgoing_connections_count": 1143, + "restricted": false, + "rpc_connections_count": 1, + "stagenet": false, + "start_time": 1720462427, + "status": "OK", + "synchronized": true, + "target": 120, + "target_height": 0, + "testnet": false, + "top_block_hash": "bdf06d18ed1931a8ee62654e9b6478cc459bc7072628b8e36f4524d339552946", + "top_hash": "", + "tx_count": 43205750, + "tx_pool_size": 12, + "untrusted": false, + "update_available": false, + "version": "0.18.3.3-release", + "was_bootstrap_ever_used": false, + "white_peerlist_size": 1000, + "wide_cumulative_difficulty": "0x514bf349299d2eb", + "wide_difficulty": "0x415c05a7a2" + } +}"#; +} + +define_request_and_response! { + hard_fork_info (json_rpc), + HARD_FORK_INFO: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "hard_fork_info" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "credits": 0, + "earliest_height": 2689608, + "enabled": true, + "state": 0, + "status": "OK", + "threshold": 0, + "top_hash": "", + "untrusted": false, + "version": 16, + "votes": 10080, + "voting": 16, + "window": 10080 + } +}"#; +} + +define_request_and_response! { + set_bans (json_rpc), + SET_BANS: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "set_bans", + "params": { + "bans": [{ + "host": "192.168.1.51", + "ban": true, + "seconds": 30 + }] + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + set_bans (json_rpc), + /// This is the same as [`SET_BANS_REQUEST`] and + /// [`SET_BANS_RESPONSE`] but it uses the `ip` parameter. + SET_BANS_IP: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "set_bans", + "params": { + "bans": [{ + "ip": 838969536, + "ban": true, + "seconds": 30 + }] + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_bans (json_rpc), + GET_BANS: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_bans" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "bans": [{ + "host": "104.248.206.131", + "ip": 2211379304, + "seconds": 689754 + },{ + "host": "209.222.252.0\/24", + "ip": 0, + "seconds": 689754 + }], + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + banned (json_rpc), + BANNED: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "banned", + "params": { + "address": "95.216.203.255" + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "banned": true, + "seconds": 689655, + "status": "OK" + } +}"#; +} + +define_request_and_response! { + flush_txpool (json_rpc), + FLUSH_TRANSACTION_POOL: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "flush_txpool", + "params": { + "txids": ["dc16fa8eaffe1484ca9014ea050e13131d3acf23b419f33bb4cc0b32b6c49308"] + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "status": "OK" + } +}"#; +} + +define_request_and_response! { + get_output_histogram (json_rpc), + GET_OUTPUT_HISTOGRAM: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_output_histogram", + "params": { + "amounts": [20000000000] + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "credits": 0, + "histogram": [{ + "amount": 20000000000, + "recent_instances": 0, + "total_instances": 381490, + "unlocked_instances": 0 + }], + "status": "OK", + "top_hash": "", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_coinbase_tx_sum (json_rpc), + GET_COINBASE_TX_SUM: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_coinbase_tx_sum", + "params": { + "height": 1563078, + "count": 2 + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "credits": 0, + "emission_amount": 9387854817320, + "emission_amount_top64": 0, + "fee_amount": 83981380000, + "fee_amount_top64": 0, + "status": "OK", + "top_hash": "", + "untrusted": false, + "wide_emission_amount": "0x889c7c06828", + "wide_fee_amount": "0x138dae29a0" + } +}"#; +} + +define_request_and_response! { + get_version (json_rpc), + GET_VERSION: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_version" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "current_height": 3195051, + "hard_forks": [{ + "height": 1, + "hf_version": 1 + },{ + "height": 1009827, + "hf_version": 2 + },{ + "height": 1141317, + "hf_version": 3 + },{ + "height": 1220516, + "hf_version": 4 + },{ + "height": 1288616, + "hf_version": 5 + },{ + "height": 1400000, + "hf_version": 6 + },{ + "height": 1546000, + "hf_version": 7 + },{ + "height": 1685555, + "hf_version": 8 + },{ + "height": 1686275, + "hf_version": 9 + },{ + "height": 1788000, + "hf_version": 10 + },{ + "height": 1788720, + "hf_version": 11 + },{ + "height": 1978433, + "hf_version": 12 + },{ + "height": 2210000, + "hf_version": 13 + },{ + "height": 2210720, + "hf_version": 14 + },{ + "height": 2688888, + "hf_version": 15 + },{ + "height": 2689608, + "hf_version": 16 + }], + "release": true, + "status": "OK", + "untrusted": false, + "version": 196621 + } +}"#; +} + +define_request_and_response! { + get_fee_estimate (json_rpc), + GET_FEE_ESTIMATE: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_fee_estimate" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "credits": 0, + "fee": 20000, + "fees": [20000,80000,320000,4000000], + "quantization_mask": 10000, + "status": "OK", + "top_hash": "", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_alternate_chains (json_rpc), + GET_ALTERNATE_CHAINS: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_alternate_chains" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "chains": [{ + "block_hash": "4826c7d45d7cf4f02985b5c405b0e5d7f92c8d25e015492ce19aa3b209295dce", + "block_hashes": ["4826c7d45d7cf4f02985b5c405b0e5d7f92c8d25e015492ce19aa3b209295dce"], + "difficulty": 357404825113208373, + "difficulty_top64": 0, + "height": 3167471, + "length": 1, + "main_chain_parent_block": "69b5075ea627d6ba06b1c30b7e023884eeaef5282cf58ec847dab838ddbcdd86", + "wide_difficulty": "0x4f5c1cb79e22635" + },{ + "block_hash": "33ee476f5a1c5b9d889274cbbe171f5e0112df7ed69021918042525485deb401", + "block_hashes": ["33ee476f5a1c5b9d889274cbbe171f5e0112df7ed69021918042525485deb401"], + "difficulty": 354736121711617293, + "difficulty_top64": 0, + "height": 3157465, + "length": 1, + "main_chain_parent_block": "fd522fcc4cefe5c8c0e5c5600981b3151772c285df3a4e38e5c4011cf466d2cb", + "wide_difficulty": "0x4ec469f8b9ee50d" + }], + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + relay_tx (json_rpc), + RELAY_TX: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "relay_tx", + "params": { + "txids": ["9fd75c429cbe52da9a52f2ffc5fbd107fe7fd2099c0d8de274dc8a67e0c98613"] + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "status": "OK" + } +}"#; +} + +define_request_and_response! { + sync_info (json_rpc), + SYNC_INFO: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "sync_info" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "credits": 0, + "height": 3195157, + "next_needed_pruning_seed": 0, + "overview": "[]", + "peers": [{ + "info": { + "address": "142.93.128.65:44986", + "address_type": 1, + "avg_download": 1, + "avg_upload": 1, + "connection_id": "a5803c4c2dac49e7b201dccdef54c862", + "current_download": 2, + "current_upload": 1, + "height": 3195157, + "host": "142.93.128.65", + "incoming": true, + "ip": "142.93.128.65", + "live_time": 18, + "local_ip": false, + "localhost": false, + "peer_id": "6830e9764d3e5687", + "port": "44986", + "pruning_seed": 0, + "recv_count": 20340, + "recv_idle_time": 0, + "rpc_credits_per_hash": 0, + "rpc_port": 18089, + "send_count": 32235, + "send_idle_time": 6, + "state": "normal", + "support_flags": 1 + } + },{ + "info": { + "address": "4iykytmumafy5kjahdqc7uzgcs34s2vwsadfjpk4znvsa5vmcxeup2qd.onion:18083", + "address_type": 4, + "avg_download": 0, + "avg_upload": 0, + "connection_id": "277f7c821bc546878c8bd29977e780f5", + "current_download": 0, + "current_upload": 0, + "height": 3195157, + "host": "4iykytmumafy5kjahdqc7uzgcs34s2vwsadfjpk4znvsa5vmcxeup2qd.onion", + "incoming": false, + "ip": "", + "live_time": 2246, + "local_ip": false, + "localhost": false, + "peer_id": "0000000000000001", + "port": "", + "pruning_seed": 389, + "recv_count": 65164, + "recv_idle_time": 15, + "rpc_credits_per_hash": 0, + "rpc_port": 0, + "send_count": 99120, + "send_idle_time": 15, + "state": "normal", + "support_flags": 0 + } + }], + "status": "OK", + "target_height": 0, + "top_hash": "", + "untrusted": false + } +}"#; +} + +// TODO: binary string. +// define_request_and_response! { +// get_txpool_backlog (json_rpc), +// GET_TRANSACTION_POOL_BACKLOG: &str, +// Request = +// r#"{ +// "jsonrpc": "2.0", +// "id": "0", +// "method": "get_txpool_backlog" +// }"#; +// Response = +// r#"{ +// "id": "0", +// "jsonrpc": "2.0", +// "result": { +// "backlog": "...Binary...", +// "status": "OK", +// "untrusted": false +// } +// }"#; +// } + +define_request_and_response! { + get_output_distribution (json_rpc), + GET_OUTPUT_DISTRIBUTION: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_output_distribution", + "params": { + "amounts": [628780000], + "from_height": 1462078 + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "credits": 0, + "distributions": [{ + "amount": 2628780000, + "base": 0, + "distribution": "", + "start_height": 1462078, + "binary": false + }], + "status": "OK", + "top_hash": "", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + get_miner_data (json_rpc), + GET_MINER_DATA: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_miner_data" +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "already_generated_coins": 18186022843595960691, + "difficulty": "0x48afae42de", + "height": 2731375, + "major_version": 16, + "median_weight": 300000, + "prev_id": "78d50c5894d187c4946d54410990ca59a75017628174a9e8c7055fa4ca5c7c6d", + "seed_hash": "a6b869d50eca3a43ec26fe4c369859cf36ae37ce6ecb76457d31ffeb8a6ca8a6", + "status": "OK", + "tx_backlog": [{ + "fee": 30700000, + "id": "9868490d6bb9207fdd9cf17ca1f6c791b92ca97de0365855ea5c089f67c22208", + "weight": 1535 + },{ + "fee": 44280000, + "id": "b6000b02bbec71e18ad704bcae09fb6e5ae86d897ced14a718753e76e86c0a0a", + "weight": 2214 + }], + "untrusted": false + } +}"#; +} + +define_request_and_response! { + prune_blockchain (json_rpc), + PRUNE_BLOCKCHAIN: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "prune_blockchain", + "params": { + "check": true + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "pruned": true, + "pruning_seed": 387, + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + calc_pow (json_rpc), + CALC_POW: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "calc_pow", + "params": { + "major_version": 14, + "height": 2286447, + "block_blob": "0e0ed286da8006ecdc1aab3033cf1716c52f13f9d8ae0051615a2453643de94643b550d543becd0000000002abc78b0101ffefc68b0101fcfcf0d4b422025014bb4a1eade6622fd781cb1063381cad396efa69719b41aa28b4fce8c7ad4b5f019ce1dc670456b24a5e03c2d9058a2df10fec779e2579753b1847b74ee644f16b023c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051399a1bc46a846474f5b33db24eae173a26393b976054ee14f9feefe99925233802867097564c9db7a36af5bb5ed33ab46e63092bd8d32cef121608c3258edd55562812e21cc7e3ac73045745a72f7d74581d9a0849d6f30e8b2923171253e864f4e9ddea3acb5bc755f1c4a878130a70c26297540bc0b7a57affb6b35c1f03d8dbd54ece8457531f8cba15bb74516779c01193e212050423020e45aa2c15dcb", + "seed_hash": "d432f499205150873b2572b5f033c9c6e4b7c6f3394bd2dd93822cd7085e7307" + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": "d0402d6834e26fb94a9ce38c6424d27d2069896a9b8b1ce685d79936bca6e0a8" +}"#; +} + +define_request_and_response! { + flush_cache (json_rpc), + FLUSH_CACHE: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "flush_cache", + "params": { + "bad_txs": true, + "bad_blocks": true + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + add_aux_pow (json_rpc), + ADD_AUX_POW: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "add_aux_pow", + "params": { + "blocktemplate_blob": "1010f4bae0b4069d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a0000000002c681c30101ff8a81c3010180e0a596bb11033b7eedf47baf878f3490cb20b696079c34bd017fe59b0d070e74d73ffabc4bb0e05f011decb630f3148d0163b3bd39690dde4078e4cfb69fecf020d6278a27bad10c58023c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "aux_pow": [{ + "id": "3200b4ea97c3b2081cd4190b58e49572b2319fed00d030ad51809dff06b5d8c8", + "hash": "7b35762de164b20885e15dbe656b1138db06bb402fa1796f5765a23933d8859a" + }] + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "aux_pow": [{ + "hash": "7b35762de164b20885e15dbe656b1138db06bb402fa1796f5765a23933d8859a", + "id": "3200b4ea97c3b2081cd4190b58e49572b2319fed00d030ad51809dff06b5d8c8" + }], + "blockhashing_blob": "1010ee97e2a106e9f8ebe8887e5b609949ac8ea6143e560ed13552b110cb009b21f0cfca1eaccf00000000b2685c1283a646bc9020c758daa443be145b7370ce5a6efacb3e614117032e2c22", + "blocktemplate_blob": "1010f4bae0b4069d648e741d85ca0e7acb4501f051b27e9b107d3cd7a3f03aa7f776089117c81a0000000002c681c30101ff8a81c3010180e0a596bb11033b7eedf47baf878f3490cb20b696079c34bd017fe59b0d070e74d73ffabc4bb0e05f011decb630f3148d0163b3bd39690dde4078e4cfb69fecf020d6278a27bad10c58023c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "merkle_root": "7b35762de164b20885e15dbe656b1138db06bb402fa1796f5765a23933d8859a", + "merkle_tree_depth": 0, + "status": "OK", + "untrusted": false + } +}"#; +} + +define_request_and_response! { + UNDOCUMENTED_METHOD (json_rpc), + GET_TX_IDS_LOOSE: &str, + Request = +r#"{ + "jsonrpc": "2.0", + "id": "0", + "method": "get_txids_loose", + "params": { + "txid_template": "0000000000000000aea473c43708aa50b2c9eaf0e441aa209afc9b43458fb09e", + "num_matching_bits": 192 + } +}"#; + Response = +r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "txids": "", + "status": "OK", + "untrusted": false + } +}"#; +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/test-utils/src/rpc/data/macros.rs b/test-utils/src/rpc/data/macros.rs new file mode 100644 index 00000000..632917a2 --- /dev/null +++ b/test-utils/src/rpc/data/macros.rs @@ -0,0 +1,168 @@ +//! Macros. + +//---------------------------------------------------------------------------------------------------- define_request_and_response +/// A template for generating the RPC request and response `const` data. +/// +/// See the [`crate::json`] module for example usage. +/// +/// # Macro internals +/// This macro uses: +/// - [`define_request_and_response_doc`] +/// - [`define_request_and_response_test`] +macro_rules! define_request_and_response { + ( + // The markdown tag for Monero daemon RPC documentation. Not necessarily the endpoint. + // + // Adding `(json)` after this will trigger the macro to automatically + // add a `serde_json` test for the request/response data. + $monero_daemon_rpc_doc_link:ident $(($test:ident))?, + + // The base name. + // Attributes added here will apply to _both_ + // request and response types. + $( #[$attr:meta] )* + $name:ident: $type:ty, + + // The request type (and any doc comments, derives, etc). + $( #[$request_attr:meta] )* + Request = $request:expr; + + // The response type (and any doc comments, derives, etc). + $( #[$response_attr:meta] )* + Response = $response:expr; + ) => { paste::paste! { + #[doc = $crate::rpc::data::macros::define_request_and_response_doc!( + "response" => [<$name:upper _RESPONSE>], + $monero_daemon_rpc_doc_link, + )] + /// + $( #[$attr] )* + /// + $( #[$request_attr] )* + /// + $( + #[doc = $crate::rpc::data::macros::define_request_and_response_doc_test!([<$name:upper _REQUEST>], $test)] + )? + pub const [<$name:upper _REQUEST>]: $type = $request; + + #[doc = $crate::rpc::data::macros::define_request_and_response_doc!( + "request" => [<$name:upper _REQUEST>], + $monero_daemon_rpc_doc_link, + )] + /// + $( #[$attr] )* + /// + $( #[$response_attr] )* + /// + $( + #[doc = $crate::rpc::data::macros::define_request_and_response_doc_test!([<$name:upper _RESPONSE>], $test)] + )? + pub const [<$name:upper _RESPONSE>]: $type = $response; + }}; +} +pub(super) use define_request_and_response; + +//---------------------------------------------------------------------------------------------------- define_request_and_response_doc +/// Generate documentation for the types generated +/// by the [`define_request_and_response`] macro. +/// +/// See it for more info on inputs. +macro_rules! define_request_and_response_doc { + ( + // This labels the last `[request]` or `[response]` + // hyperlink in documentation. Input is either: + // - "request" + // - "response" + // + // Remember this is linking to the _other_ type, + // so if defining a `Request` type, input should + // be "response". + $request_or_response:literal => $request_or_response_type:ident, + $monero_daemon_rpc_doc_link:ident, + ) => { + concat!( + "", + "[Documentation](", + "https://www.getmonero.org/resources/developer-guides/daemon-rpc.html", + "#", + stringify!($monero_daemon_rpc_doc_link), + "), [", + $request_or_response, + "](", + stringify!($request_or_response_type), + ")." + ) + }; +} +pub(super) use define_request_and_response_doc; + +//---------------------------------------------------------------------------------------------------- define_request_and_response_test +/// Generate documentation for the types generated +/// by the [`define_request_and_response`] macro. +/// +/// See it for more info on inputs. +macro_rules! define_request_and_response_doc_test { + // `/json_rpc` doc test. + ( + // The ident of the `const` request/response. + $name:ident, + json_rpc + ) => { + concat!( + "```rust\n", + "use cuprate_test_utils::rpc::data::json::*;\n", + "use serde_json::{to_value, Value};\n", + "\n", + "let value = serde_json::from_str::(&", + stringify!($name), + ").unwrap();\n", + "let Value::Object(map) = value else {\n", + " panic!();\n", + "};\n", + "\n", + r#"assert_eq!(map.get("jsonrpc").unwrap(), "2.0");"#, + "\n", + r#"map.get("id").unwrap();"#, + "\n\n", + r#"if map.get("method").is_some() {"#, + "\n", + r#" return;"#, + "\n", + "}\n", + "\n", + r#"if map.get("result").is_none() {"#, + "\n", + r#" map.get("error").unwrap();"#, + "\n", + "}\n", + "\n", + "```\n", + ) + }; + + // Other JSON endpoint doc test. + ( + $name:ident, + other + ) => { + concat!( + "```rust\n", + "use cuprate_test_utils::rpc::data::other::*;\n", + "use serde_json::{to_value, Value};\n", + "\n", + "let value = serde_json::from_str::(&", + stringify!($name), + ");\n", + "```\n", + ) + }; + + // No doc test. + ( + $name:ident, + $test:ident, + ) => { + "" + }; +} +pub(super) use define_request_and_response_doc_test; diff --git a/test-utils/src/rpc/data/mod.rs b/test-utils/src/rpc/data/mod.rs new file mode 100644 index 00000000..09f0d602 --- /dev/null +++ b/test-utils/src/rpc/data/mod.rs @@ -0,0 +1,18 @@ +//! Monero RPC data. +//! +//! This module contains real `monerod` RPC requests/responses +//! as `const` [`str`]s and byte arrays (binary). +//! +//! The strings include the JSON-RPC 2.0 portions of the JSON. +//! - Tests exist within this crate that ensure the JSON is valid +//! - Tests exist within Cuprate's `rpc/` crates that ensure these strings (de)serialize as valid types +//! +//! # Determinism +//! Note that although both request/response data is defined, +//! they aren't necessarily tied to each other, i.e. the request +//! will not deterministically lead to the response. + +pub mod bin; +pub mod json; +mod macros; +pub mod other; diff --git a/test-utils/src/rpc/data/other.rs b/test-utils/src/rpc/data/other.rs new file mode 100644 index 00000000..80a48ab1 --- /dev/null +++ b/test-utils/src/rpc/data/other.rs @@ -0,0 +1,862 @@ +//! JSON data from the [`other`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#other-daemon-rpc-calls) endpoints. + +//---------------------------------------------------------------------------------------------------- Import +use crate::rpc::data::macros::define_request_and_response; + +//---------------------------------------------------------------------------------------------------- TODO +define_request_and_response! { + // `(other)` adds a JSON sanity-check test. + get_height (other), + GET_HEIGHT: &str, + Request = +r#"{}"#; + Response = +r#"{ + "hash": "68bb1a1cff8e2a44c3221e8e1aff80bc6ca45d06fa8eff4d2a3a7ac31d4efe3f", + "height": 3195160, + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + get_transactions (other), + GET_TRANSACTIONS: &str, + Request = +r#"{ + "txs_hashes": ["d6e48158472848e6687173a91ae6eebfa3e1d778e65252ee99d7515d63090408"] +}"#; + Response = +r#"{ + "credits": 0, + "status": "OK", + "top_hash": "", + "txs": [{ + "as_hex": "0100940102ffc7afa02501b3056ebee1b651a8da723462b4891d471b990ddc226049a0866d3029b8e2f75b70120280a0b6cef785020190dd0a200bd02b70ee707441a8863c5279b4e4d9f376dc97a140b1e5bc7d72bc5080690280c0caf384a30201d0b12b751e8f6e2e31316110fa6631bf2eb02e88ac8d778ec70d42b24ef54843fd75d90280d0dbc3f40201c498358287895f16b62a000a3f2fd8fb2e70d8e376858fb9ba7d9937d3a076e36311bb0280f092cbdd0801e5a230c6250d5835877b735c71d41587082309bf593d06a78def1b4ec57355a37838b5028080bfb59dd20d01c36c6dd3a9826658642ba4d1d586366f2782c0768c7e9fb93f32e8fdfab18c0228ed0280d0b8e1981a01bfb0158a530682f78754ab5b1b81b15891b2c7a22d4d7a929a5b51c066ffd73ac360230280f092cbdd0801f9a330a1217984cc5d31bf0e76ed4f8e3d4115f470824bc214fa84929fcede137173a60280e0bcefa75701f3910e3a8b3c031e15573f7a69db9f8dda3b3f960253099d8f844169212f2de772f6ff0280d0b8e1981a01adc1157920f2c72d6140afd4b858da3f41d07fc1655f2ebe593d32f96d5335d11711ee0280d0dbc3f40201ca8635a1373fa829f58e8f46d72c8e52aa1ce53fa1d798284ed08b44849e2e9ad79b620280a094a58d1d01faf729e5ab208fa809dd2efc6f0b74d3e7eff2a66c689a3b5c31c33c8a14e2359ac484028080e983b1de1601eced0182c8d37d77ce439824ddb3c8ff7bd60642181e183c409545c9d6f9c36683908f028080d194b57401ead50b3eefebb5303e14a5087de37ad1799a4592cf0e897eafb46d9b57257b5732949e0280a094a58d1d01d3882a1e949b2d1b6fc1fd5e44df95bae9068b090677d76b6c307188da44dd4e343cef028090cad2c60e0196c73a74a60fc4ce3a7b14d1abdf7a0c70a9efb490a9de6ec6208a846f8282d878132b028080bb8b939b4401c03dbcbfd9fb02e181d99c0093e53aceecf42bf6ccc0ec611a5093fe6f2b2738a07f0280f092cbdd0801b98d30c27f297ae4cb89fb7bb29ed11adff17db9b71d39edf736172892784897488c5d0280d0dbc3f40201da9a353d39555c27a2d620bf69136e4c665aaa19557d6fc0255cbb67ec69bf2403b63e0280b09dc2df0101f8820caab7a5e736f5445b5624837de46e9ef906cb538f7c860f688a7f7d155e19e0ac0280808d93f5d77101b544e62708eb27ff140b58c521e4a90acab5eca36f1ce9516a6318306f7d48beddbc0280a0b6cef7850201abdd0a5453712326722427f66b865e67f8cdb7188001aaacb70f1a018403d3289fcb130280c0caf384a30201a2b32b2cfb06b7022668b2cd5b8263a162511c03154b259ce91c6c97270e4c19efe4710280c0caf384a302018db32bda81bfbe5f9cdf94b20047d12a7fb5f097a83099fafdfedc03397826fb4d18d50280c0fc82aa0201b2e60b825e8c0360b4b44f4fe0a30f4d2f18c80d5bbb7bfc5ddf671f27b6867461c51d028080e983b1de1601b2eb0156dd7ab6dcb0970d4a5dbcb4e04281c1db350198e31893cec9b9d77863fedaf60280e08d84ddcb0101e0960fe3cafedb154111449c5112fc1d9e065222ed0243de3207c3e6f974941a66f177028080df9ad7949f01019815c8c5032f2c28e7e6c9f9c70f6fccdece659d8df53e54ad99a0f7fa5d831cf762028090dfc04a01b4fb123d97504b9d832f7041c4d1db1cda3b7a6d307194aff104ec6b711cced2b005e2028080dd9da41701bef1179c9a459e75a0c4cf4aff1a81f31f477bd682e28a155231da1a1aa7a25ef219910280d88ee16f01facd0f043485225a1e708aa89d71f951bc092724b53942a67a35b2315bfeac4e8af0eb0280d0dbc3f40201c4e634d6e1f3a1b232ef130d4a5417379c4fcc9d078f71899f0617cec8b1e72a1844b60280f092cbdd0801f6b9300c8c94a337fefc1c19f12dee0f2551a09ee0aaa954d1762c93fec8dadae2146c0280c0f9decfae0101ce8d09f26c90144257b5462791487fd1b017eb283268b1c86c859c4194bf1a987c62bf0280c0caf384a30201cead2bbb01653d0d7ff8a42958040814c3cbf228ebb772e03956b367bace3b684b9b7f0280a0e5b9c2910101c1b40c1796904ac003f7a6dd72b4845625e99ba12bdd003e65b2dd2760a4e460821178028080e983b1de160186e9013f55160cd9166756ea8e2c9af065dcdfb16a684e9376c909d18b65fd5306f9690280a0e5b9c2910101aeb70c4433f95ff4cdc4aa54a1ede9ae725cec06350db5d3056815486e761e381ae4d00280c0a8ca9a3a01ebe2139bd558b63ebb9f4d12aca270159ccf565e9cffaadd717ce200db779f202b106f0280d0dbc3f40201b9963568acf599958be4e72f71c3446332a39c815876c185198fa2dcf13877eba3627b0280c0f4c198af0b01bccc01408cbb5a210ad152bd6138639673a6161efd2f85be310b477ae14891870985f90280a0b6cef7850201cadd0a82e7950f5d9e62d14d0f7c6af84002ea9822cdeefabbb866b7a5776c6436636b028080d287e2bc2d01d888013a7146b96a7abc5ce5249b7981fb54250eef751964ff00530915084479b5d6ba028080d287e2bc2d018c8901d8cc1933366dceb49416b2f48fd2ce297cfd8da8baadc7f63856c46130368fca0280a0b787e90501b6d242f93a6570ad920332a354b14ad93b37c0f3815bb5fa2dcc7ca5e334256bd165320280a0e5b9c2910101a3ac0c4ed5ebf0c11285c351ddfd0bb52bd225ee0f8a319025dc416a5e2ba8e84186680280c0f9decfae0101f18709daddc52dccb6e1527ac83da15e19c2272206ee0b2ac37ac478b4dd3e6bcac5dc0280f8cce2840201b7c80bc872a1e1323d61342a2a7ac480b4b061163811497e08698057a8774493a1abe50280e0bcefa75701f38b0e532d34791916b1f56c3f008b2231de5cc62bd1ec898c62d19fb1ec716d467ae20280c0fc82aa0201d6da0b7de4dc001430473852620e13e5931960c55ab6ebeff574fbddea995cbc9d7c010280c0f4c198af0b01edca017ec6af4ece2622edaf9914cfb1cc6663639285256912d7d9d70e905120a6961c028090cad2c60e01cad43abcc63a192d53fe8269ecaf2d9ca3171c2172d85956fe44fcc1ac01efe4c610dd0280809aa6eaafe30101a92fccd2bcadfa42dcbd28d483d32abde14b377b72c4e6ef31a1f1e0ff6c2c9f452f0280a0b787e9050197d9420ce413f5321a785cd5bea4952e6c32acd0b733240a2ea2835747bb916a032da7028080d287e2bc2d01c48a01a3c4afcbbdac70524b27585c68ed1f8ea3858c1c392aeb7ada3432f3eed7cab10280f092cbdd0801abb130d362484a00ea63b2a250a8ae7cf8b904522638838a460653a3c37db1b76ff3de0280e08d84ddcb0101cc980fce5c2d8b34b3039e612adeb707f9ab397c75f76c1f0da8af92c64cd021976ff3028080dd9da4170198c217e4a7b63fd645bd27aa5afc6fae7db1e66896cece0d4b84ac76428268b5c213c30280a0b6cef7850201e3e20acab086a758c56372cf461e5339c09745ee510a785b540d68c7f62c35b8b6c5c10280a094a58d1d01ab842ac0ba57e87a3f204a56cbecca405419e4c896c817c5ced715d903a104a09735660280e0a596bb1101ade921f1ef2fe56c74320ceb1f6c52506d0b835808474b928ad6309398b42434d27f3d0280d0b8e1981a01dab515d684a324cff4b00753a9ef0868f308b8121cbc79077ede84d70cf6015ec989be0280c0ee8ed20b01f99c227da8d616ce3669517282902cdf1ef75e75a427385270d1a94197b93cf6620c15028080dd9da41701d2d0175c8494f151e585f01e80c303c84eea460b69874b773ba01d20f28a05916111a0028090dfc04a01a4f312c12a9f52a99f69e43979354446fd4e2ba5e2d5fb8aaa17cd25cdf591543149da0280d0dbc3f40201969c3510fbca0efa6d5b0c45dca32a5a91b10608594a58e5154d6a453493d4a0f10cf70280f8cce2840201ddcb0b76ca6a2df4544ea2d9634223becf72b6d6a176eae609d8a496ee7c0a45bec8240280e0bcefa7570180920e95d8d04f9f7a4b678497ded16da4caca0934fc019f582d8e1db1239920914d35028090cad2c60e01c2d43a30bbb2dcbb2b6c361dc49649a6cf733b29df5f6e7504b03a55ee707ed3db2c4e028080d287e2bc2d01c38801941b46cb00712de68cebc99945dc7b472b352c9a2e582a9217ea6d0b8c3f07590280e0a596bb1101b6eb219463e6e8aa6395eefc4e7d2d7d6484b5f684e7018fe56d3d6ddca82f4b89c5840280d0dbc3f40201db9535db1fb02f4a45c21eae26f5c40c01ab1bca304deac2fb08d2b3d9ac4f65fd10c60280a094a58d1d01948c2a413da2503eb92880f02f764c2133ed6f2951ae86e8c8c17d1e9e024ca4dc72320280c0ee8ed20b01869f22d3e106082527d6f0b052106a4850801fcd59d0b6ce61b237c2321111ed8bdf47028080d194b57401acd20b9c0e61b23698c4b4d47965a597284d409f71d7f16f4997bc04ba042d3cbe044d028090cad2c60e0194b83ac3b448f0bd45f069df6a80e49778c289edeb93b9f213039e53a828e685c270f90280a094a58d1d01bdfb2984b37167dce720d3972eaa50ba42ae1c73ce8e8bc15b5b420e55c9ae96e5ca8c028090dfc04a01abf3120595fbef2082079af5448c6d0d6491aa758576881c1839f4934fa5f6276b33810280e0a596bb1101f9ea2170a571f721540ec01ae22501138fa808045bb8d86b22b1be686b258b2cc999c5028088aca3cf02019cb60d1ffda55346c6612364a9f426a8b9942d9269bef1360f20b8f3ccf57e9996b5f70280e8eda1ba0101aff90c87588ff1bb510a30907357afbf6c3292892c2d9ff41e363889af32e70891cb9b028080d49ca7981201d65ee875df2a98544318a5f4e9aa70a799374b40cff820c132a388736b86ff6c7b7d0280c0caf384a30201dab52bbf532aa44298858b0a313d0f29953ea90efd3ac3421c674dbda79530e4a6b0060280f092cbdd0801c3ab30b0fc9f93dddc6c3e4d976e9c5e4cfee5bfd58415c96a3e7ec05a3172c29f223f0280a094a58d1d01a2812a3e0ec75af0330302c35c582d9a14c8e5f00a0bf84da22eec672c4926ca6fccb10280a094a58d1d01ca842a2b03a22e56f164bae94e43d1c353217c1a1048375731c0c47bb63216e1ef6c480280e08d84ddcb0101d68b0fb2d29505b3f25a8e36f17a2fde13bce41752ecec8c2042a7e1a7d65a0fd35cdf028090cad2c60e0199ce3afa0b62692f1b87324fd4904bf9ffd45ed169d1f5096634a3ba8602919681e5660280c0f9decfae010193ed081977c266b88f1c3cb7027c0216adb506f0e929ce650cd178b81645458c3af4c6028090cad2c60e01eec13a9cce0e6750904e5649907b0bdc6c6963f62fef41ef3932765418d933fc1cd97a0280c0ee8ed20b019ea8228d467474d1073d5c906acdec6ee799b71e16141930c9d66b7f181dbd7a6e924a028080bb8b939b4401c23d3cb4e840ad6766bb0fd6d2b81462f1e4828d2eae341ce3bd8d7ce38b036ac6fb028080e983b1de1601b9e601e3cf485fa441836d83d1f1be6d8611599eccc29f3af832b922e45ab1cd7f31d00280f092cbdd0801fc9e30fff408d7b0c5d88f662dddb5d06ff382baa06191102278e54a0030f7e3246e7c0280d88ee16f01bfcd0f96a24f27ac225278701c6b54df41c6aa511dd86ce682516fb1824ff104c572fb0280f092cbdd0801cbb430bd5f343e45c62efcd6e0f62e77ceb3c95ef945b0cff7002872ea350b5dfffef10280c0caf384a30201bfb22b14dccbba4582da488aef91b530395979f73fa83511e3b3bcb311221c6832b18d0280a0b6cef7850201c4dc0a31fb268316ab21229072833191b7a77b9832afea700a1b93f2e86fb31be9480f028090cad2c60e01cab63a313af96a15c06fcf1f1cf10b69eae50e2c1d005357df457768d384b7a35fa0bb0280d0dbc3f40201fe9835fffd89020879ec3ca02db3eadbb81b0c34a6d77f9d1031438d55fd9c33827db00280d0dbc3f40201d19b354a25ddf2641fc7e000f9306df1c6bf731bddfe9ab148713781bbfab4814ed87e0280e08d84ddcb0101ba960fec80e1dcda9fae2d63e14500354f191f287811f5503e269c9ec1ae56cef4cd110280a0b787e90501acde42b0bdfd00ab0518c8bd6938f0a6efab1b1762704e86c71a154f6d6378dd63ce840280e0bcefa75701b5900eedc2ce12618978351788e46fefe8b9b776510ec32add7423f649c613b9db853a028080e983b1de1601edeb01d68b226cd6b71903a812aa6a6f0799381cf6f70870810df560042cd732b26526028080f696a6b6880101ca18a91fd53d6370a95ca2e0700aabc3784e355afcafb25656c70d780de90e30be31028090cad2c60e0184c03adc307ee3753a20f8f687344aae487639ab12276b604b1f74789d47f1371cac6b0280c0fc82aa0201a2dc0b000aa40a7e3e44d0181aaa5cc64df6307cf119798797fbf82421e3b78a0aa2760280e8eda1ba0101daf20caa8a7c80b400f4dd189e4a00ef1074e26fcc186fed46f0d97814c464aa7561e20280c0f9decfae0101a18b092ee7d48a9fb88cefb22874e5a1ed7a1bf99cc06e93e55c7f75ca4bf38ad185a60280a094a58d1d01dff92904ef53b1415cdb435a1589c072a7e6bd8e69a31bf31153c3beb07ebf585aa838028080bfb59dd20d01916c9d21b580aed847f256b4f507f562858396a9d392adc92b7ed3585d78acf9b38b028080a2a9eae80101fab80b153098181e5fabf1056b4e88db7ce5ed875132e3b7d78ed3b6fc528edda921050280d88ee16f019fcf0fd5f4d68c9afe2e543c125963043024fe557e817c279dbd0602b158fe96ec4b6f0280e0bcefa75701d1910e44b59722c588c30a65b920fc72e0e58c5acc1535b4cad4fc889a89fccfa271510280d0dbc3f40201b78b358b066d485145ada1c39153caacf843fcd9c2f4681d226d210a9a9942109314d90280e0bcefa75701a88b0e5f100858379f9edbbfe92d8f3de825990af354e38edc3b0d246d8a62f01ab3220280d0dbc3f40201959135c6a904269f0bf29fbf8cef1f705dde8c7364ba4618ad9ee378b69a3d590af5680280e0a596bb1101edeb2140e07858aa36619a74b0944c52663b7d3818ab6bf9e66ee792cda1b6dd39842e0280c0a8ca9a3a01aae213a90a6708e741f688a32fb5f1b800800e64cfd341c0f82f8e1ca822336d70c78e0280c0fc82aa02018ddf0b5e03adc078c32952c9077fee655a65a933558c610f23245cd7416669da12611e0280f092cbdd0801aca0305b7157269b35d5068d64f8d43386e8463f2893695bc94f07b4a14f9f5c85e8c50280e0bcefa75701b18f0efd26a0ad840829429252c7e6db2ff0eb7980a8f4c4e400b3a68475f6831cc5f50280b09dc2df0101a6830c2b7555fd29e82d1f0cf6a00f2c671c94c3c683254853c045519d1c5d5dc314fb028080bb8b939b4401be3d76fcfea2c6216513382a75aedaba8932f339ed56f4aad33fb04565429c7f7fa50280c0ee8ed20b01b4a322218a5fd3a13ed0847e8165a28861fb3edc0e2c1603e95e042e2cbb0040e49ab50280c0caf384a30201ecb42b7c10020495d95b3c1ea45ce8709ff4a181771fc053911e5ec51d237d585f19610280f092cbdd0801eba3309533ea103add0540f9624cb24c5cecadf4b93ceb39aa2e541668a0dd23bf3f2f028090dfc04a01a6f3121520ad99387cf4dd779410542b3f5ed9991f0fadbb40f292be0057f4a1dfbf10028090cad2c60e019ac83a125492706ba043a4e3b927ab451c8dccb4b798f83312320dcf4d306bc45c3016028080a2a9eae80101b4ba0bd413da8f7f0aad9cd41d728b4fef20e31fbc61fc397a585c6755134406680b14028080d49ca798120192600ef342c8cf4c0e9ebf52986429add3de7f7757c3d5f7c951810b2fb5352aec620280a0b787e90501afe442797256544eb3515e6fa45b1785d65816dd179bd7f0372a561709f87fae7f95f10280a094a58d1d01dc882aacbd3e13a0b97c2a08b6b6deec5e9685b94409d30c774c85a373b252169d588f028090dfc04a0184f81225e7ded2e83d4f9f0ee64f60c9c8bce2dcb110fd2f3d66c17aafdff53fbf6bbe028080d287e2bc2d01d18901e2fd0eeb4fe9223b4610e05022fcc194240e8afe5472fceda8346cb5b66a0a5902808095e789c60401cf88036cf7317af6dc47cd0ce319a51aaaf936854287c07a24afad791a1431cbd2df5c0280c0f9decfae0101999909d038b9c30a2de009813e56ba2ba17964a24d5f195aaa5f7f2f5fefacd69893e80280a0e5b9c291010199b10cf336c49e2864d07ad3c7a0b9a19e0c17aaf0e72f9fcc980180272000fe5ba1260280a0b6cef7850201a2e20a7a870af412e8fff7eba50b2f8f3be318736996a347fa1222019be9971b6f9b81028090dfc04a01bae5127889a54246328815e9819a05eea4c93bdfffaa2a2cc9747c5d8e74a9a4a8bfe10280f8cce284020191da0b24ee29cd3f554bb618f336dd2841ba23168bf123ee88ebdb48bcbb033a67a02f0280f8cce2840201e6c30b2756e87b0b6ff35103c20c1ddb3b0502f712977fd7909a0b552f1c7dfc3e0c3c028080e983b1de16018fed01a3c245ee280ff115f7e92b16dc2c25831a2da6af5321ad76a1fbbcdd6afc780c0280e0bcefa7570183920ef957193122bb2624d28c0a3cbd4370a1cfff4e1c2e0c8bb22d4c4b47e7f0a5a60280f092cbdd0801ccab30f5440aceabe0c8c408dddf755f789fae2afbf21a64bc183f2d4218a8a792f2870280e08d84ddcb0101f8870f8e26eacca06623c8291d2b29d26ca7f476f09e89c21302d0b85e144267b2712a028080aace938c0901b0b1014c9b9fab49660c2067f4b60151427cf415aa0887447da450652f83a8027524170580b09dc2df01028c792dea94dab48160e067fb681edd6247ba375281fbcfedc03cb970f3b98e2d80b081daaf14021ab33e69737e157d23e33274c42793be06a8711670e73fa10ecebc604f87cc7180a0b6cef78502020752a6308df9466f0838c978216926cb69e113761e84446d5c8453863f06a05c808095e789c60402edc8db59ee3f13d361537cb65c6c89b218e5580a8fbaf9734e9dc71c26a996d780809ce5fd9ed40a024d3ae5019faae01f3e7ae5e978ae0f6a4344976c414b617146f7e76d9c6020c52101038c6d9ccd2f949909115d5321a26e98018b4679138a0a2c06378cf27c8fdbacfd82214a59c99d9251fa00126d353f9cf502a80d8993a6c223e3c802a40ab405555637f495903d3ba558312881e586d452e6e95826d8e128345f6c0a8f9f350e8c04ef50cf34afa3a9ec19c457143496f8cf7045ed869b581f9efa2f1d65e30f1cec5272b00e9c61a34bdd3c78cf82ae8ef4df3132f70861391069b9c255cd0875496ee376e033ee44f8a2d5605a19c88c07f354483a4144f1116143bb212f02fafb5ef7190ce928d2ab32601de56eb944b76935138ca496a345b89b54526a0265614d7932ac0b99289b75b2e18eb4f2918de8cb419bf19d916569b8f90e450bb5bc0da806b7081ecf36da21737ec52379a143632ef483633059741002ee520807713c344b38f710b904c806cf93d3f604111e6565de5f4a64e9d7ea5c24140965684e03cefcb9064ecefb46b82bb5589a6b9baac1800bed502bbc6636ad92026f57fdf2839f36726b0d69615a03b35bb182ec1ef1dcd790a259127a65208e08ea0dd55c8f8cd993c32458562638cf1fb09f77aa7f40e3fecc432f16b2396d0cb7239f7e9f5600bdface5d5f5c0285a9dca1096bd033c4ccf9ceebe063c01e0ec6e2d551189a3d70ae6214a22cd79322de7710ac834c98955d93a5aeed21f900792a98210a1a4a44a17901de0d93e20863a04903e2e77eb119b31c9971653f070ddec02bd08a577bf132323ccf763d6bfc615f1a35802877c6703b70ab7216089a3d5f9b9eacb55ba430484155cb195f736d6c094528b29d3e01032fe61c2c07da6618cf5edad594056db4f6db44adb47721616c4c70e770661634d436e6e90cbcdfdb44603948338401a6ba60c64ca6b51bbf493ecd99ccddd92e6cad20160b0b983744f90cdc4260f60b0776af7c9e664eeb5394ee1182fb6881026271db0a9aad0764782ba106074a0576239681ecae941a9ef56b7b6dda7dbf08ecafac08ab8302d52ee495e4403f2c8b9b18d53ac3863e22d4181688f2bda37943afbf04a436302498f2298b50761eb6e1f43f6354bdc79671b9e97fa239f77924683904e0cf6b1351d4535393a9352d27b007dfda7a8ae8b767e2b5241313d7b5daf20523a80dd6cc9c049da66a5d23f76c132a85d772c45b4c10f2032f58b90c862f09f625cbd18c91a37bb3fc3a413a2e081618da845910cf5b2e6bffea555e883b0bb9c5f9063380a1c33ebdb764d9ffefe9e3169de40b18eeb9bfca48296457bb0b4e29d7b2b5bc4e0021ba0a1d389f77a8e253d6db149d400f675a9330f3bcfd09c7169224a947b6b4e0745ae08cd7adea4151277a94f51f85292ba082cf28300cca233ff4966b093c9cb6abcef476026040fec2b435021aff717b8bb90a40950e010f70bb416a618dc3c5c03f590c5b7ec8e0c05b85ba94078de4817918f783022364b8aa228b5df43b38fba3060c30616f265022584ab6034ddbc832450f90047d0cf41a4af8a20fb1aa66406133a17c2e905ee28d8acd186c872859c196db0474dfaaaded2d63768143cf6b5e2e34662f7bae573a08cb15069ef881892e5a0c08b5c6c7b2e6376cd2080fb29e8d3d5aa5b853662b4f1784ba7f072130e4dc00cba3cc9278fc4213f2ce2fc82bd1ea9fa91bb17b4f7c36962c78d864eab9f30ef327039da6607962a156a05c384a4a58ddd8f51a0d4fe91f64ae7b0a5199110a66f1e676392ec8d31b20a65f7a7fcff90b37a8a3962bff0c83ee6033a70c5b0af663ca48a8f22ced255839444fc51f5b6a6c1237eda5804289aa25fc93f14d0d4a63cecfa30d213eb3b2497af4a22396cc8c0e7c8b8bb57be8878bfc7fb29c038d39cf9fe0c964ebac13354a580527b1dbaced58500a292eb5f7cdafc772860f8d5c324a7079de9e0c1382228effaf2ac0278ebedad1117c5edacf08105a3f0905bca6e59cdf9fd074e1fbb53628a3d9bf3b7be28b33747438a12ae4fed62d035aa49965912839e41d35206a87fff7f79c686584cc23f38277db146dc4bebd0e612edf8b031021e88d1134188cde11bb6ea30883e6a0b0cc38ababe1eb55bf06f26955f25c25c93f40c77f27423131a7769719b09225723dd5283192f74c8a050829fc6fdec46e708111c2bcb1f562a00e831c804fad7a1f74a9be75a7e1720a552f8bd135b6d2b8e8e2a7712b562c33ec9e1030224c0cfc7a6f3b5dc2e6bd02a98d25c73b3a168daa768b70b8aef5bd12f362a89725571c4a82a06d55b22e071a30e15b006a8ea03012d2bb9a7c6a90b7fbd012efbb1c4fa4f35b2a2a2e4f0d54c4e125084208e096cdee54b2763c0f6fbd1f4f341d8829a2d551bfb889e30ca3af81b2fbecc10d0f106845b73475ec5033baab1bad777c23fa55704ba14e01d228597339f3ba6b6caaaa53a8c701b513c4272ff617494277baf9cdea37870ce0d3c03203f93a4ce87b63c577a9d41a7ccbf1c9d0bcdecd8b72a71e9b911b014e172ff24bc63ba064f6fde212df25c40e88257c92f8bc35c4139f058748b00fa511755d9ae5a7b2b2bdf7cdca13b3171ca85a0a1f75c3cae1983c7da7c748076a1c0d2669e7b2e6b71913677af2bc1a21f1c7c436509514320e248015798a050b2cbb1b076cd5eb72cc336d2aad290f959dc6636a050b0811933b01ea25ec006688da1b7e8b4bb963fbe8bc06b5f716a96b15e22be7f8b99b8feba54ac74f080b799ea3a7599daa723067bf837ca32d8921b7584b17d708971fb21cbb8a2808c7da811cff4967363fe7748f0f8378b7c14dd7a5bd10055c78ccb8b8e8b88206317f35dcad0cb2951e5eb7697d484c63764483f7bbc0ad3ca41630fc76a44006e310249d8a73d7f9ca7ce648b5602b331afb584a3e1db1ec9f2a1fc1d030650557c7dbc62008235911677709dea7b60c8d400c9da16b4b0a988b25e5cf26c00c3ef02812def049bb149ea635280e5b339db1035b7275e154b587cc50464a4c0bfd15c79f54faa10fbe571b73cf1aa4a20746b11c80c8c95899521fe5f0bb3104b0a050c55a79511e202fee30c005339694b18f4e18ab5e36ea21952a01864a0e067d9f19362e009a21c6c1a798f7c1325edd95e98fd1f9cb544909fdf9d076070d1233e183fb6d46a46fbc6e10452ef4c45fa0b88a84962ad6e91cbcc52bc000b12a82e93ae5998b20ee9000a8ef68ec8a44862cc108869fd388142692be6b0657e3fe79eff0e8b72f63aeec5874acf5fb0bfc9fa22645ed6ecaaf186eca690ecdf8a71b8f4789ac41b1f4f7539e04c53dd05e67488ea5849bf069d4eefc040273f6018819fdcbaa170c2ce078062b7bbe951d2214b077c4c836db85e1b138059c382ab408a65a3b94132136945cc4a3974c0f96d88eaa1b07cce02dce04ea0126e6210a9543129bb8296839949f6c3867243d4b0e1ff32be58c188ba905d40e32c53f7871920967210de94f71709f73e826036b4e3fa3e42c23f2912f4ea50557dff78aeb34cb35444965614812cbe14068a62be075fce6bf3310b9e8b12e0dd8379104360f728d47a327c172257134e2c0e7c32e01321f4d636f9047bd750e7993eeda7d39fc16f29696b1becee4d8026e967f8149935b947fce8517b2ce02b7831a232f3a29010129c49494ed2b84c7f881b7e4b02a00ebabf5a36023c404002d6cb88cee76c8ce97b03143ca867359d7e118d54e053b02c94998e6fd8409f8d46fc1741a2e56aebb1e7dab7ca3296a2566263d9be2f4bbef4872a49ee1082cbaf86e21b0c232c4182fc660f0c0b6aaeb0393750e553bc406e2a27842bd033da45a562ed1998ef9bd83e35ed813bef00a3e6147cb363bee63c543ba5e770b043dbacc155214a2496f91879bbc9170a2a513d7b48fad40c8c2d96f951e3a0932f6d12956789198430b352803852aa9726163fbe839979b33f8dbf7f76cd50755c1ce0c40a072aeec35057d06abaf59e878000b1d796e51908bfbf23b13900dcb30f9bd52b52994e7245a7017653a404a70d1c444b8c613ff10a2b057c02d062c5faf13cdc4445809fb6e096923cdbbdca18f59318ff86c7e449f596050b404d3cde0338dfdf9b1389178b1d4c70eefa2bbd76fefc1ee1f1688ef507821e40ae31d8d8e673d183b54563e2cbd27e0e042f61b046877d37a68c1b5784830690f2dd4ebbbd2dbdb35800b9e0ba8ea985fa106dd2ce8493e845586716c538ee9008b88a7c482f3c00c14c08468230d40cdc040e145282c4d61985cb5800306e305146204f63e96ad194bcdf1338ab8480341b6fbccf18fc32145f84bece4069c09e41096e94c24fa4f0db988e860a3bff3604143f2b17e8c219f28189e4cd49a0e506fe62dc419299bcd78c6ccb107f63eb31b4bd8ea1e2fed10e3ac17341d3505019e2376b01f7a7fcea3db110fb090c681c866ac86f13e6f8d44a32861e0580def063736b5c771b2b3b9067045867b4393f3eb2a4610bd0216e29906aaac370986451c6bf78264dda7e7a5fcbcf7bd6e024ff6003c6db780d89b97765cee8d0ff3ff25d94d4b4b919f722b26a6903a017daa62af387843087680c57952de06064de05b662af87be49b6e34cf0991cec7be3396e2eec9678ba259bd8de1c192014d02928f9113488215658df4078ed661fa4e79e58decaeb0ee5a00488b094b0b77f083b2b7844f481e7788ffe8004b96ccdf853532bfd9632a8a652c2d97d10173c90864fbb6facf47fae415df4acc0b099140a657b35d083d74dbdfbf107303e74c64471bed4b2199f2babcb4e1fc593d6f309e21f85e68ffd9904731559d0f2b673b36d3984e5d66d897dfa17d601edef3ed78cb70dc5115d4ae240c203e031263f0cf1e98075bac0361fde24cbcb852b8055d53ae01d61a0a1e1ba423d00833747e7364df7ebfd1f84598d801c249e1805279dc37d39fc7f7e27b067e4e0287aec432ed49e4d701a0ff377e88179968430d110cb20476ed4c6bf1624d1907ef24406d3295fcacde2a102cc85f4f3d0cb87a8fae7535a06e442833e58cfc04242ff85fb654d05f9874c0a6756f542db4e9d8b0366191fbb8b09a1bbcb6af04c069978417ca80d92f442b7dbd092f74e1268aa73b54e4b64e84543449ecd30b5ea392a1669a5f441d7208925e91c75df611cd26042630c6b98f160b8c0156048108d5465b71bbc54d31a9f90e34428d97590a427e1ae618d4a35fc1022d4e007c6108dcb1672b88d43ae4d886a5adcc26faf56bc5e5a0b08342fb88263fd80940d1edf794c6ad6d339b974e164b38439e11b4fa87cc793b080b4f8bf0eb56043f79ed3911da21092475fcf8320b55b9f558f194c6c8121b2e696039340d97057be2583726d762b5ae4327e5286a2d8c14ddbe0027c75aacbf7e9de13037390df7d72e13b46bc06bad0363b070e0174d034120d7fa7b4550e7dc28f7f0241f059ae266fc13dccd1d07f744208a7d6a2e565b6613d46e4550f79ef3209c46a805b97284df558719e131f44e419e690f4fc28ee4862b9d1f8f7e1a164ac18141076087693e70ac76a10f7851530d4cbc65def90d5544671ad64249569c3abf0200d09be3c63efaa7cb723b39ccffc9b3a3ba0c8426847123d2a881efbd4937a40cb3e8011c70ba427f80b3dc91a608086f8b61f8bd86fcb482ea388299a7cfbd00a3ddfadb4b6d0e51c1369276c25889a9f3592900b6502d9af1c732e1fb7db307d71e45deb1553ba1568d0480ea9e132b52564da6ac5c56eff823e7f37976cd075ce8f7a78aaef1b87f7437a0b84035677f266f7d0596e493101fec3e14fcf80b22322454587b51fda0636231c07d4e63e007f1b89137d8a37b03bf00f3a7c10169f757d9a74b7bffba797c746e3845decc3d0559d7cf6f08f3bd67dac5f33109b212582dc7df5d561ad63dddc794f2aea4e493db1a73c702d258c9b922c35d04c47f88f87c54c821a29f04abd91a079ce8cef252a21dc72d409fd1618c9be709af029ba98b0140e74666fcb01bced4f88ab68e6b63b8ed6febc0905d22cb2200493c071ce136833697406f8a04e77b21747dda997046cf3c7080096fe481790d77cf5904e7f7128ed95a6e576d109fdf10eb0c888db35a4a685b62253987b70fb1538e6c0932889460fa31c60d123266b7bcb828f846a35b2851127679b05f05a75266529c343a6075e54c455e02b2c83e6f7bf1ae23326506a5f532472d780815c5af425f7d8b543a8f014966e0538f48ca84d181695381a09701eb65c9ae084bf2a4dc84f1b2071be32be25d5f4fcdc59668fd800496ef7eb6dddf867ab908e543cb51f0451706cce4be6b9f68a537a79ea88e17fcd78965b3da68c0d9d30623a2a9e275e1c320f59e118e09c02eee527167bc06f7693e7b584e3b25ecc1093d46b80a1cacced87c2b32e2f90c5bbb9cd1b701aae69a04b16d535fac6eab0d091790fc5fdfa8a8842bfcb62dbf963cbf62c4afb4c468be98c770e6078b8c0a8cfcbae43dcfff17d3c6d587c3e4309fd39c66acd14781fea66fc57278b02302c0fa386280e67acff19955b6a428b0e22ceb1e54e913a37cd19eb6e9d2268a039f2b5fdda7d5804db79385f0e50082b128c952f8dfdedc4411d0675d95127f0bfc01710a869b10d7a8b9e632dad944062567439e6d192fb09329d058e87ecd0aa8981328f541e87ed02cfe4031f2d3a046ff517a2a80486b04ade31a647aec0884fb96ed753ffc47892431c6e6f08fd1c633a1a44e882d3d8b92c567e0fb8305327a354851464ca0f18d89c6ee2a91a4afef0c55883acf8fcb68c2c3b7402e005d8affc19c13f1f26fee0698dff181ab22cb84a2b31e0a6a81dc5d02e60a3c07090397ae58a985526b2ad6ee5725e82328062b68566b4871705ce3b9856e550d068c20fd9aaeb27740c07aad53d79fc20e46e40e7103e2d69626ee64b6aa600f6f1a86f37948ff4990d88f43c34994e2fe586cb779997be323da53329c10480aeb08fe440e9e4b979171371c73b94da9f928a3f6c8f6792f782f3d6432b86d06f54557327fef31fd6ae0a3f6d2f16c9ad947d132e14def33fa24cb4565370e0832fa50f5f5f93c9f3d65776cc22608b68a4f3719e9be47a19432991e4a2c49089c0ea20e7f7c73feaa47970da424d8543d80d622e2f2be9f4c65cc39dc369009a9d41a52bdea7cc0e8e04da87a633fd4f814fda1b646121a469ba0b5b8006d0e9118761d97b5d1856e2d690f27a81c42b176df853d07cf4a66ee83c9eb24ac0a382f5143a10a33ec3ddf17dcd8a8303fac8f279d31b4d04d74bd8804cefbb400c86174ad444e43ed33ee1e1e73f660b9814d5ca3cb1d650f1978a825a617bb05f84eab3b9b8359b991e1084cf4e8179ecb67f92398638e31227ff63427b67f0f232b454a341d85d4b56e31135c9035e231f7d9318ca12b5ab524f87bb0ca9b04b80effed202897ab016d5acc054c4fe62a5f0192f136cf2cd714998a4b164b0c2cdbace52243fdc9ea879b0d247d4fe8bd80481fad6b325cb5f2cfa2534dec0e47d41b6b99352e6e5faccb5ee28ca2fe96e04f9c83a0461ba34cfb499d864f05dc734b6c8f51cc0c994290b2a868cb8312df39fb6d457a81e62d872c65d4f3007094be42663bca3d64ebbcc8401158fce4f5a38a49c20f029c338f126451820459866e77c6984a467aad571cc7452392a9cb9f8e65099fff2f2acd170a833e01ed3d22a683356ee42dcbe6bab6d05d3edda2d40e9ba53884d430c2e0cd87c0067dc8cb68c868bd9f29db1dd73703c139ffc15e4f7264e727c70560ae02da100871f30e8a7c1275805f621752d73aafecddc2a7808b6c2ecbb8d0134a644bb603f30f8d18b3fc5efaa7f206ce180bfb14f5dbd3b0115145a227113eeaf1c1ec04244227b931388e72960833a40baa4319c5cf74aa94f7e3233e1d09f0a4f74409999684ad1cc836ac74563c85b164664dfab08ea084b25e2cbd7e7b94a781a10fcd455ee38b65126dcc52016127fd195c80b05660ed931e128b0cb92868955c0d032ada9fb951210a5442d21c1718ebc4702ad4a57967e15a63ffb05e1e072a0c41ebdf1e7205373eeaf4587f695de887fa3a8c96b8deb99e040fa1fc4dc2a402a017891943d734ae2f3798b22b1269d3d9f6d65581b9c637a6896a4fb554810bbd3db5c5737391a74150b43413b2e3824490b7911cbeb845147f1a8521620b0dd31306f13a9754a01bcdbd18bfdeade06b0ec97f48df56c45d3670a1fe18d00ef13e613c8a77aeb40401a814b377137cf44f29cb2cb94186ad1161ecb05a7c07837a5ab3474e57990cff2ab16b4d99f62e646da28e8bb712a5b561cf0e25be039c3e08583c8ebc3dd2fdb8fdc6e135ecc7851c73218a70b75e697cc84ea50504b9c34a33ed52f87230b9d192a940f3b7bb6d45b58dbf52f0afeb8dac85c77b06bdf9b70a10cb81c50055c9d8cf7e3a5c4b7dfae55beabcb3e8a8a1cb822d8d0bf6c01e32056929f853021eae6c97fdb0c5031df6b2e7c57f1318866769a9cc09c38ed62d8bf4663334c0df67c47236ed73f6ce7f54e0ada9270398c1aa558d0f993b0d25d97aea77b1635ee4832362cd590bae5fc1549402ddcd42b15efc930111a01535c0242116078d6d2d53b8612d378c4370e90d0d01b01bd7da591bec07981652a98485d8ed5c8f3def2bdac7d992ee5fc6a1ec7bd36940e1bc58c7050451248fc3ee6069e6b1b0d3ef122c6ef2a9b99aa0f145fb43341c58dbb472130b51730c956273a3ef6df9e000f6a87c2bacdefcdb5daef28b6170f61bc3a9c101f439755c86e6b85ee06a7a60688b3843eb359cd4acd9221a2ee131e2fd2e190652e5c47c0b98c41010eb99a991ec48a5de99cc8f403d6d76f8307d6657c1e007ebd64eec7bbd0d4f1ba2db7bb0efe27c7828f053e00def775943ab01a7e33d0fffcfe6f9a7285237f2c381b638758e373f8ceac672190664bb25fb5d355c240bd1773d61bda7f7ef1f4261b80ff5058ec6f7e024ab9459b1103815624b81f80c39db2f6fecb72de452b11636b0f71b16cb55f883d93bebb94328f13ef1ab6d0df449e32d27884f5139af584035547dace65ee25ba05cc461e74760d4468af90dcaa982e52cb902e2b84b3324019da575601ca54e91655913892e703257deaa01d14fd8459ff780c724161ba4d4280b70a5039dcfb5d775560714009724cb0d0b7e178c71e777b896bcfcde7d4c9c3dc6ab819d74a1a1fda8486448b1ad79be02fb134ea93a8600f1bc2a42e68d0213ab461a07cef3ad3965bc130beb76bab409102f82bf6c4cd626f6df3388e17b87584310c50832cde3191f6557f0014bdc0a68d924119e43111043bc6f26d16a5f2612dae6ab24984e2d87a71d93d5f4670dba2176d4f16633407bf7c10b51b6842dfdbc6fe3eaa4b6a12f0550700ece070ca382dec3b587e0e1fc317a48a83754d15aaf9a6971b8cb641fd8b32846d89002e6301700a0e7056e8002d8f269d29ebaf64f4493b1f1e676fc78e673067fb00625df15dc0490235b386ee14e55b335f3bc6dcedd7d3a80fd3a6e9bc2ccf3af0d89be71b5ca92bd7a9b97b9ff8976f75702419aa5bf9be34600496ca1bfa8ad0400602a23579365574252434f2bcd7efb360b0e8a495e8f7e78923b6fbf2207049e9179f0d4d7d6b4a4a10ca10f0ef4dd6cb5a74f7574e832044d6120fbc1580a68eddfbc65ab300bed960a6f24a102dc36b72937a8be4385daf5946e81ccde0619251babbff17e5685217a134d22f6130d0322483b3475227ffd27adc73ca202a6debfa37e5731747f4449ac70a33684f460eede65918c6d89acf4b50fd28d040ffbd436a944d3be0210606bfc2301e7ac66d462dba29a0489eb55af714a760e5302592cccc726e535b945ceb6126eb84e31f0f140ff54df8be0fa3a22f418036ad996787a5616a97a42049ebce351dc11857cab3dc914ef26833b0e75653004a8cafea099fb0750135255c41ef43e2f29c75714e2f0be2545e7c109b70c43004a471daa85b47befc65907d033f133b2f3ac2ad568df630ee80506610b8dc9052d442668dc06b13ea76ab1ab7b34870341d660af5d3007c21bb72512e4f8a60d8916a037b93f9e15ac9e4a6a1246d73ebb40e5fdd5a0d6dc0cf175023b891301f69fe5a3ca6f12cb8312d16333de1cd3ebb99339ab18c0715bfcd35b8365b407ad759e2c591d8270ad335381573e27ec18af7ca157b4a2bbca921db083d9b0009dc332a79dde14354a8c18bce76a1bfc1a25a1e702ccaa0feb521ee9279b8a01ceab6e237bbe4128b23cb53b1e5185f3266e20670a307ea0cfb5377025e0bb0790d48f1636c8b836c1a1f69ad61265f19057197e86cd526da6ddb94fd1ece80b60852f27ef2ce56ccb5a32d8cab6d16be06f380dfde3602ea4c1ae927173b2001ff0d9e29bc66b2b2a20c3e3ac174fcba187aacab0876c1356d30d4021e6dd0048c3bdfbf254108bb09d3ca9f2be423a92408bca52fbcd68f972c46fc8d20e0350d12c2f2d6c7da85e96bcec3ce61119793d44a210f81ece859fef6360ae3b0e1af0634fc141a8b50b3b383fb264e8a4fb84ea06db6becbf5e140edf66ee190da8968da579eb349fedea45e4c252a79570501278bab5fa984d7b1179d7c2460faa7beafee153bbae0a591701632aa94839528d3ef50cf809c1f7209b9e5c99010eaff7f921c45b6546358ee7a90948e3c710cd3e1796860839a345516fdf4f07c415029627abe1273a1f510c36a662562d18169b23305b4efadfefbfbb41a400ab533e61c14cafa49bc5d2818058ee4f3e1aeb329e150820d1de1f1eaaad31051a6dfdd3a1d5cec7b16bc0ea2c649d409917faa42138b1f824b4d534a050be0a99ea6772daf0b2e58623cc7a250ef37599bd556508f08886e663ef0917ecd3077072c3268ea5b9b89cbb6b761ee9f9c4765d8b267d9eb19728a28ce67a42ed0cad142b5dc0fc5313853860ec3f0ee2bc3d47cbe12dbe9633db809967d5b8bf0e45574eac657059530c30aeeade1e4f858a4a6e79d6e441b4af0127a13340d908d48cfec849ee93d53b1564231f048d34885e791a9d40c61a7b00f12f6f72a5050bcaabcc98480170ea6e20bef6b5c6f504c808108454fe2f3c275bf8f89a5e0a3304a7c4787e6d4fbe569930f7cfd38ab7d1d2ebd599bbb411950cec3e53b90cefb82234990d353c71ce4c21ef674a1c4070f71c90e1ea7edf35f5a421118f01b49a92ea97720e2d4df6b5885c181002656629a90eaa1904fe1c379b8291480ca15d0dc2b65a20c22f1e01d612d21ecb5738e5ebfc578a4a65066ee6e913e3030d3fdfb0168fd75022492728ee82869deb9ff2827f4e10759ecddb20f67e9808e707257a74d3dc0a6068f264066f95c9f772a3dcec0b4f0a327e3745517ad60ccbc5392890d2479b724d068fcdb83607e02291c06e1a5a1dac7604889cce2500f418da2f7080a7e9a1bdf28b87028a2bbb0c14f059f10f46d46716eac2cdfc06676cbec91b8c2c0f7c9bea7e27fa5048662398b23a9b488a49e1d3330c04e60179a4492c8b836780899899d2af17e6119a94d54a890ce8c0b550e87fd54cba0821fa7c48f6e09a60dcbddf853f82b47195aa44a5ceae14a9257296acd711c8073ff3345befca5d3ebf64901b283df96395fe9785d7090176bfe5a9f13ceae701c6c93af0e13d949bc3c7e9b06674a73e7affc508302258a27fb34569c3742201c0721aef282a31c69a5d98a67ac5c3d920d50c089896f7f8c8c237a81f803f0444f417246d695e89a3a523b62a3cd2203d42607cac7c7782dec1f9edbb806c0a7a37d1a969082a126bc726151a50233456a07d374399e74aaa8cc66821511d092615950d302e815cfcc021e1250cdea20fd9e1e4b5e88280d6e4283b918e780d12cbba59ef2ed2ce86135a48fba6c0dc2bf2efee190d9a3f9aa22a622b1953058f2bb3a371637d13e045d54e7eb54c0d25851f49283d7d34e9785d2d5c3f70086c48a8325a2083bdf5b3531fcc697cc0c9f63892a866c84585d673a2a63fd60e77995bfb0c0a44a4b63c0ff67e813027d3e84cddd393a0f4e6bc95525c5ae20eed9d0cea4a12aa748eb5209cfd75990b055f1ad0472f9f7599f569a8743a720755aa11555df4bb2e725fa93bc5dea603a964e8dc9fb1742e81825022866fc50a6b2a19b6a234a38ee27a74f2f5832b294143ca7ff8d07fd7d4e01f479e9792058871d90ee3aaa3329e82cebe41dff5e6d00a36268a7965466b80c6510ac1350cee797e1d6737f6aaff155266d2a2d611b2124affed1ac73a6a06515627b2230ce0d7fed33ecbde511f4d472cbc556cc8d9c5640e67657035112976b626847a0a4ea5fdd14d4a3eed57f0dfbe153393d8bd28c8b4f9e62940e8379790393fa20c617050c780a7d870193b4611bd7a12d26947a3cf4605e225da8b1646a76984015a5e317016a4d8301eaeec0db3ae0daa719182e2f4479154dbcccfcce1f365099de6c91934c395ce82abba8062a51d7773b418330921766cd3d275c689098e06039698db6f09accb292e7eb79e7a022d4257bb2f9ed993c519860919bc229a06ad88954c9ebf7f5b9fe95cf56e8181cb9175dac06be0be70fd28df20cdb4600ef0869668c645c9ea01360fdae7c922cb3d2b3583ae1de5ae7d899a83ff2bb00d7365c782a0fccfcba7f87bb29416469bb051f9b0755123e0f2fa76dc7644b70e452f49a84bc372b384c843b8161b7f9b63699adcadd5cb2b33b36c7eb3e1b00f25218bc16447968b939016242fceaebd796c17a24d1b9870991a9c3ae90e380302b7bb320adacc08cfb9249d29cd9275c52476dac6a7e9870ee3776cbc3352036f9c8f681d44856c6c5f90b7cde0877472ddd48719c449f59dca1f49442f7505e4809c6d323b37530ecccf3e41e19822f53d64dc90efb113405ee88799c37f0a342293b5bfc019a9057138326de6107b5613554dffc737aed7237fb16cd77e09f581d12220ac930c6ca279efd1d07a92125fb2606ec3ec35351987a15fc72806cfb3cb66fce8dcfabee5c1e586bf0f802fa12ae5ad5a708e3a5d54e1926dbd0202bf1150f1bb612b9a4590b5b520b86a90860ec3d9c2184f9975ced15ae1300882d9918021b43a1184ba88ddd7091539fe5a7017b8708d0f5c916f9c42de5103f8116863864b508f5880ca60b7492385c16a02b6ceb64d257a4838873b85d2041517c5c7c4508e4d5a5faa72729d73af0361e11828eeca992b8f20d903a5ef065976a9f322e34bd4b3984bb09e18be40e77e833c8c1a2e80093227d3f40d4a067f5e3aee9fce9bd234bb6ff4d0c34fc060d23e86b1f5a6d8d052e53e913182052a2d9c5e97bb0e0a51bb2fafbe7346bacfcbadb00ce2ba129f29d41a11f7d105cf19bb60b5f5b0dfd6a894698ef7f56a02d69cc03eb62a56563d3a77e3ac2302", + "as_json": "", + "block_height": 993442, + "block_timestamp": 1457749396, + "confirmations": 2201720, + "double_spend_seen": false, + "in_pool": false, + "output_indices": [198769,418598,176616,50345,509], + "prunable_as_hex": "", + "prunable_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "pruned_as_hex": "", + "tx_hash": "d6e48158472848e6687173a91ae6eebfa3e1d778e65252ee99d7515d63090408" + }], + "txs_as_hex": ["0100940102ffc7afa02501b3056ebee1b651a8da723462b4891d471b990ddc226049a0866d3029b8e2f75b70120280a0b6cef785020190dd0a200bd02b70ee707441a8863c5279b4e4d9f376dc97a140b1e5bc7d72bc5080690280c0caf384a30201d0b12b751e8f6e2e31316110fa6631bf2eb02e88ac8d778ec70d42b24ef54843fd75d90280d0dbc3f40201c498358287895f16b62a000a3f2fd8fb2e70d8e376858fb9ba7d9937d3a076e36311bb0280f092cbdd0801e5a230c6250d5835877b735c71d41587082309bf593d06a78def1b4ec57355a37838b5028080bfb59dd20d01c36c6dd3a9826658642ba4d1d586366f2782c0768c7e9fb93f32e8fdfab18c0228ed0280d0b8e1981a01bfb0158a530682f78754ab5b1b81b15891b2c7a22d4d7a929a5b51c066ffd73ac360230280f092cbdd0801f9a330a1217984cc5d31bf0e76ed4f8e3d4115f470824bc214fa84929fcede137173a60280e0bcefa75701f3910e3a8b3c031e15573f7a69db9f8dda3b3f960253099d8f844169212f2de772f6ff0280d0b8e1981a01adc1157920f2c72d6140afd4b858da3f41d07fc1655f2ebe593d32f96d5335d11711ee0280d0dbc3f40201ca8635a1373fa829f58e8f46d72c8e52aa1ce53fa1d798284ed08b44849e2e9ad79b620280a094a58d1d01faf729e5ab208fa809dd2efc6f0b74d3e7eff2a66c689a3b5c31c33c8a14e2359ac484028080e983b1de1601eced0182c8d37d77ce439824ddb3c8ff7bd60642181e183c409545c9d6f9c36683908f028080d194b57401ead50b3eefebb5303e14a5087de37ad1799a4592cf0e897eafb46d9b57257b5732949e0280a094a58d1d01d3882a1e949b2d1b6fc1fd5e44df95bae9068b090677d76b6c307188da44dd4e343cef028090cad2c60e0196c73a74a60fc4ce3a7b14d1abdf7a0c70a9efb490a9de6ec6208a846f8282d878132b028080bb8b939b4401c03dbcbfd9fb02e181d99c0093e53aceecf42bf6ccc0ec611a5093fe6f2b2738a07f0280f092cbdd0801b98d30c27f297ae4cb89fb7bb29ed11adff17db9b71d39edf736172892784897488c5d0280d0dbc3f40201da9a353d39555c27a2d620bf69136e4c665aaa19557d6fc0255cbb67ec69bf2403b63e0280b09dc2df0101f8820caab7a5e736f5445b5624837de46e9ef906cb538f7c860f688a7f7d155e19e0ac0280808d93f5d77101b544e62708eb27ff140b58c521e4a90acab5eca36f1ce9516a6318306f7d48beddbc0280a0b6cef7850201abdd0a5453712326722427f66b865e67f8cdb7188001aaacb70f1a018403d3289fcb130280c0caf384a30201a2b32b2cfb06b7022668b2cd5b8263a162511c03154b259ce91c6c97270e4c19efe4710280c0caf384a302018db32bda81bfbe5f9cdf94b20047d12a7fb5f097a83099fafdfedc03397826fb4d18d50280c0fc82aa0201b2e60b825e8c0360b4b44f4fe0a30f4d2f18c80d5bbb7bfc5ddf671f27b6867461c51d028080e983b1de1601b2eb0156dd7ab6dcb0970d4a5dbcb4e04281c1db350198e31893cec9b9d77863fedaf60280e08d84ddcb0101e0960fe3cafedb154111449c5112fc1d9e065222ed0243de3207c3e6f974941a66f177028080df9ad7949f01019815c8c5032f2c28e7e6c9f9c70f6fccdece659d8df53e54ad99a0f7fa5d831cf762028090dfc04a01b4fb123d97504b9d832f7041c4d1db1cda3b7a6d307194aff104ec6b711cced2b005e2028080dd9da41701bef1179c9a459e75a0c4cf4aff1a81f31f477bd682e28a155231da1a1aa7a25ef219910280d88ee16f01facd0f043485225a1e708aa89d71f951bc092724b53942a67a35b2315bfeac4e8af0eb0280d0dbc3f40201c4e634d6e1f3a1b232ef130d4a5417379c4fcc9d078f71899f0617cec8b1e72a1844b60280f092cbdd0801f6b9300c8c94a337fefc1c19f12dee0f2551a09ee0aaa954d1762c93fec8dadae2146c0280c0f9decfae0101ce8d09f26c90144257b5462791487fd1b017eb283268b1c86c859c4194bf1a987c62bf0280c0caf384a30201cead2bbb01653d0d7ff8a42958040814c3cbf228ebb772e03956b367bace3b684b9b7f0280a0e5b9c2910101c1b40c1796904ac003f7a6dd72b4845625e99ba12bdd003e65b2dd2760a4e460821178028080e983b1de160186e9013f55160cd9166756ea8e2c9af065dcdfb16a684e9376c909d18b65fd5306f9690280a0e5b9c2910101aeb70c4433f95ff4cdc4aa54a1ede9ae725cec06350db5d3056815486e761e381ae4d00280c0a8ca9a3a01ebe2139bd558b63ebb9f4d12aca270159ccf565e9cffaadd717ce200db779f202b106f0280d0dbc3f40201b9963568acf599958be4e72f71c3446332a39c815876c185198fa2dcf13877eba3627b0280c0f4c198af0b01bccc01408cbb5a210ad152bd6138639673a6161efd2f85be310b477ae14891870985f90280a0b6cef7850201cadd0a82e7950f5d9e62d14d0f7c6af84002ea9822cdeefabbb866b7a5776c6436636b028080d287e2bc2d01d888013a7146b96a7abc5ce5249b7981fb54250eef751964ff00530915084479b5d6ba028080d287e2bc2d018c8901d8cc1933366dceb49416b2f48fd2ce297cfd8da8baadc7f63856c46130368fca0280a0b787e90501b6d242f93a6570ad920332a354b14ad93b37c0f3815bb5fa2dcc7ca5e334256bd165320280a0e5b9c2910101a3ac0c4ed5ebf0c11285c351ddfd0bb52bd225ee0f8a319025dc416a5e2ba8e84186680280c0f9decfae0101f18709daddc52dccb6e1527ac83da15e19c2272206ee0b2ac37ac478b4dd3e6bcac5dc0280f8cce2840201b7c80bc872a1e1323d61342a2a7ac480b4b061163811497e08698057a8774493a1abe50280e0bcefa75701f38b0e532d34791916b1f56c3f008b2231de5cc62bd1ec898c62d19fb1ec716d467ae20280c0fc82aa0201d6da0b7de4dc001430473852620e13e5931960c55ab6ebeff574fbddea995cbc9d7c010280c0f4c198af0b01edca017ec6af4ece2622edaf9914cfb1cc6663639285256912d7d9d70e905120a6961c028090cad2c60e01cad43abcc63a192d53fe8269ecaf2d9ca3171c2172d85956fe44fcc1ac01efe4c610dd0280809aa6eaafe30101a92fccd2bcadfa42dcbd28d483d32abde14b377b72c4e6ef31a1f1e0ff6c2c9f452f0280a0b787e9050197d9420ce413f5321a785cd5bea4952e6c32acd0b733240a2ea2835747bb916a032da7028080d287e2bc2d01c48a01a3c4afcbbdac70524b27585c68ed1f8ea3858c1c392aeb7ada3432f3eed7cab10280f092cbdd0801abb130d362484a00ea63b2a250a8ae7cf8b904522638838a460653a3c37db1b76ff3de0280e08d84ddcb0101cc980fce5c2d8b34b3039e612adeb707f9ab397c75f76c1f0da8af92c64cd021976ff3028080dd9da4170198c217e4a7b63fd645bd27aa5afc6fae7db1e66896cece0d4b84ac76428268b5c213c30280a0b6cef7850201e3e20acab086a758c56372cf461e5339c09745ee510a785b540d68c7f62c35b8b6c5c10280a094a58d1d01ab842ac0ba57e87a3f204a56cbecca405419e4c896c817c5ced715d903a104a09735660280e0a596bb1101ade921f1ef2fe56c74320ceb1f6c52506d0b835808474b928ad6309398b42434d27f3d0280d0b8e1981a01dab515d684a324cff4b00753a9ef0868f308b8121cbc79077ede84d70cf6015ec989be0280c0ee8ed20b01f99c227da8d616ce3669517282902cdf1ef75e75a427385270d1a94197b93cf6620c15028080dd9da41701d2d0175c8494f151e585f01e80c303c84eea460b69874b773ba01d20f28a05916111a0028090dfc04a01a4f312c12a9f52a99f69e43979354446fd4e2ba5e2d5fb8aaa17cd25cdf591543149da0280d0dbc3f40201969c3510fbca0efa6d5b0c45dca32a5a91b10608594a58e5154d6a453493d4a0f10cf70280f8cce2840201ddcb0b76ca6a2df4544ea2d9634223becf72b6d6a176eae609d8a496ee7c0a45bec8240280e0bcefa7570180920e95d8d04f9f7a4b678497ded16da4caca0934fc019f582d8e1db1239920914d35028090cad2c60e01c2d43a30bbb2dcbb2b6c361dc49649a6cf733b29df5f6e7504b03a55ee707ed3db2c4e028080d287e2bc2d01c38801941b46cb00712de68cebc99945dc7b472b352c9a2e582a9217ea6d0b8c3f07590280e0a596bb1101b6eb219463e6e8aa6395eefc4e7d2d7d6484b5f684e7018fe56d3d6ddca82f4b89c5840280d0dbc3f40201db9535db1fb02f4a45c21eae26f5c40c01ab1bca304deac2fb08d2b3d9ac4f65fd10c60280a094a58d1d01948c2a413da2503eb92880f02f764c2133ed6f2951ae86e8c8c17d1e9e024ca4dc72320280c0ee8ed20b01869f22d3e106082527d6f0b052106a4850801fcd59d0b6ce61b237c2321111ed8bdf47028080d194b57401acd20b9c0e61b23698c4b4d47965a597284d409f71d7f16f4997bc04ba042d3cbe044d028090cad2c60e0194b83ac3b448f0bd45f069df6a80e49778c289edeb93b9f213039e53a828e685c270f90280a094a58d1d01bdfb2984b37167dce720d3972eaa50ba42ae1c73ce8e8bc15b5b420e55c9ae96e5ca8c028090dfc04a01abf3120595fbef2082079af5448c6d0d6491aa758576881c1839f4934fa5f6276b33810280e0a596bb1101f9ea2170a571f721540ec01ae22501138fa808045bb8d86b22b1be686b258b2cc999c5028088aca3cf02019cb60d1ffda55346c6612364a9f426a8b9942d9269bef1360f20b8f3ccf57e9996b5f70280e8eda1ba0101aff90c87588ff1bb510a30907357afbf6c3292892c2d9ff41e363889af32e70891cb9b028080d49ca7981201d65ee875df2a98544318a5f4e9aa70a799374b40cff820c132a388736b86ff6c7b7d0280c0caf384a30201dab52bbf532aa44298858b0a313d0f29953ea90efd3ac3421c674dbda79530e4a6b0060280f092cbdd0801c3ab30b0fc9f93dddc6c3e4d976e9c5e4cfee5bfd58415c96a3e7ec05a3172c29f223f0280a094a58d1d01a2812a3e0ec75af0330302c35c582d9a14c8e5f00a0bf84da22eec672c4926ca6fccb10280a094a58d1d01ca842a2b03a22e56f164bae94e43d1c353217c1a1048375731c0c47bb63216e1ef6c480280e08d84ddcb0101d68b0fb2d29505b3f25a8e36f17a2fde13bce41752ecec8c2042a7e1a7d65a0fd35cdf028090cad2c60e0199ce3afa0b62692f1b87324fd4904bf9ffd45ed169d1f5096634a3ba8602919681e5660280c0f9decfae010193ed081977c266b88f1c3cb7027c0216adb506f0e929ce650cd178b81645458c3af4c6028090cad2c60e01eec13a9cce0e6750904e5649907b0bdc6c6963f62fef41ef3932765418d933fc1cd97a0280c0ee8ed20b019ea8228d467474d1073d5c906acdec6ee799b71e16141930c9d66b7f181dbd7a6e924a028080bb8b939b4401c23d3cb4e840ad6766bb0fd6d2b81462f1e4828d2eae341ce3bd8d7ce38b036ac6fb028080e983b1de1601b9e601e3cf485fa441836d83d1f1be6d8611599eccc29f3af832b922e45ab1cd7f31d00280f092cbdd0801fc9e30fff408d7b0c5d88f662dddb5d06ff382baa06191102278e54a0030f7e3246e7c0280d88ee16f01bfcd0f96a24f27ac225278701c6b54df41c6aa511dd86ce682516fb1824ff104c572fb0280f092cbdd0801cbb430bd5f343e45c62efcd6e0f62e77ceb3c95ef945b0cff7002872ea350b5dfffef10280c0caf384a30201bfb22b14dccbba4582da488aef91b530395979f73fa83511e3b3bcb311221c6832b18d0280a0b6cef7850201c4dc0a31fb268316ab21229072833191b7a77b9832afea700a1b93f2e86fb31be9480f028090cad2c60e01cab63a313af96a15c06fcf1f1cf10b69eae50e2c1d005357df457768d384b7a35fa0bb0280d0dbc3f40201fe9835fffd89020879ec3ca02db3eadbb81b0c34a6d77f9d1031438d55fd9c33827db00280d0dbc3f40201d19b354a25ddf2641fc7e000f9306df1c6bf731bddfe9ab148713781bbfab4814ed87e0280e08d84ddcb0101ba960fec80e1dcda9fae2d63e14500354f191f287811f5503e269c9ec1ae56cef4cd110280a0b787e90501acde42b0bdfd00ab0518c8bd6938f0a6efab1b1762704e86c71a154f6d6378dd63ce840280e0bcefa75701b5900eedc2ce12618978351788e46fefe8b9b776510ec32add7423f649c613b9db853a028080e983b1de1601edeb01d68b226cd6b71903a812aa6a6f0799381cf6f70870810df560042cd732b26526028080f696a6b6880101ca18a91fd53d6370a95ca2e0700aabc3784e355afcafb25656c70d780de90e30be31028090cad2c60e0184c03adc307ee3753a20f8f687344aae487639ab12276b604b1f74789d47f1371cac6b0280c0fc82aa0201a2dc0b000aa40a7e3e44d0181aaa5cc64df6307cf119798797fbf82421e3b78a0aa2760280e8eda1ba0101daf20caa8a7c80b400f4dd189e4a00ef1074e26fcc186fed46f0d97814c464aa7561e20280c0f9decfae0101a18b092ee7d48a9fb88cefb22874e5a1ed7a1bf99cc06e93e55c7f75ca4bf38ad185a60280a094a58d1d01dff92904ef53b1415cdb435a1589c072a7e6bd8e69a31bf31153c3beb07ebf585aa838028080bfb59dd20d01916c9d21b580aed847f256b4f507f562858396a9d392adc92b7ed3585d78acf9b38b028080a2a9eae80101fab80b153098181e5fabf1056b4e88db7ce5ed875132e3b7d78ed3b6fc528edda921050280d88ee16f019fcf0fd5f4d68c9afe2e543c125963043024fe557e817c279dbd0602b158fe96ec4b6f0280e0bcefa75701d1910e44b59722c588c30a65b920fc72e0e58c5acc1535b4cad4fc889a89fccfa271510280d0dbc3f40201b78b358b066d485145ada1c39153caacf843fcd9c2f4681d226d210a9a9942109314d90280e0bcefa75701a88b0e5f100858379f9edbbfe92d8f3de825990af354e38edc3b0d246d8a62f01ab3220280d0dbc3f40201959135c6a904269f0bf29fbf8cef1f705dde8c7364ba4618ad9ee378b69a3d590af5680280e0a596bb1101edeb2140e07858aa36619a74b0944c52663b7d3818ab6bf9e66ee792cda1b6dd39842e0280c0a8ca9a3a01aae213a90a6708e741f688a32fb5f1b800800e64cfd341c0f82f8e1ca822336d70c78e0280c0fc82aa02018ddf0b5e03adc078c32952c9077fee655a65a933558c610f23245cd7416669da12611e0280f092cbdd0801aca0305b7157269b35d5068d64f8d43386e8463f2893695bc94f07b4a14f9f5c85e8c50280e0bcefa75701b18f0efd26a0ad840829429252c7e6db2ff0eb7980a8f4c4e400b3a68475f6831cc5f50280b09dc2df0101a6830c2b7555fd29e82d1f0cf6a00f2c671c94c3c683254853c045519d1c5d5dc314fb028080bb8b939b4401be3d76fcfea2c6216513382a75aedaba8932f339ed56f4aad33fb04565429c7f7fa50280c0ee8ed20b01b4a322218a5fd3a13ed0847e8165a28861fb3edc0e2c1603e95e042e2cbb0040e49ab50280c0caf384a30201ecb42b7c10020495d95b3c1ea45ce8709ff4a181771fc053911e5ec51d237d585f19610280f092cbdd0801eba3309533ea103add0540f9624cb24c5cecadf4b93ceb39aa2e541668a0dd23bf3f2f028090dfc04a01a6f3121520ad99387cf4dd779410542b3f5ed9991f0fadbb40f292be0057f4a1dfbf10028090cad2c60e019ac83a125492706ba043a4e3b927ab451c8dccb4b798f83312320dcf4d306bc45c3016028080a2a9eae80101b4ba0bd413da8f7f0aad9cd41d728b4fef20e31fbc61fc397a585c6755134406680b14028080d49ca798120192600ef342c8cf4c0e9ebf52986429add3de7f7757c3d5f7c951810b2fb5352aec620280a0b787e90501afe442797256544eb3515e6fa45b1785d65816dd179bd7f0372a561709f87fae7f95f10280a094a58d1d01dc882aacbd3e13a0b97c2a08b6b6deec5e9685b94409d30c774c85a373b252169d588f028090dfc04a0184f81225e7ded2e83d4f9f0ee64f60c9c8bce2dcb110fd2f3d66c17aafdff53fbf6bbe028080d287e2bc2d01d18901e2fd0eeb4fe9223b4610e05022fcc194240e8afe5472fceda8346cb5b66a0a5902808095e789c60401cf88036cf7317af6dc47cd0ce319a51aaaf936854287c07a24afad791a1431cbd2df5c0280c0f9decfae0101999909d038b9c30a2de009813e56ba2ba17964a24d5f195aaa5f7f2f5fefacd69893e80280a0e5b9c291010199b10cf336c49e2864d07ad3c7a0b9a19e0c17aaf0e72f9fcc980180272000fe5ba1260280a0b6cef7850201a2e20a7a870af412e8fff7eba50b2f8f3be318736996a347fa1222019be9971b6f9b81028090dfc04a01bae5127889a54246328815e9819a05eea4c93bdfffaa2a2cc9747c5d8e74a9a4a8bfe10280f8cce284020191da0b24ee29cd3f554bb618f336dd2841ba23168bf123ee88ebdb48bcbb033a67a02f0280f8cce2840201e6c30b2756e87b0b6ff35103c20c1ddb3b0502f712977fd7909a0b552f1c7dfc3e0c3c028080e983b1de16018fed01a3c245ee280ff115f7e92b16dc2c25831a2da6af5321ad76a1fbbcdd6afc780c0280e0bcefa7570183920ef957193122bb2624d28c0a3cbd4370a1cfff4e1c2e0c8bb22d4c4b47e7f0a5a60280f092cbdd0801ccab30f5440aceabe0c8c408dddf755f789fae2afbf21a64bc183f2d4218a8a792f2870280e08d84ddcb0101f8870f8e26eacca06623c8291d2b29d26ca7f476f09e89c21302d0b85e144267b2712a028080aace938c0901b0b1014c9b9fab49660c2067f4b60151427cf415aa0887447da450652f83a8027524170580b09dc2df01028c792dea94dab48160e067fb681edd6247ba375281fbcfedc03cb970f3b98e2d80b081daaf14021ab33e69737e157d23e33274c42793be06a8711670e73fa10ecebc604f87cc7180a0b6cef78502020752a6308df9466f0838c978216926cb69e113761e84446d5c8453863f06a05c808095e789c60402edc8db59ee3f13d361537cb65c6c89b218e5580a8fbaf9734e9dc71c26a996d780809ce5fd9ed40a024d3ae5019faae01f3e7ae5e978ae0f6a4344976c414b617146f7e76d9c6020c52101038c6d9ccd2f949909115d5321a26e98018b4679138a0a2c06378cf27c8fdbacfd82214a59c99d9251fa00126d353f9cf502a80d8993a6c223e3c802a40ab405555637f495903d3ba558312881e586d452e6e95826d8e128345f6c0a8f9f350e8c04ef50cf34afa3a9ec19c457143496f8cf7045ed869b581f9efa2f1d65e30f1cec5272b00e9c61a34bdd3c78cf82ae8ef4df3132f70861391069b9c255cd0875496ee376e033ee44f8a2d5605a19c88c07f354483a4144f1116143bb212f02fafb5ef7190ce928d2ab32601de56eb944b76935138ca496a345b89b54526a0265614d7932ac0b99289b75b2e18eb4f2918de8cb419bf19d916569b8f90e450bb5bc0da806b7081ecf36da21737ec52379a143632ef483633059741002ee520807713c344b38f710b904c806cf93d3f604111e6565de5f4a64e9d7ea5c24140965684e03cefcb9064ecefb46b82bb5589a6b9baac1800bed502bbc6636ad92026f57fdf2839f36726b0d69615a03b35bb182ec1ef1dcd790a259127a65208e08ea0dd55c8f8cd993c32458562638cf1fb09f77aa7f40e3fecc432f16b2396d0cb7239f7e9f5600bdface5d5f5c0285a9dca1096bd033c4ccf9ceebe063c01e0ec6e2d551189a3d70ae6214a22cd79322de7710ac834c98955d93a5aeed21f900792a98210a1a4a44a17901de0d93e20863a04903e2e77eb119b31c9971653f070ddec02bd08a577bf132323ccf763d6bfc615f1a35802877c6703b70ab7216089a3d5f9b9eacb55ba430484155cb195f736d6c094528b29d3e01032fe61c2c07da6618cf5edad594056db4f6db44adb47721616c4c70e770661634d436e6e90cbcdfdb44603948338401a6ba60c64ca6b51bbf493ecd99ccddd92e6cad20160b0b983744f90cdc4260f60b0776af7c9e664eeb5394ee1182fb6881026271db0a9aad0764782ba106074a0576239681ecae941a9ef56b7b6dda7dbf08ecafac08ab8302d52ee495e4403f2c8b9b18d53ac3863e22d4181688f2bda37943afbf04a436302498f2298b50761eb6e1f43f6354bdc79671b9e97fa239f77924683904e0cf6b1351d4535393a9352d27b007dfda7a8ae8b767e2b5241313d7b5daf20523a80dd6cc9c049da66a5d23f76c132a85d772c45b4c10f2032f58b90c862f09f625cbd18c91a37bb3fc3a413a2e081618da845910cf5b2e6bffea555e883b0bb9c5f9063380a1c33ebdb764d9ffefe9e3169de40b18eeb9bfca48296457bb0b4e29d7b2b5bc4e0021ba0a1d389f77a8e253d6db149d400f675a9330f3bcfd09c7169224a947b6b4e0745ae08cd7adea4151277a94f51f85292ba082cf28300cca233ff4966b093c9cb6abcef476026040fec2b435021aff717b8bb90a40950e010f70bb416a618dc3c5c03f590c5b7ec8e0c05b85ba94078de4817918f783022364b8aa228b5df43b38fba3060c30616f265022584ab6034ddbc832450f90047d0cf41a4af8a20fb1aa66406133a17c2e905ee28d8acd186c872859c196db0474dfaaaded2d63768143cf6b5e2e34662f7bae573a08cb15069ef881892e5a0c08b5c6c7b2e6376cd2080fb29e8d3d5aa5b853662b4f1784ba7f072130e4dc00cba3cc9278fc4213f2ce2fc82bd1ea9fa91bb17b4f7c36962c78d864eab9f30ef327039da6607962a156a05c384a4a58ddd8f51a0d4fe91f64ae7b0a5199110a66f1e676392ec8d31b20a65f7a7fcff90b37a8a3962bff0c83ee6033a70c5b0af663ca48a8f22ced255839444fc51f5b6a6c1237eda5804289aa25fc93f14d0d4a63cecfa30d213eb3b2497af4a22396cc8c0e7c8b8bb57be8878bfc7fb29c038d39cf9fe0c964ebac13354a580527b1dbaced58500a292eb5f7cdafc772860f8d5c324a7079de9e0c1382228effaf2ac0278ebedad1117c5edacf08105a3f0905bca6e59cdf9fd074e1fbb53628a3d9bf3b7be28b33747438a12ae4fed62d035aa49965912839e41d35206a87fff7f79c686584cc23f38277db146dc4bebd0e612edf8b031021e88d1134188cde11bb6ea30883e6a0b0cc38ababe1eb55bf06f26955f25c25c93f40c77f27423131a7769719b09225723dd5283192f74c8a050829fc6fdec46e708111c2bcb1f562a00e831c804fad7a1f74a9be75a7e1720a552f8bd135b6d2b8e8e2a7712b562c33ec9e1030224c0cfc7a6f3b5dc2e6bd02a98d25c73b3a168daa768b70b8aef5bd12f362a89725571c4a82a06d55b22e071a30e15b006a8ea03012d2bb9a7c6a90b7fbd012efbb1c4fa4f35b2a2a2e4f0d54c4e125084208e096cdee54b2763c0f6fbd1f4f341d8829a2d551bfb889e30ca3af81b2fbecc10d0f106845b73475ec5033baab1bad777c23fa55704ba14e01d228597339f3ba6b6caaaa53a8c701b513c4272ff617494277baf9cdea37870ce0d3c03203f93a4ce87b63c577a9d41a7ccbf1c9d0bcdecd8b72a71e9b911b014e172ff24bc63ba064f6fde212df25c40e88257c92f8bc35c4139f058748b00fa511755d9ae5a7b2b2bdf7cdca13b3171ca85a0a1f75c3cae1983c7da7c748076a1c0d2669e7b2e6b71913677af2bc1a21f1c7c436509514320e248015798a050b2cbb1b076cd5eb72cc336d2aad290f959dc6636a050b0811933b01ea25ec006688da1b7e8b4bb963fbe8bc06b5f716a96b15e22be7f8b99b8feba54ac74f080b799ea3a7599daa723067bf837ca32d8921b7584b17d708971fb21cbb8a2808c7da811cff4967363fe7748f0f8378b7c14dd7a5bd10055c78ccb8b8e8b88206317f35dcad0cb2951e5eb7697d484c63764483f7bbc0ad3ca41630fc76a44006e310249d8a73d7f9ca7ce648b5602b331afb584a3e1db1ec9f2a1fc1d030650557c7dbc62008235911677709dea7b60c8d400c9da16b4b0a988b25e5cf26c00c3ef02812def049bb149ea635280e5b339db1035b7275e154b587cc50464a4c0bfd15c79f54faa10fbe571b73cf1aa4a20746b11c80c8c95899521fe5f0bb3104b0a050c55a79511e202fee30c005339694b18f4e18ab5e36ea21952a01864a0e067d9f19362e009a21c6c1a798f7c1325edd95e98fd1f9cb544909fdf9d076070d1233e183fb6d46a46fbc6e10452ef4c45fa0b88a84962ad6e91cbcc52bc000b12a82e93ae5998b20ee9000a8ef68ec8a44862cc108869fd388142692be6b0657e3fe79eff0e8b72f63aeec5874acf5fb0bfc9fa22645ed6ecaaf186eca690ecdf8a71b8f4789ac41b1f4f7539e04c53dd05e67488ea5849bf069d4eefc040273f6018819fdcbaa170c2ce078062b7bbe951d2214b077c4c836db85e1b138059c382ab408a65a3b94132136945cc4a3974c0f96d88eaa1b07cce02dce04ea0126e6210a9543129bb8296839949f6c3867243d4b0e1ff32be58c188ba905d40e32c53f7871920967210de94f71709f73e826036b4e3fa3e42c23f2912f4ea50557dff78aeb34cb35444965614812cbe14068a62be075fce6bf3310b9e8b12e0dd8379104360f728d47a327c172257134e2c0e7c32e01321f4d636f9047bd750e7993eeda7d39fc16f29696b1becee4d8026e967f8149935b947fce8517b2ce02b7831a232f3a29010129c49494ed2b84c7f881b7e4b02a00ebabf5a36023c404002d6cb88cee76c8ce97b03143ca867359d7e118d54e053b02c94998e6fd8409f8d46fc1741a2e56aebb1e7dab7ca3296a2566263d9be2f4bbef4872a49ee1082cbaf86e21b0c232c4182fc660f0c0b6aaeb0393750e553bc406e2a27842bd033da45a562ed1998ef9bd83e35ed813bef00a3e6147cb363bee63c543ba5e770b043dbacc155214a2496f91879bbc9170a2a513d7b48fad40c8c2d96f951e3a0932f6d12956789198430b352803852aa9726163fbe839979b33f8dbf7f76cd50755c1ce0c40a072aeec35057d06abaf59e878000b1d796e51908bfbf23b13900dcb30f9bd52b52994e7245a7017653a404a70d1c444b8c613ff10a2b057c02d062c5faf13cdc4445809fb6e096923cdbbdca18f59318ff86c7e449f596050b404d3cde0338dfdf9b1389178b1d4c70eefa2bbd76fefc1ee1f1688ef507821e40ae31d8d8e673d183b54563e2cbd27e0e042f61b046877d37a68c1b5784830690f2dd4ebbbd2dbdb35800b9e0ba8ea985fa106dd2ce8493e845586716c538ee9008b88a7c482f3c00c14c08468230d40cdc040e145282c4d61985cb5800306e305146204f63e96ad194bcdf1338ab8480341b6fbccf18fc32145f84bece4069c09e41096e94c24fa4f0db988e860a3bff3604143f2b17e8c219f28189e4cd49a0e506fe62dc419299bcd78c6ccb107f63eb31b4bd8ea1e2fed10e3ac17341d3505019e2376b01f7a7fcea3db110fb090c681c866ac86f13e6f8d44a32861e0580def063736b5c771b2b3b9067045867b4393f3eb2a4610bd0216e29906aaac370986451c6bf78264dda7e7a5fcbcf7bd6e024ff6003c6db780d89b97765cee8d0ff3ff25d94d4b4b919f722b26a6903a017daa62af387843087680c57952de06064de05b662af87be49b6e34cf0991cec7be3396e2eec9678ba259bd8de1c192014d02928f9113488215658df4078ed661fa4e79e58decaeb0ee5a00488b094b0b77f083b2b7844f481e7788ffe8004b96ccdf853532bfd9632a8a652c2d97d10173c90864fbb6facf47fae415df4acc0b099140a657b35d083d74dbdfbf107303e74c64471bed4b2199f2babcb4e1fc593d6f309e21f85e68ffd9904731559d0f2b673b36d3984e5d66d897dfa17d601edef3ed78cb70dc5115d4ae240c203e031263f0cf1e98075bac0361fde24cbcb852b8055d53ae01d61a0a1e1ba423d00833747e7364df7ebfd1f84598d801c249e1805279dc37d39fc7f7e27b067e4e0287aec432ed49e4d701a0ff377e88179968430d110cb20476ed4c6bf1624d1907ef24406d3295fcacde2a102cc85f4f3d0cb87a8fae7535a06e442833e58cfc04242ff85fb654d05f9874c0a6756f542db4e9d8b0366191fbb8b09a1bbcb6af04c069978417ca80d92f442b7dbd092f74e1268aa73b54e4b64e84543449ecd30b5ea392a1669a5f441d7208925e91c75df611cd26042630c6b98f160b8c0156048108d5465b71bbc54d31a9f90e34428d97590a427e1ae618d4a35fc1022d4e007c6108dcb1672b88d43ae4d886a5adcc26faf56bc5e5a0b08342fb88263fd80940d1edf794c6ad6d339b974e164b38439e11b4fa87cc793b080b4f8bf0eb56043f79ed3911da21092475fcf8320b55b9f558f194c6c8121b2e696039340d97057be2583726d762b5ae4327e5286a2d8c14ddbe0027c75aacbf7e9de13037390df7d72e13b46bc06bad0363b070e0174d034120d7fa7b4550e7dc28f7f0241f059ae266fc13dccd1d07f744208a7d6a2e565b6613d46e4550f79ef3209c46a805b97284df558719e131f44e419e690f4fc28ee4862b9d1f8f7e1a164ac18141076087693e70ac76a10f7851530d4cbc65def90d5544671ad64249569c3abf0200d09be3c63efaa7cb723b39ccffc9b3a3ba0c8426847123d2a881efbd4937a40cb3e8011c70ba427f80b3dc91a608086f8b61f8bd86fcb482ea388299a7cfbd00a3ddfadb4b6d0e51c1369276c25889a9f3592900b6502d9af1c732e1fb7db307d71e45deb1553ba1568d0480ea9e132b52564da6ac5c56eff823e7f37976cd075ce8f7a78aaef1b87f7437a0b84035677f266f7d0596e493101fec3e14fcf80b22322454587b51fda0636231c07d4e63e007f1b89137d8a37b03bf00f3a7c10169f757d9a74b7bffba797c746e3845decc3d0559d7cf6f08f3bd67dac5f33109b212582dc7df5d561ad63dddc794f2aea4e493db1a73c702d258c9b922c35d04c47f88f87c54c821a29f04abd91a079ce8cef252a21dc72d409fd1618c9be709af029ba98b0140e74666fcb01bced4f88ab68e6b63b8ed6febc0905d22cb2200493c071ce136833697406f8a04e77b21747dda997046cf3c7080096fe481790d77cf5904e7f7128ed95a6e576d109fdf10eb0c888db35a4a685b62253987b70fb1538e6c0932889460fa31c60d123266b7bcb828f846a35b2851127679b05f05a75266529c343a6075e54c455e02b2c83e6f7bf1ae23326506a5f532472d780815c5af425f7d8b543a8f014966e0538f48ca84d181695381a09701eb65c9ae084bf2a4dc84f1b2071be32be25d5f4fcdc59668fd800496ef7eb6dddf867ab908e543cb51f0451706cce4be6b9f68a537a79ea88e17fcd78965b3da68c0d9d30623a2a9e275e1c320f59e118e09c02eee527167bc06f7693e7b584e3b25ecc1093d46b80a1cacced87c2b32e2f90c5bbb9cd1b701aae69a04b16d535fac6eab0d091790fc5fdfa8a8842bfcb62dbf963cbf62c4afb4c468be98c770e6078b8c0a8cfcbae43dcfff17d3c6d587c3e4309fd39c66acd14781fea66fc57278b02302c0fa386280e67acff19955b6a428b0e22ceb1e54e913a37cd19eb6e9d2268a039f2b5fdda7d5804db79385f0e50082b128c952f8dfdedc4411d0675d95127f0bfc01710a869b10d7a8b9e632dad944062567439e6d192fb09329d058e87ecd0aa8981328f541e87ed02cfe4031f2d3a046ff517a2a80486b04ade31a647aec0884fb96ed753ffc47892431c6e6f08fd1c633a1a44e882d3d8b92c567e0fb8305327a354851464ca0f18d89c6ee2a91a4afef0c55883acf8fcb68c2c3b7402e005d8affc19c13f1f26fee0698dff181ab22cb84a2b31e0a6a81dc5d02e60a3c07090397ae58a985526b2ad6ee5725e82328062b68566b4871705ce3b9856e550d068c20fd9aaeb27740c07aad53d79fc20e46e40e7103e2d69626ee64b6aa600f6f1a86f37948ff4990d88f43c34994e2fe586cb779997be323da53329c10480aeb08fe440e9e4b979171371c73b94da9f928a3f6c8f6792f782f3d6432b86d06f54557327fef31fd6ae0a3f6d2f16c9ad947d132e14def33fa24cb4565370e0832fa50f5f5f93c9f3d65776cc22608b68a4f3719e9be47a19432991e4a2c49089c0ea20e7f7c73feaa47970da424d8543d80d622e2f2be9f4c65cc39dc369009a9d41a52bdea7cc0e8e04da87a633fd4f814fda1b646121a469ba0b5b8006d0e9118761d97b5d1856e2d690f27a81c42b176df853d07cf4a66ee83c9eb24ac0a382f5143a10a33ec3ddf17dcd8a8303fac8f279d31b4d04d74bd8804cefbb400c86174ad444e43ed33ee1e1e73f660b9814d5ca3cb1d650f1978a825a617bb05f84eab3b9b8359b991e1084cf4e8179ecb67f92398638e31227ff63427b67f0f232b454a341d85d4b56e31135c9035e231f7d9318ca12b5ab524f87bb0ca9b04b80effed202897ab016d5acc054c4fe62a5f0192f136cf2cd714998a4b164b0c2cdbace52243fdc9ea879b0d247d4fe8bd80481fad6b325cb5f2cfa2534dec0e47d41b6b99352e6e5faccb5ee28ca2fe96e04f9c83a0461ba34cfb499d864f05dc734b6c8f51cc0c994290b2a868cb8312df39fb6d457a81e62d872c65d4f3007094be42663bca3d64ebbcc8401158fce4f5a38a49c20f029c338f126451820459866e77c6984a467aad571cc7452392a9cb9f8e65099fff2f2acd170a833e01ed3d22a683356ee42dcbe6bab6d05d3edda2d40e9ba53884d430c2e0cd87c0067dc8cb68c868bd9f29db1dd73703c139ffc15e4f7264e727c70560ae02da100871f30e8a7c1275805f621752d73aafecddc2a7808b6c2ecbb8d0134a644bb603f30f8d18b3fc5efaa7f206ce180bfb14f5dbd3b0115145a227113eeaf1c1ec04244227b931388e72960833a40baa4319c5cf74aa94f7e3233e1d09f0a4f74409999684ad1cc836ac74563c85b164664dfab08ea084b25e2cbd7e7b94a781a10fcd455ee38b65126dcc52016127fd195c80b05660ed931e128b0cb92868955c0d032ada9fb951210a5442d21c1718ebc4702ad4a57967e15a63ffb05e1e072a0c41ebdf1e7205373eeaf4587f695de887fa3a8c96b8deb99e040fa1fc4dc2a402a017891943d734ae2f3798b22b1269d3d9f6d65581b9c637a6896a4fb554810bbd3db5c5737391a74150b43413b2e3824490b7911cbeb845147f1a8521620b0dd31306f13a9754a01bcdbd18bfdeade06b0ec97f48df56c45d3670a1fe18d00ef13e613c8a77aeb40401a814b377137cf44f29cb2cb94186ad1161ecb05a7c07837a5ab3474e57990cff2ab16b4d99f62e646da28e8bb712a5b561cf0e25be039c3e08583c8ebc3dd2fdb8fdc6e135ecc7851c73218a70b75e697cc84ea50504b9c34a33ed52f87230b9d192a940f3b7bb6d45b58dbf52f0afeb8dac85c77b06bdf9b70a10cb81c50055c9d8cf7e3a5c4b7dfae55beabcb3e8a8a1cb822d8d0bf6c01e32056929f853021eae6c97fdb0c5031df6b2e7c57f1318866769a9cc09c38ed62d8bf4663334c0df67c47236ed73f6ce7f54e0ada9270398c1aa558d0f993b0d25d97aea77b1635ee4832362cd590bae5fc1549402ddcd42b15efc930111a01535c0242116078d6d2d53b8612d378c4370e90d0d01b01bd7da591bec07981652a98485d8ed5c8f3def2bdac7d992ee5fc6a1ec7bd36940e1bc58c7050451248fc3ee6069e6b1b0d3ef122c6ef2a9b99aa0f145fb43341c58dbb472130b51730c956273a3ef6df9e000f6a87c2bacdefcdb5daef28b6170f61bc3a9c101f439755c86e6b85ee06a7a60688b3843eb359cd4acd9221a2ee131e2fd2e190652e5c47c0b98c41010eb99a991ec48a5de99cc8f403d6d76f8307d6657c1e007ebd64eec7bbd0d4f1ba2db7bb0efe27c7828f053e00def775943ab01a7e33d0fffcfe6f9a7285237f2c381b638758e373f8ceac672190664bb25fb5d355c240bd1773d61bda7f7ef1f4261b80ff5058ec6f7e024ab9459b1103815624b81f80c39db2f6fecb72de452b11636b0f71b16cb55f883d93bebb94328f13ef1ab6d0df449e32d27884f5139af584035547dace65ee25ba05cc461e74760d4468af90dcaa982e52cb902e2b84b3324019da575601ca54e91655913892e703257deaa01d14fd8459ff780c724161ba4d4280b70a5039dcfb5d775560714009724cb0d0b7e178c71e777b896bcfcde7d4c9c3dc6ab819d74a1a1fda8486448b1ad79be02fb134ea93a8600f1bc2a42e68d0213ab461a07cef3ad3965bc130beb76bab409102f82bf6c4cd626f6df3388e17b87584310c50832cde3191f6557f0014bdc0a68d924119e43111043bc6f26d16a5f2612dae6ab24984e2d87a71d93d5f4670dba2176d4f16633407bf7c10b51b6842dfdbc6fe3eaa4b6a12f0550700ece070ca382dec3b587e0e1fc317a48a83754d15aaf9a6971b8cb641fd8b32846d89002e6301700a0e7056e8002d8f269d29ebaf64f4493b1f1e676fc78e673067fb00625df15dc0490235b386ee14e55b335f3bc6dcedd7d3a80fd3a6e9bc2ccf3af0d89be71b5ca92bd7a9b97b9ff8976f75702419aa5bf9be34600496ca1bfa8ad0400602a23579365574252434f2bcd7efb360b0e8a495e8f7e78923b6fbf2207049e9179f0d4d7d6b4a4a10ca10f0ef4dd6cb5a74f7574e832044d6120fbc1580a68eddfbc65ab300bed960a6f24a102dc36b72937a8be4385daf5946e81ccde0619251babbff17e5685217a134d22f6130d0322483b3475227ffd27adc73ca202a6debfa37e5731747f4449ac70a33684f460eede65918c6d89acf4b50fd28d040ffbd436a944d3be0210606bfc2301e7ac66d462dba29a0489eb55af714a760e5302592cccc726e535b945ceb6126eb84e31f0f140ff54df8be0fa3a22f418036ad996787a5616a97a42049ebce351dc11857cab3dc914ef26833b0e75653004a8cafea099fb0750135255c41ef43e2f29c75714e2f0be2545e7c109b70c43004a471daa85b47befc65907d033f133b2f3ac2ad568df630ee80506610b8dc9052d442668dc06b13ea76ab1ab7b34870341d660af5d3007c21bb72512e4f8a60d8916a037b93f9e15ac9e4a6a1246d73ebb40e5fdd5a0d6dc0cf175023b891301f69fe5a3ca6f12cb8312d16333de1cd3ebb99339ab18c0715bfcd35b8365b407ad759e2c591d8270ad335381573e27ec18af7ca157b4a2bbca921db083d9b0009dc332a79dde14354a8c18bce76a1bfc1a25a1e702ccaa0feb521ee9279b8a01ceab6e237bbe4128b23cb53b1e5185f3266e20670a307ea0cfb5377025e0bb0790d48f1636c8b836c1a1f69ad61265f19057197e86cd526da6ddb94fd1ece80b60852f27ef2ce56ccb5a32d8cab6d16be06f380dfde3602ea4c1ae927173b2001ff0d9e29bc66b2b2a20c3e3ac174fcba187aacab0876c1356d30d4021e6dd0048c3bdfbf254108bb09d3ca9f2be423a92408bca52fbcd68f972c46fc8d20e0350d12c2f2d6c7da85e96bcec3ce61119793d44a210f81ece859fef6360ae3b0e1af0634fc141a8b50b3b383fb264e8a4fb84ea06db6becbf5e140edf66ee190da8968da579eb349fedea45e4c252a79570501278bab5fa984d7b1179d7c2460faa7beafee153bbae0a591701632aa94839528d3ef50cf809c1f7209b9e5c99010eaff7f921c45b6546358ee7a90948e3c710cd3e1796860839a345516fdf4f07c415029627abe1273a1f510c36a662562d18169b23305b4efadfefbfbb41a400ab533e61c14cafa49bc5d2818058ee4f3e1aeb329e150820d1de1f1eaaad31051a6dfdd3a1d5cec7b16bc0ea2c649d409917faa42138b1f824b4d534a050be0a99ea6772daf0b2e58623cc7a250ef37599bd556508f08886e663ef0917ecd3077072c3268ea5b9b89cbb6b761ee9f9c4765d8b267d9eb19728a28ce67a42ed0cad142b5dc0fc5313853860ec3f0ee2bc3d47cbe12dbe9633db809967d5b8bf0e45574eac657059530c30aeeade1e4f858a4a6e79d6e441b4af0127a13340d908d48cfec849ee93d53b1564231f048d34885e791a9d40c61a7b00f12f6f72a5050bcaabcc98480170ea6e20bef6b5c6f504c808108454fe2f3c275bf8f89a5e0a3304a7c4787e6d4fbe569930f7cfd38ab7d1d2ebd599bbb411950cec3e53b90cefb82234990d353c71ce4c21ef674a1c4070f71c90e1ea7edf35f5a421118f01b49a92ea97720e2d4df6b5885c181002656629a90eaa1904fe1c379b8291480ca15d0dc2b65a20c22f1e01d612d21ecb5738e5ebfc578a4a65066ee6e913e3030d3fdfb0168fd75022492728ee82869deb9ff2827f4e10759ecddb20f67e9808e707257a74d3dc0a6068f264066f95c9f772a3dcec0b4f0a327e3745517ad60ccbc5392890d2479b724d068fcdb83607e02291c06e1a5a1dac7604889cce2500f418da2f7080a7e9a1bdf28b87028a2bbb0c14f059f10f46d46716eac2cdfc06676cbec91b8c2c0f7c9bea7e27fa5048662398b23a9b488a49e1d3330c04e60179a4492c8b836780899899d2af17e6119a94d54a890ce8c0b550e87fd54cba0821fa7c48f6e09a60dcbddf853f82b47195aa44a5ceae14a9257296acd711c8073ff3345befca5d3ebf64901b283df96395fe9785d7090176bfe5a9f13ceae701c6c93af0e13d949bc3c7e9b06674a73e7affc508302258a27fb34569c3742201c0721aef282a31c69a5d98a67ac5c3d920d50c089896f7f8c8c237a81f803f0444f417246d695e89a3a523b62a3cd2203d42607cac7c7782dec1f9edbb806c0a7a37d1a969082a126bc726151a50233456a07d374399e74aaa8cc66821511d092615950d302e815cfcc021e1250cdea20fd9e1e4b5e88280d6e4283b918e780d12cbba59ef2ed2ce86135a48fba6c0dc2bf2efee190d9a3f9aa22a622b1953058f2bb3a371637d13e045d54e7eb54c0d25851f49283d7d34e9785d2d5c3f70086c48a8325a2083bdf5b3531fcc697cc0c9f63892a866c84585d673a2a63fd60e77995bfb0c0a44a4b63c0ff67e813027d3e84cddd393a0f4e6bc95525c5ae20eed9d0cea4a12aa748eb5209cfd75990b055f1ad0472f9f7599f569a8743a720755aa11555df4bb2e725fa93bc5dea603a964e8dc9fb1742e81825022866fc50a6b2a19b6a234a38ee27a74f2f5832b294143ca7ff8d07fd7d4e01f479e9792058871d90ee3aaa3329e82cebe41dff5e6d00a36268a7965466b80c6510ac1350cee797e1d6737f6aaff155266d2a2d611b2124affed1ac73a6a06515627b2230ce0d7fed33ecbde511f4d472cbc556cc8d9c5640e67657035112976b626847a0a4ea5fdd14d4a3eed57f0dfbe153393d8bd28c8b4f9e62940e8379790393fa20c617050c780a7d870193b4611bd7a12d26947a3cf4605e225da8b1646a76984015a5e317016a4d8301eaeec0db3ae0daa719182e2f4479154dbcccfcce1f365099de6c91934c395ce82abba8062a51d7773b418330921766cd3d275c689098e06039698db6f09accb292e7eb79e7a022d4257bb2f9ed993c519860919bc229a06ad88954c9ebf7f5b9fe95cf56e8181cb9175dac06be0be70fd28df20cdb4600ef0869668c645c9ea01360fdae7c922cb3d2b3583ae1de5ae7d899a83ff2bb00d7365c782a0fccfcba7f87bb29416469bb051f9b0755123e0f2fa76dc7644b70e452f49a84bc372b384c843b8161b7f9b63699adcadd5cb2b33b36c7eb3e1b00f25218bc16447968b939016242fceaebd796c17a24d1b9870991a9c3ae90e380302b7bb320adacc08cfb9249d29cd9275c52476dac6a7e9870ee3776cbc3352036f9c8f681d44856c6c5f90b7cde0877472ddd48719c449f59dca1f49442f7505e4809c6d323b37530ecccf3e41e19822f53d64dc90efb113405ee88799c37f0a342293b5bfc019a9057138326de6107b5613554dffc737aed7237fb16cd77e09f581d12220ac930c6ca279efd1d07a92125fb2606ec3ec35351987a15fc72806cfb3cb66fce8dcfabee5c1e586bf0f802fa12ae5ad5a708e3a5d54e1926dbd0202bf1150f1bb612b9a4590b5b520b86a90860ec3d9c2184f9975ced15ae1300882d9918021b43a1184ba88ddd7091539fe5a7017b8708d0f5c916f9c42de5103f8116863864b508f5880ca60b7492385c16a02b6ceb64d257a4838873b85d2041517c5c7c4508e4d5a5faa72729d73af0361e11828eeca992b8f20d903a5ef065976a9f322e34bd4b3984bb09e18be40e77e833c8c1a2e80093227d3f40d4a067f5e3aee9fce9bd234bb6ff4d0c34fc060d23e86b1f5a6d8d052e53e913182052a2d9c5e97bb0e0a51bb2fafbe7346bacfcbadb00ce2ba129f29d41a11f7d105cf19bb60b5f5b0dfd6a894698ef7f56a02d69cc03eb62a56563d3a77e3ac2302"], + "untrusted": false +}"#; +} + +define_request_and_response! { + get_alt_blocks_hashes (other), + GET_ALT_BLOCKS_HASHES: &str, + Request = +r#"{}"#; + Response = +r#"{ + "blks_hashes": ["8ee10db35b1baf943f201b303890a29e7d45437bd76c2bd4df0d2f2ee34be109"], + "credits": 0, + "status": "OK", + "top_hash": "", + "untrusted": false +}"#; +} + +define_request_and_response! { + is_key_image_spent (other), + IS_KEY_IMAGE_SPENT: &str, + Request = +r#"{ + "key_images": [ + "8d1bd8181bf7d857bdb281e0153d84cd55a3fcaa57c3e570f4a49f935850b5e3", + "7319134bfc50668251f5b899c66b005805ee255c136f0e1cecbb0f3a912e09d4" + ] +}"#; + Response = +r#"{ + "credits": 0, + "spent_status": [1,1], + "status": "OK", + "top_hash": "", + "untrusted": false +}"#; +} + +define_request_and_response! { + send_raw_transaction (other), + SEND_RAW_TRANSACTION: &str, + Request = +r#"{ + "tx_as_hex": "dc16fa8eaffe1484ca9014ea050e13131d3acf23b419f33bb4cc0b32b6c49308", + "do_not_relay": false +}"#; + Response = +r#"{ + "credits": 0, + "double_spend": false, + "fee_too_low": false, + "invalid_input": false, + "invalid_output": false, + "low_mixin": false, + "not_relayed": false, + "overspend": false, + "reason": "", + "sanity_check_failed": false, + "status": "Failed", + "too_big": false, + "too_few_outputs": false, + "top_hash": "", + "tx_extra_too_big": false, + "untrusted": false +}"#; +} + +define_request_and_response! { + start_mining (other), + START_MINING: &str, + Request = +r#"{ + "do_background_mining": false, + "ignore_battery": true, + "miner_address": "47xu3gQpF569au9C2ajo5SSMrWji6xnoE5vhr94EzFRaKAGw6hEGFXYAwVADKuRpzsjiU1PtmaVgcjUJF89ghGPhUXkndHc", + "threads_count": 1 +}"#; + Response = +r#"{ + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + stop_mining (other), + STOP_MINING: &str, + Request = +r#"{}"#; + Response = +r#"{ + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + mining_status (other), + MINING_STATUS: &str, + Request = +r#"{}"#; + Response = +r#"{ + "active": false, + "address": "", + "bg_idle_threshold": 0, + "bg_ignore_battery": false, + "bg_min_idle_seconds": 0, + "bg_target": 0, + "block_reward": 0, + "block_target": 120, + "difficulty": 292022797663, + "difficulty_top64": 0, + "is_background_mining_enabled": false, + "pow_algorithm": "RandomX", + "speed": 0, + "status": "OK", + "threads_count": 0, + "untrusted": false, + "wide_difficulty": "0x43fdea455f" +}"#; +} + +define_request_and_response! { + save_bc (other), + SAVE_BC: &str, + Request = +r#"{}"#; + Response = +r#"{ + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + get_peer_list (other), + GET_PEER_LIST: &str, + Request = +r#"{}"#; + Response = +r#"{ + "gray_list": [{ + "host": "161.97.193.0", + "id": 18269586253849566614, + "ip": 12673441, + "last_seen": 0, + "port": 18080 + },{ + "host": "193.142.4.2", + "id": 10865563782170056467, + "ip": 33853121, + "last_seen": 0, + "port": 18085, + "pruning_seed": 387, + "rpc_port": 19085 + }], + "status": "OK", + "untrusted": false, + "white_list": [{ + "host": "78.27.98.0", + "id": 11368279936682035606, + "ip": 6429518, + "last_seen": 1721246387, + "port": 18080, + "pruning_seed": 384 + },{ + "host": "67.4.163.2", + "id": 16545113262826842499, + "ip": 44237891, + "last_seen": 1721246387, + "port": 18080 + },{ + "host": "70.52.75.3", + "id": 3863337548778177169, + "ip": 55260230, + "last_seen": 1721246387, + "port": 18080, + "rpc_port": 18081 + }] +}"#; +} + +define_request_and_response! { + set_log_hash_rate (other), + SET_LOG_HASH_RATE: &str, + Request = +r#"{ + "visible": true +}"#; + Response = +r#" +{ + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + set_log_level (other), + SET_LOG_LEVEL: &str, + Request = +r#"{ + "level": 1 +}"#; + Response = +r#"{ + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + set_log_categories (other), + SET_LOG_CATEGORIES: &str, + Request = +r#"{ + "categories": "*:INFO" +}"#; + Response = +r#" +{ + "categories": "*:INFO", + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + set_bootstrap_daemon (other), + SET_BOOTSTRAP_DAEMON: &str, + Request = +r#"{ + "address": "http://getmonero.org:18081" +}"#; + Response = +r#"{ + "status": "OK" +}"#; +} + +define_request_and_response! { + get_transaction_pool (other), + GET_TRANSACTION_POOL: &str, + Request = +r#"{}"#; + Response = +r#"{ + "credits": 0, + "spent_key_images": [{ + "id_hash": "563cd0f22a17177353e494beb070af0f53ed6d003ada32123c7ec3c23f681393", + "txs_hashes": ["63b7d903d41ab2605043be9df08eb45b752727bf7a02d0d686c823d5863d7d83"] + },{ + "id_hash": "913f889441c829e62c741c27614cdbb6278555b768fbd583424e1bb45c65e43b", + "txs_hashes": ["3fd963b931b1ac20e3709ba0249143fe8cff4856200055336ba9330970e6306a"] + },{ + "id_hash": "0007a41ed49aa2f094518d30db5442accaa7d3632381474d649644678b6d23c0", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "05138378dedfae3adbd844cf76c060226aaeddcd4450c67178e41085d0ae9e53", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "1cccfcece29fbd7a28052821fdd7aac6548212cab0d679dd779a37799111f9ec", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "1eda8e08b1024028064450019b924eca2e3b3e3446d1ac58d0b8e89dc4ba980d", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "38d739cfb68aba73f0f451c7d8d8e51ae8821e17b275d03214054cc1fe4f72d6", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "40e57cb9a9f313f864eef7bf70dea07c2636952f3cbff30385ac26ee244a4349", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "52418ac25be58fbfcc8bd35c9833532d0fa911c875fa34b53118df5be0b3ba48", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "65bb760c9a31da39911fa6d0e918e884538f0a218d479f84a1c9cca2f9a5f500", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "7d805459f05d89c92443f43863fa5a4d17241d936fc042cc9847a33a461090c5", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "88f7594b26dcbaff22f7e7569473462c49d8fb845aa916d7a7663be8b85b8553", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "a2b08a090f611ea1097622cc63a49256a2d94a90b8dbaaa5e53a85001c86d55a", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "a5ebf4914f887ecdfde8e7ef303a7f2cc20521a2a305ba9a618e63d95debfb22", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "c5b7d94e661c5eb09714b243f3854cc06531b1085442834c9e870501031b73da", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "987605d678e8bfb17e8d2651e8dd5c69c73c705d003c82e4e35d2b5b89c9ebe3", + "txs_hashes": ["7c32ac906393a55797b17efef623ca9577ba5e3d26c1cf54231dcf06459eff81"] + },{ + "id_hash": "ca559feaf79de4445ca4d2bcc05883b25ecff2f6dd8fd02a9a14adea4849f06f", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "d656ac13a64576e7af5ca416d99b899b0bafef5e71d50e349e467fa463b13600", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "dc006e92fc1e623298b3415ddccfc96a8cae64cb7c9199505a767a16ddd39bb9", + "txs_hashes": ["b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986"] + },{ + "id_hash": "fb3e7cc08761a6037ca29965f27d2a145f045da5a1018ca7e6a5a5a93dbbd33d", + "txs_hashes": ["c072513a1e96497ad7a99c2cc39182bcb4f820e42cce0f04718048424713d9b1"] + },{ + "id_hash": "4ffd1487bf46e5a1929ca0dd48077cb8ddbff923e74517f1aeb7c54317c0fd68", + "txs_hashes": ["88504bd6a72b26bccbc7563efe365baeedb295011a4022089bdc735f508a9412"] + },{ + "id_hash": "f64056280ede74b3b1fe275cf9b9aa1feda77b3b5fd5218d6a765384e3d180ff", + "txs_hashes": ["88504bd6a72b26bccbc7563efe365baeedb295011a4022089bdc735f508a9412"] + },{ + "id_hash": "d2ed8513f48724933df6229a9fb6ededdcf5d0963280ee44fa9216ceebe7941f", + "txs_hashes": ["a60834967cc6d22e61acbc298d5d2c725cbf5c8c492b999f3420da126f41b6c7"] + },{ + "id_hash": "428be79097b510e49fe5b25804029ac8bfa5e2a640a8b0e3e0a8199b1d26f22f", + "txs_hashes": ["d696e0a07d4a5315239fda1d2fec3fa94c7f87148e254a2e6ce8a648bed86bb3"] + },{ + "id_hash": "368fbc77179fb30bf07073783f6ef08bfb1a8c096e9bd60bb57aead3b0f3663d", + "txs_hashes": ["9d1bcbdb17d24a4e615a9af7100da671ab34bffc808da978004dcef86ddf831e"] + },{ + "id_hash": "45a88adb7fcac982f5f4d8367f84e0f205235f58ad997f5dfa4707192fd3d9e0", + "txs_hashes": ["9d1bcbdb17d24a4e615a9af7100da671ab34bffc808da978004dcef86ddf831e"] + },{ + "id_hash": "6d80d9c12f1439b0a994f767d71d98d2d2cde1a54c6a6134a00c2f07135d98cf", + "txs_hashes": ["dbabb82a5f97d9da58d587c179b5861576144ea0cd14b96bef62b83d8838f363"] + },{ + "id_hash": "2c479dbff819502441604a914af485db2f795b7f5bc0eab877d60a1419ee5498", + "txs_hashes": ["aef60754dc1b2cd788faf23dd3c62afd3a0ac14e088cd6c8d22f1597860e47cd"] + },{ + "id_hash": "a7f204f932169b1b056fc63be06db8ec91a436f7188a30545bcd6a8bae817ca4", + "txs_hashes": ["8a6ebea82ede84b743c256c050a40ae7999e67b443c06ccb2322db626d62d970"] + }], + "status": "OK", + "top_hash": "", + "transactions": [{ + "blob_size": 2221, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 44420000, + "id_hash": "88504bd6a72b26bccbc7563efe365baeedb295011a4022089bdc735f508a9412", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261656, + "max_used_block_height": 3195160, + "max_used_block_id_hash": "2f7b8ca3dbd64cb33f428ece414b2b1cef405cfcd85fab1a70383490cc7ed603", + "receive_time": 1721261656, + "relayed": true, + "tx_blob": "020002020010f0bcd533b7d71bdb8915caa004b3a214f99f0993a303fd9804d1f101aa6c870de32a932ab774f80fc92cf64056280ede74b3b1fe275cf9b9aa1feda77b3b5fd5218d6a765384e3d180ff020010bc85cd27e4bfb407c598a104bc5e8e8c2d9bfc40add114c0e501b09e04edb204d1a901d2f4019603d50d9f07c4354ffd1487bf46e5a1929ca0dd48077cb8ddbff923e74517f1aeb7c54317c0fd68020003bbb37ea2c935e3c7245150d154748b59bdf280d557779b6cf8063165a7d9b5b9210003ffc23770cf9e4536c0db95978dbc937d1de339cb8dedf909f5adf7ce6fb8c4ea5b2c01d3b65a92cbd04597cc4f3da6a003ca5309af343f2d0f190102fdd7ed13e5b9ee020901d979920cfa70c6ef06a0979715e6d4eb51a032a1511050389f0d8eafbe5f91af7e37907d37de11bae31af65af35e1fabe13cece1d503a9987d7f4813c8ad2b6ef2b5e77281113637ace74d9ac41f16f431a1a49e6d8de1b73fe877c5c301fcaac875ca31629041923a1bb014f86a232227c41e1611f5961a0e095c6201ea34a7b7367b0045c3a841d57ebac2a9b323ab21f6d954f4441fc79fd98414e5c4acdbff571108da31d0face012eb149b24bc16de858b4cb8d96fdc0bb614cc2895a6859cdeb086e647983308714da41be9ada21abfbec1ed4d224315017dacf01c5b2a59d18ac5c3b81c9bfd5c031b9929c1dc802a22593bdea39612039601c0e09f64702dde1507e3daef5655b0f1f32e19fcbcbaeea6b495fed05543cbb65010730de65cd66a314cfdbe7474a387045b3000dd43eedc021ed492075d314da6d8c6a3905275d41cdc8758e258c4a71a64d2ba1aec68b7ad68018aa8fdcf97538898c61392ded8e0715ddd471638be54eda62622f5787cafc577da4ff7dda01578982328c51f59ad3d9218eb0d3201d1136d54e7567e15c3f8bc956772bee20f5976b0f343096ab4a0c2b68099bca4d61eff7a078c91875483213f4cd226b587b5c12bf7a41abc9079e274e6229187f4c3cc1a8579f60f2a8112aafa78eaefa765ed7588be97d471720979fa5b907c5b83be30d62d5a2b0b9a59f1330dface4cddd07f591829caac227efef5fe5076e3fa93dc9a787be8f57c3d2ec216342784321c80b956f44dec2d484500371f9a4fdad1de571f16d2cccca13f2f3bb65718dc4a861276d08d11bc72536b787537aa0b26d68462500baa1b5b47a1ff669346481ac5c0d2199d6197dfc9c74cdd6adf13e06223af430e48bcafce9cd8765ae6411d5a3ff2c8827ca2fb9ec63cfd0c84c2e1cc76d2fb0a3f9619034adc3d0fa60b729fa3352433a1f4f2c7bbb51fc61673b61833f70d8700446442d57e0a6fde600fc1cd0d659f8b6b6ca8e320395b831d2b79b95d006c2fc5afb72635535ce1e953d9e70a0022ac9091cb5810450d72edf9bff63c2b64933e0d69881b6ae9c9bc402b11bcd2ca24cea5171ce4040398ba42f87dec9791ac7376adce1cb47be22bc0ba083395b214bf88c5e81357eb95b461c1ea4c814357b5fa7dda0b7083a3360089af604f25927d738dfb3806a559285f04435a9245356051d27cbb0f5c020f40612a4ed7e2d124a8d6dc7d2e39127f5d66bbcad0bd8af4cf173b89283d09c610ec53ffb5b2e0aaedad8700de5555decda90d9b1f022ec0f1cabe3627ea89bb80420d73a60f7f33886541626e5aa0cb758bc9775a80c2427bd9fd373ce1492f90e93d0cd063f1233975d5ae4d732970c686b21850cd4146e5775af2a3b48f6920f021bc91e59b79200bacd2ebef0e2045ae01d7287f14a3ef08de813492f92c6034a7ef9b7a74660125a9761379d3495eb8cbd5e0461a0ae90ac7bb4dd6ee57902158dab0fbe495663b5536ac17e444ea5d6a5ec67f2145d5ac6eb033fe3ed88023133d514073957b3ab9506b375b52ab4e25df97c81f210d2a5d0ac2fd625c00522ee9cecb7dc0c84a47eef9338472bfa766f0bf5919be6a5b56bf5b84f1be1058d570d7ea8f372c5c253c189f006b314d377610bbc9a41fc162b3df3d860d904c74d0451fccecdcb8c0fdb66f55f10a955f49406f16c6ce397b78af25dcbfa001a1cc63df1dfcd2918dba5e64532af7a24f3a95c722815ad2192f488fe8da9080f8c295fdf955dbfe98666d411605e11598745385d7b639d8aed5b5499ffd007143ff548f1f2956da85253ed716d16f7ed1ba3ed100426e2a81dfa2bdd952f06997389359aef4673cff1fcf634c4261c3f8a028c25712896381ef8e88b53ce0996cd93d9bfc6fa1a578554d1b0767962bbfb88f553d5bb129cf18ed93685b50d60d8e13ef8c06f14e7f4fb212ac28be059f83bd3c375220c4368d405ecc9f601a31d48f081ab49014d562c39b464f850af6679daffddb75935f4bdf2d8735a013df11848f92dca8088339595f99024bb766c19e863175c0234157738925e4c0f4b5a83686667e9711547b3a2a96946fc126a026cdfc477de41e6c85835dad80a1a370f59950b9c9759595425609d6371d41801098202cb87ff96fcfb0247730bb2178497eeb94f794d151fd5393082ec7a0359b409b7508303493f749723780159badacf201cb6bf41691ba3ed894dc4a3b22a3a829ff13a349256379e65b108b53c62247b27176ae5d22295393e1856372f1a89fa7d364173647ba296b76c04950e768eec5f38634e8cf3beff55bd7ce266b5bebe5b854a02cad0307b0b670433bdbf8bc2631430773465cb091c7a666709285816e6d503acd5e649045d3c07baad6c71df6b89a0481a1fe7f45f6aa8837625ccb43ed5f9e8e9f7daf2cdee0e7f1baa61b625d9f5d0c9ee49605d403afa625927e9bf41d7e5d48b454dc1520b19e1c35dd25fc5dff641ea05bc2b6b5697485f96bd3664f90077c567923d4f0404107251310935d78e2d06471962f23277b5207500917d528aeef43e2b670c03e614bd3ee3fd58b486a3d0a2494916785325e6546dec8fb880cc401319a7f90b4d3832853ea6e0ae698543e20975eaa9c6606068f2465bade18d110a6937e199229fc569e4dbb09f253fdc89279b76b70f3bf61d6808d7fa8ff438c795464101a97d68d18f240c9d7137f2db1d38013ba94cf478338fa0353b2a5cbf937f00bb", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 108355184, 453559, 345307, 69706, 332083, 151545, 53651, 68733, 30929, 13866, 1671, 5475, 5395, 14903, 2040, 5705\n ], \n \"k_image\": \"f64056280ede74b3b1fe275cf9b9aa1feda77b3b5fd5218d6a765384e3d180ff\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 83051196, 15540196, 8932421, 12092, 738830, 1064475, 338093, 29376, 69424, 72045, 21713, 31314, 406, 1749, 927, 6852\n ], \n \"k_image\": \"4ffd1487bf46e5a1929ca0dd48077cb8ddbff923e74517f1aeb7c54317c0fd68\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"bbb37ea2c935e3c7245150d154748b59bdf280d557779b6cf8063165a7d9b5b9\", \n \"view_tag\": \"21\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"ffc23770cf9e4536c0db95978dbc937d1de339cb8dedf909f5adf7ce6fb8c4ea\", \n \"view_tag\": \"5b\"\n }\n }\n }\n ], \n \"extra\": [ 1, 211, 182, 90, 146, 203, 208, 69, 151, 204, 79, 61, 166, 160, 3, 202, 83, 9, 175, 52, 63, 45, 15, 25, 1, 2, 253, 215, 237, 19, 229, 185, 238, 2, 9, 1, 217, 121, 146, 12, 250, 112, 198, 239\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 44420000, \n \"ecdhInfo\": [ {\n \"amount\": \"e6d4eb51a032a151\"\n }, {\n \"amount\": \"1050389f0d8eafbe\"\n }], \n \"outPk\": [ \"5f91af7e37907d37de11bae31af65af35e1fabe13cece1d503a9987d7f4813c8\", \"ad2b6ef2b5e77281113637ace74d9ac41f16f431a1a49e6d8de1b73fe877c5c3\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"fcaac875ca31629041923a1bb014f86a232227c41e1611f5961a0e095c6201ea\", \n \"A1\": \"34a7b7367b0045c3a841d57ebac2a9b323ab21f6d954f4441fc79fd98414e5c4\", \n \"B\": \"acdbff571108da31d0face012eb149b24bc16de858b4cb8d96fdc0bb614cc289\", \n \"r1\": \"5a6859cdeb086e647983308714da41be9ada21abfbec1ed4d224315017dacf01\", \n \"s1\": \"c5b2a59d18ac5c3b81c9bfd5c031b9929c1dc802a22593bdea39612039601c0e\", \n \"d1\": \"09f64702dde1507e3daef5655b0f1f32e19fcbcbaeea6b495fed05543cbb6501\", \n \"L\": [ \"30de65cd66a314cfdbe7474a387045b3000dd43eedc021ed492075d314da6d8c\", \"6a3905275d41cdc8758e258c4a71a64d2ba1aec68b7ad68018aa8fdcf9753889\", \"8c61392ded8e0715ddd471638be54eda62622f5787cafc577da4ff7dda015789\", \"82328c51f59ad3d9218eb0d3201d1136d54e7567e15c3f8bc956772bee20f597\", \"6b0f343096ab4a0c2b68099bca4d61eff7a078c91875483213f4cd226b587b5c\", \"12bf7a41abc9079e274e6229187f4c3cc1a8579f60f2a8112aafa78eaefa765e\", \"d7588be97d471720979fa5b907c5b83be30d62d5a2b0b9a59f1330dface4cddd\"\n ], \n \"R\": [ \"f591829caac227efef5fe5076e3fa93dc9a787be8f57c3d2ec216342784321c8\", \"0b956f44dec2d484500371f9a4fdad1de571f16d2cccca13f2f3bb65718dc4a8\", \"61276d08d11bc72536b787537aa0b26d68462500baa1b5b47a1ff669346481ac\", \"5c0d2199d6197dfc9c74cdd6adf13e06223af430e48bcafce9cd8765ae6411d5\", \"a3ff2c8827ca2fb9ec63cfd0c84c2e1cc76d2fb0a3f9619034adc3d0fa60b729\", \"fa3352433a1f4f2c7bbb51fc61673b61833f70d8700446442d57e0a6fde600fc\", \"1cd0d659f8b6b6ca8e320395b831d2b79b95d006c2fc5afb72635535ce1e953d\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"9e70a0022ac9091cb5810450d72edf9bff63c2b64933e0d69881b6ae9c9bc402\", \"b11bcd2ca24cea5171ce4040398ba42f87dec9791ac7376adce1cb47be22bc0b\", \"a083395b214bf88c5e81357eb95b461c1ea4c814357b5fa7dda0b7083a336008\", \"9af604f25927d738dfb3806a559285f04435a9245356051d27cbb0f5c020f406\", \"12a4ed7e2d124a8d6dc7d2e39127f5d66bbcad0bd8af4cf173b89283d09c610e\", \"c53ffb5b2e0aaedad8700de5555decda90d9b1f022ec0f1cabe3627ea89bb804\", \"20d73a60f7f33886541626e5aa0cb758bc9775a80c2427bd9fd373ce1492f90e\", \"93d0cd063f1233975d5ae4d732970c686b21850cd4146e5775af2a3b48f6920f\", \"021bc91e59b79200bacd2ebef0e2045ae01d7287f14a3ef08de813492f92c603\", \"4a7ef9b7a74660125a9761379d3495eb8cbd5e0461a0ae90ac7bb4dd6ee57902\", \"158dab0fbe495663b5536ac17e444ea5d6a5ec67f2145d5ac6eb033fe3ed8802\", \"3133d514073957b3ab9506b375b52ab4e25df97c81f210d2a5d0ac2fd625c005\", \"22ee9cecb7dc0c84a47eef9338472bfa766f0bf5919be6a5b56bf5b84f1be105\", \"8d570d7ea8f372c5c253c189f006b314d377610bbc9a41fc162b3df3d860d904\", \"c74d0451fccecdcb8c0fdb66f55f10a955f49406f16c6ce397b78af25dcbfa00\", \"1a1cc63df1dfcd2918dba5e64532af7a24f3a95c722815ad2192f488fe8da908\"], \n \"c1\": \"0f8c295fdf955dbfe98666d411605e11598745385d7b639d8aed5b5499ffd007\", \n \"D\": \"143ff548f1f2956da85253ed716d16f7ed1ba3ed100426e2a81dfa2bdd952f06\"\n }, {\n \"s\": [ \"997389359aef4673cff1fcf634c4261c3f8a028c25712896381ef8e88b53ce09\", \"96cd93d9bfc6fa1a578554d1b0767962bbfb88f553d5bb129cf18ed93685b50d\", \"60d8e13ef8c06f14e7f4fb212ac28be059f83bd3c375220c4368d405ecc9f601\", \"a31d48f081ab49014d562c39b464f850af6679daffddb75935f4bdf2d8735a01\", \"3df11848f92dca8088339595f99024bb766c19e863175c0234157738925e4c0f\", \"4b5a83686667e9711547b3a2a96946fc126a026cdfc477de41e6c85835dad80a\", \"1a370f59950b9c9759595425609d6371d41801098202cb87ff96fcfb0247730b\", \"b2178497eeb94f794d151fd5393082ec7a0359b409b7508303493f7497237801\", \"59badacf201cb6bf41691ba3ed894dc4a3b22a3a829ff13a349256379e65b108\", \"b53c62247b27176ae5d22295393e1856372f1a89fa7d364173647ba296b76c04\", \"950e768eec5f38634e8cf3beff55bd7ce266b5bebe5b854a02cad0307b0b6704\", \"33bdbf8bc2631430773465cb091c7a666709285816e6d503acd5e649045d3c07\", \"baad6c71df6b89a0481a1fe7f45f6aa8837625ccb43ed5f9e8e9f7daf2cdee0e\", \"7f1baa61b625d9f5d0c9ee49605d403afa625927e9bf41d7e5d48b454dc1520b\", \"19e1c35dd25fc5dff641ea05bc2b6b5697485f96bd3664f90077c567923d4f04\", \"04107251310935d78e2d06471962f23277b5207500917d528aeef43e2b670c03\"], \n \"c1\": \"e614bd3ee3fd58b486a3d0a2494916785325e6546dec8fb880cc401319a7f90b\", \n \"D\": \"4d3832853ea6e0ae698543e20975eaa9c6606068f2465bade18d110a6937e199\"\n }], \n \"pseudoOuts\": [ \"229fc569e4dbb09f253fdc89279b76b70f3bf61d6808d7fa8ff438c795464101\", \"a97d68d18f240c9d7137f2db1d38013ba94cf478338fa0353b2a5cbf937f00bb\"]\n }\n}", + "weight": 2221 + },{ + "blob_size": 2348, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 56160000, + "id_hash": "9d1bcbdb17d24a4e615a9af7100da671ab34bffc808da978004dcef86ddf831e", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261653, + "max_used_block_height": 3195160, + "max_used_block_id_hash": "2f7b8ca3dbd64cb33f428ece414b2b1cef405cfcd85fab1a70383490cc7ed603", + "receive_time": 1721261653, + "relayed": true, + "tx_blob": "0200020200108df79b209386c61387df3decb508a9bb05acc3028d9902deaf01bf208d05ac06ac09b014fd05b501de0d45a88adb7fcac982f5f4d8367f84e0f205235f58ad997f5dfa4707192fd3d9e0020010ace4d528efdb8009c5f8ca01a6f859b4a204c6bc24e3c306d68a01b1e203d401cbc6038103cb61840ad204a40f368fbc77179fb30bf07073783f6ef08bfb1a8c096e9bd60bb57aead3b0f3663d03000300b4750401a83c37a01ad5e0a9404faacc1356e0b553d17f31e16e8b8570e86cea0003befd3d4f80d897085c68417d4af03a4c2520b5ff75c400d1d018ec0d4617e6770a00037b2e69e0187086e600aa2ee921c61ff6847ebf80512e5f86c0500a889da2e8205c210147aa7f30ddab6c818f008f074a4a20528d522fbed0be3f581a8f574e697876a00680dee31a0a24de88aa0fcb7e567bad3c08c3a7247b06d68e46efce9940d6036d4b2b7e03c383c26989ac5176cef29aa6592d688747a7bc1989d88311aaad63d60db25d64d04ed7d40ebbbd4b6adeff521ddc60c9b00594381c89b9d9e4afabcff8a906fe5120b70bd38328753d4cee997f92552087ba220aa427758f010c8c7c9eb2e43443be439372d50e3cad3141de924fa118bad635f8105a086eb741d4b609c9a7fca073984dd6b9561b8a5dd0bc1fefd32a839ba25fa34b2c3d5021cd4b2157a936ecd28cbd4ad243876c84a0c09447b0ecfcf216c7c9f7ae6ec8c58d694f185dc9c4d4115e5d8d58dbffec407c39cd455c5842410c92ff9c0107359836b8b462f81e20852f2b14ad81c2931a6bc41097d824d175310f15d7890a99ac8ab0cf3e9cf0048da193a3a7706c824dbc98d3be3c7aee69d296c519380d085c64adc25186867f568afbd0ab351aeaebda94ff0832ea71771c255ff165669495211b42b692e6f6f745365f45baa6af535b6bf4868eb9699d7fd7b5d8a00a4c35bb4a24827cfc8d6f77f56a907a20dc80437a3f9cd3a4a889ad30eae65bfe3481af3a7f398933896be7bb06bce96c64113c3da8732c4f28f0a83b12ebc7ec329627128e8f6332b0c89fc1f18850786f594a7ef566a2970a4c308d5a0a7649dd81890916da41de7d8f74349fdea603f529d6aa77747ef2bda70d02688b4f121399f25ddbed7f3d2fe2cb136daa069b3548c9f0736057b68e249165662f8782f6cdbfe3e6a9eee2623dcb25eb492ad6ad825ae9917453c2772643e8bcb44b8122085661c1ea7185fdabc273bd62dffd938e1bee1b95c6a5940900331254bdb7f0361d66af8aa68615444bc98631b9cbee7ad6fb2c64fa92e42b472b7098acf2564206e152560f919105aee65e4116fd730f6201e639b585a0886262aef0e65db50b3413e399527b6a031081a272c0bdc89516b55107d186038fd8b2641690dbed6148e3084de6599e25bda1c719290dc31742b6863d3fcc57980ccb808215dd17a4b949f725231f2c29eabf7487e7c93bdf92cc5786c5614e77b2d2d10d9b9ccd475a2bf8992c52219480706e5676b40c203ee6799bca4636aeb7011875eb13dc2da7e6a777c7ccf246a19195e7aa4d7a617f0e5b7797a13228abfa6d288799b17702e263aa90ab2883591877afd734279ccf3442e40437401e4a6875b6603bb10db2503ff62c27a6ee89ba54efff4a15c149aae234cbdd165b6acd87f8f68d0408c3357e22b6ac83ea40186091d41ee82a467e6eedfb81065e1a6261493572820bb3a02732c48afe03a04344e108a16c6ed9b57f8bc70c0373c14e02aa58c39d081b9d63df16411a24065419e149dd5cf0d16c361d52077e372668658217ae0008052f944bf07460dc2650b271abbd3470f9d9f1151915b47f4e843c2cb66e8104b954b9b11711f668d700a7e8e73bba0e1f3325c7ee168c2e52fbbc42a9a37c0a5acfbd247117d89d3dec678cd2f1e5a358adba45cd96eb83bdefa574fe7ee70462e723e878f558b4ff133353015d12039d88ff4e4a42542274a29eec9d48f606f819b255f796a383db4db031d966f0512c0be65bcb570be40c5afeb301ced60bbd96cb11101317b582c5a4938f4b42aa4e4764923917fe03a849783821052701b601b95768bf304145239a0dcad54850a49431706b58139f92ab1aff4083ce021da8fbb5e19ef43bdcb40a61b67d615fdcc7d1ef9937efe0f64c65a6fc7d3a0f656bc19201f87d279c7dec96160b8cd0001f5064609b3561259728e19b5da20e57181f2d5e36ab9edc5fe93b71fde30f262d4e60525df697dcd66d45f079da0e5d95974c8bff7e89d06a64c25714eaa1b65522218c996c7ddb6cf902519f340524e084e6b195f994bc9be4cbfd8e6e9c5a1e00551148e1f8b6c154b2115b1f088c5ab9747f5d6f49a819f0eb06a4277090292c3b14f219196d91a3d8333b11278b5ca04440eb42f46cca567d8a2c3e9fd2880078544e47a6e932a828b494a7056038560559de1bdefeda1e9da672b6e931f22d530a7dd7ff8586184b5d593d0ebed9c5263818225fb9db1e64cbfc0174f872a3ea5e92f314c87b96daa8e7a400742ea90a1c357194d610b998a0c07dbad2bd2541158439be096b8f59ad7dcc073345072ecdb181af27eecaa21642d23abca25c1cd6a45daec5d423c8c002f60a99e4218d23ae7bd8055f9d473a8de7a36d5c00c64eea4be99c751655ab904604992a602c49416d56179835e3c6c5d0e89aca53a372dc61cfd9d105c7c0bdeb02765e41608cec6275ba3b7c19f2dcecd8cf7c02eed764b953066535c9395ba50ea8e8223b185992e646d89d4ab71009b3244dd2c2949d98557b21ad8cd2f03d0f140613a8e1366fe9d73e2282e471274b7304b94dc5a99a84411c5504b86176042351e695642aa3608fb3c413e7221b153d2ba3c0f8adca0d9636b3f95922ed040c19b92c35505041a39b8c9492c08577c9689bebbf1955827ab7298779d9d50b861f800ae0baa6761569a92e7b74d2d9dde4d8c98ffb37737edba691f4c94a0c78ef2c281ffe9c1d7f290cc7c60efd992f2b436c73b04f920984a1e8b8256a053d938f9f385b823aeee4f760e7c4346a3be6921700c433664d6806deaa3a6c039a1dc227d0fb1d07b26156b4b998053b2d07e037c024eb305212852edea364010936723eed3db539fd792c5acfe56bf2444998a107cff83eb7e37504b05c990f78111416918e784305b77030f0e2a14124afce9bf4d285c09a77264b2cf85e7fe99253575d3a7dbca07223afc25c085bc2e45aea9b399d58339b4ac1edfcd5e9ef14d6f64ab94d4f94eca4765dfabac9c91a423283387f51301a31d35de5b61d", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 67566477, 40993555, 1011591, 137964, 89513, 41388, 35981, 22494, 4159, 653, 812, 1196, 2608, 765, 181, 1758\n ], \n \"k_image\": \"45a88adb7fcac982f5f4d8367f84e0f205235f58ad997f5dfa4707192fd3d9e0\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 85291564, 18886127, 3324997, 1473574, 69940, 597574, 106979, 17750, 61745, 212, 58187, 385, 12491, 1284, 594, 1956\n ], \n \"k_image\": \"368fbc77179fb30bf07073783f6ef08bfb1a8c096e9bd60bb57aead3b0f3663d\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"00b4750401a83c37a01ad5e0a9404faacc1356e0b553d17f31e16e8b8570e86c\", \n \"view_tag\": \"ea\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"befd3d4f80d897085c68417d4af03a4c2520b5ff75c400d1d018ec0d4617e677\", \n \"view_tag\": \"0a\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"7b2e69e0187086e600aa2ee921c61ff6847ebf80512e5f86c0500a889da2e820\", \n \"view_tag\": \"5c\"\n }\n }\n }\n ], \n \"extra\": [ 1, 71, 170, 127, 48, 221, 171, 108, 129, 143, 0, 143, 7, 74, 74, 32, 82, 141, 82, 47, 190, 208, 190, 63, 88, 26, 143, 87, 78, 105, 120, 118, 160\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 56160000, \n \"ecdhInfo\": [ {\n \"amount\": \"0a24de88aa0fcb7e\"\n }, {\n \"amount\": \"567bad3c08c3a724\"\n }, {\n \"amount\": \"7b06d68e46efce99\"\n }], \n \"outPk\": [ \"40d6036d4b2b7e03c383c26989ac5176cef29aa6592d688747a7bc1989d88311\", \"aaad63d60db25d64d04ed7d40ebbbd4b6adeff521ddc60c9b00594381c89b9d9\", \"e4afabcff8a906fe5120b70bd38328753d4cee997f92552087ba220aa427758f\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"0c8c7c9eb2e43443be439372d50e3cad3141de924fa118bad635f8105a086eb7\", \n \"A1\": \"41d4b609c9a7fca073984dd6b9561b8a5dd0bc1fefd32a839ba25fa34b2c3d50\", \n \"B\": \"21cd4b2157a936ecd28cbd4ad243876c84a0c09447b0ecfcf216c7c9f7ae6ec8\", \n \"r1\": \"c58d694f185dc9c4d4115e5d8d58dbffec407c39cd455c5842410c92ff9c0107\", \n \"s1\": \"359836b8b462f81e20852f2b14ad81c2931a6bc41097d824d175310f15d7890a\", \n \"d1\": \"99ac8ab0cf3e9cf0048da193a3a7706c824dbc98d3be3c7aee69d296c519380d\", \n \"L\": [ \"5c64adc25186867f568afbd0ab351aeaebda94ff0832ea71771c255ff1656694\", \"95211b42b692e6f6f745365f45baa6af535b6bf4868eb9699d7fd7b5d8a00a4c\", \"35bb4a24827cfc8d6f77f56a907a20dc80437a3f9cd3a4a889ad30eae65bfe34\", \"81af3a7f398933896be7bb06bce96c64113c3da8732c4f28f0a83b12ebc7ec32\", \"9627128e8f6332b0c89fc1f18850786f594a7ef566a2970a4c308d5a0a7649dd\", \"81890916da41de7d8f74349fdea603f529d6aa77747ef2bda70d02688b4f1213\", \"99f25ddbed7f3d2fe2cb136daa069b3548c9f0736057b68e249165662f8782f6\", \"cdbfe3e6a9eee2623dcb25eb492ad6ad825ae9917453c2772643e8bcb44b8122\"\n ], \n \"R\": [ \"5661c1ea7185fdabc273bd62dffd938e1bee1b95c6a5940900331254bdb7f036\", \"1d66af8aa68615444bc98631b9cbee7ad6fb2c64fa92e42b472b7098acf25642\", \"06e152560f919105aee65e4116fd730f6201e639b585a0886262aef0e65db50b\", \"3413e399527b6a031081a272c0bdc89516b55107d186038fd8b2641690dbed61\", \"48e3084de6599e25bda1c719290dc31742b6863d3fcc57980ccb808215dd17a4\", \"b949f725231f2c29eabf7487e7c93bdf92cc5786c5614e77b2d2d10d9b9ccd47\", \"5a2bf8992c52219480706e5676b40c203ee6799bca4636aeb7011875eb13dc2d\", \"a7e6a777c7ccf246a19195e7aa4d7a617f0e5b7797a13228abfa6d288799b177\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"02e263aa90ab2883591877afd734279ccf3442e40437401e4a6875b6603bb10d\", \"b2503ff62c27a6ee89ba54efff4a15c149aae234cbdd165b6acd87f8f68d0408\", \"c3357e22b6ac83ea40186091d41ee82a467e6eedfb81065e1a6261493572820b\", \"b3a02732c48afe03a04344e108a16c6ed9b57f8bc70c0373c14e02aa58c39d08\", \"1b9d63df16411a24065419e149dd5cf0d16c361d52077e372668658217ae0008\", \"052f944bf07460dc2650b271abbd3470f9d9f1151915b47f4e843c2cb66e8104\", \"b954b9b11711f668d700a7e8e73bba0e1f3325c7ee168c2e52fbbc42a9a37c0a\", \"5acfbd247117d89d3dec678cd2f1e5a358adba45cd96eb83bdefa574fe7ee704\", \"62e723e878f558b4ff133353015d12039d88ff4e4a42542274a29eec9d48f606\", \"f819b255f796a383db4db031d966f0512c0be65bcb570be40c5afeb301ced60b\", \"bd96cb11101317b582c5a4938f4b42aa4e4764923917fe03a849783821052701\", \"b601b95768bf304145239a0dcad54850a49431706b58139f92ab1aff4083ce02\", \"1da8fbb5e19ef43bdcb40a61b67d615fdcc7d1ef9937efe0f64c65a6fc7d3a0f\", \"656bc19201f87d279c7dec96160b8cd0001f5064609b3561259728e19b5da20e\", \"57181f2d5e36ab9edc5fe93b71fde30f262d4e60525df697dcd66d45f079da0e\", \"5d95974c8bff7e89d06a64c25714eaa1b65522218c996c7ddb6cf902519f3405\"], \n \"c1\": \"24e084e6b195f994bc9be4cbfd8e6e9c5a1e00551148e1f8b6c154b2115b1f08\", \n \"D\": \"8c5ab9747f5d6f49a819f0eb06a4277090292c3b14f219196d91a3d8333b1127\"\n }, {\n \"s\": [ \"8b5ca04440eb42f46cca567d8a2c3e9fd2880078544e47a6e932a828b494a705\", \"6038560559de1bdefeda1e9da672b6e931f22d530a7dd7ff8586184b5d593d0e\", \"bed9c5263818225fb9db1e64cbfc0174f872a3ea5e92f314c87b96daa8e7a400\", \"742ea90a1c357194d610b998a0c07dbad2bd2541158439be096b8f59ad7dcc07\", \"3345072ecdb181af27eecaa21642d23abca25c1cd6a45daec5d423c8c002f60a\", \"99e4218d23ae7bd8055f9d473a8de7a36d5c00c64eea4be99c751655ab904604\", \"992a602c49416d56179835e3c6c5d0e89aca53a372dc61cfd9d105c7c0bdeb02\", \"765e41608cec6275ba3b7c19f2dcecd8cf7c02eed764b953066535c9395ba50e\", \"a8e8223b185992e646d89d4ab71009b3244dd2c2949d98557b21ad8cd2f03d0f\", \"140613a8e1366fe9d73e2282e471274b7304b94dc5a99a84411c5504b8617604\", \"2351e695642aa3608fb3c413e7221b153d2ba3c0f8adca0d9636b3f95922ed04\", \"0c19b92c35505041a39b8c9492c08577c9689bebbf1955827ab7298779d9d50b\", \"861f800ae0baa6761569a92e7b74d2d9dde4d8c98ffb37737edba691f4c94a0c\", \"78ef2c281ffe9c1d7f290cc7c60efd992f2b436c73b04f920984a1e8b8256a05\", \"3d938f9f385b823aeee4f760e7c4346a3be6921700c433664d6806deaa3a6c03\", \"9a1dc227d0fb1d07b26156b4b998053b2d07e037c024eb305212852edea36401\"], \n \"c1\": \"0936723eed3db539fd792c5acfe56bf2444998a107cff83eb7e37504b05c990f\", \n \"D\": \"78111416918e784305b77030f0e2a14124afce9bf4d285c09a77264b2cf85e7f\"\n }], \n \"pseudoOuts\": [ \"e99253575d3a7dbca07223afc25c085bc2e45aea9b399d58339b4ac1edfcd5e9\", \"ef14d6f64ab94d4f94eca4765dfabac9c91a423283387f51301a31d35de5b61d\"]\n }\n}", + "weight": 2808 + },{ + "blob_size": 2387, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 116500000, + "id_hash": "dbabb82a5f97d9da58d587c179b5861576144ea0cd14b96bef62b83d8838f363", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261653, + "max_used_block_height": 3195160, + "max_used_block_id_hash": "2f7b8ca3dbd64cb33f428ece414b2b1cef405cfcd85fab1a70383490cc7ed603", + "receive_time": 1721261653, + "relayed": true, + "tx_blob": "020001020010df97d533d1c51185e031f69e08adce05b0aa06fb8a03b137895afb14c08e02b0549c1af60da305cb1f6d80d9c12f1439b0a994f767d71d98d2d2cde1a54c6a6134a00c2f07135d98cf0b0003e205de62fdbf4ff1b6554e3180bfd09051e368d70a5739bc039d864eea9d355da60003dacd0606c42ee249c304393e3169de7baacc28a9ff94240b603c54b0238d542d11000324dffd0f9dfb98b459a463f1c62df0d5ec76ad3781c41d5a65bdce94dc43aa5e5b0003631aff9f6291c9890f02a667c976b7e13bce27d443d00bda47d72d32ca2eacff40000360ba12b274ebbfe8f74abfe132a02a4a91e96f2f5db3b5772e5bab2e1066015959000348e42fd446643b5ab32024d193050ad976173b0ce43cc33add718167b0d99b33ac000356a51fe30876666e834c9741ee4e4fcbd08372005319f58cdc1a46bd98db5dec4b0003421637f078091694b19f1d2d3691a2ba8fe38198a808d0bd38f4202270d5b47dbb0003b42ff611a9b780b8e4afc2958d059013c55512102b3f6d7b6f30f4da75c5bbbb340003c3253ebf1a9125a52b9a7d4fca7d29cc41c701aefd7b8d5caa6ca02a128a0433cf00030fbe7d06b657e84ffa29bcb937a649a647cc1e967fdeb3a1c7a00f2e72faddbbaf21016b391fa654bc1e4141eec1d8a55bfbe363d9632b56dd4c7393b7b1f8934932e506a0ccc6376dad44ee6f7e9686a70f600a18341bd60dcbe99a71bc8a05eb8f5ad01274c1590bb0e0848ccd0872e311b24115067be37fa3244c7785ce295d3eaff88e7bd50450bcfe9aebd902dbf3fd4d3d5f5deaa8f5517e89834007556c99f923978cb868d155d761085d03d11684d6d75b79e461da935fe8b8ccff02aaba98f24626e21abd6b7cee23ff35c800e99b7b8719b7adcf2987f61a51c7e81728cfab8ac5ec5fa22a10dbd58ae64976b188860a88ea9a7ea3ce49cc5460168c355de58788139c74011a95e49b0d91e6e033be4f212573cae4ae8b77c7f59e727f4ff9ef5045de7a733c53eb98c57bf4c643a1183da886f66d7984e4559192c1de69533b7777e5da4290431063f42b9f900f82564deffb967e214eb22c6835fda6a27569c7211b2ccf9602046049c5b72ed13a70193cec07dc1336b609c575b4dd58693376a102246f13508d44c88dec158b3a9b263bf8557818979f7a833f758602ffc62037d78994082ea998cd1b4944ebed5a3cba2f3e19dcc7f9bfc96901a60c2293b2949b0e9d3b84260d2f6085c628ba1a0fbfaab1611bdad0ec39ba599342f1de56d2beb0930aea3ef3f446ebf52bf5c0cc89566a68ca68ad05f9cb012acf619b5da2f005a99b98d6ab1244a4458435fdd4ba5aabab8884eede69ee25d063c5e054a0763bd8e6cad5859a1f54d5d280af664cd2b3ab6a8ba944e2858bc9979f63b12e75b3a647fcb31e8796502947ada7d65526c74f70e2ff4820e599a03200a8bc6475cde069c7e90a9fda27d204a8c01c10706f1fec6c791d6e6d05707f92d85119d8b54b5818e894ddc4e5ce45612e601094aca90fac825b8f13060b9cf42dc96310520835ddff845dd782508fd8b73d99613d425323b206d3ae060a6ae9c2bf3c383f11392df4b0cdb537c62e618cd97de259135abc4de9fc3fa1e9d64eaa642cdd7d6a6e3f2208ed76771b2be4a2416e53c15f1e3bd7eee3ec3e92f6ceb61873e03d5ac5f2bf583fb5f86453310e232a2620a6bfa4cfb4981b533b8625b08e0938291ba52a9850629e3097bfef1b3764c73643ade61a8d425d3f2da30df56d9fd7c736efe223b158ed49114e1eb8e616a2ef47d673c8ed52b2d94c5c25ebab35e7e0512aa8d9a48c01ece39a0dd29125cafa904e1a33cca56d64ea06551895fa1f1691674216e56eca2ac8ba84da58eac436746fe39e028815dd812f19d8236dadccbc23378bece2a78a8cf6ce42a40d7e97ac095f2fbe3c769eb5674359a42c69eba31b8952c54fcc90ef3f141acb32d9f4f7d88c0d416d541aa2300560059f8f8ed209d935b3e26185f387ebc5f0ac4a279b661a97f13da023010afcc6f7d3542ee9511acf4ff6782d5f516ebcb08f76ef089cfbca6012001893bb8ef584c963ec50d1249f063d3a138a836e98fd3c845b15d97e819e60fc8b7298bb52ac2e31690570e777e3a18b0d2d30e9219d4c53ad018c05f9150963045dfacd472e1d89929f4df50429794c4214bd02cf7ef7a1dbac9bd7d882748c36ade1c99a664e1ebf3b88c9f2af6b0facadce56c11ee6e7f9bb642d612952642a2e584bc487bae947cac5301fb28e8635b282d5aeb999ac21fe3060a351649c00ed98646e663edab73c1d545771f1e85919b2d89ba44149a086e188ecaec21f5ecebe04198c506cae6da01a2413e414d2aaa0309f16c8afb17b146577f41fc383957d8ab02506d67795f41c6709c5cba79a14fa32842c4e38e9d97b0c69500f9dffcf78e6aa8232f46e7f6b2e402d0a5ee548a0e503be9b789a08803e0970f2d745249fbc34dc464b3ae4d5ba8b98946e3201fb1d9185507aae2a77e129394c8b64071d658e5eff25510bd2becae400f2db0a865a4a6fd55fbe6ea016f1370eab2d0090eddd7545194e0b3fce7cab06c968a6bd17cc848f5d3d20a26f5dccd004e803839d366680832ed1290ed6469f0a75a5ef9a35c426381a9372d4f0cddd77370d506ec1d7573950192844f4f3fe1089077dc6faa84d6513eb75621f5ade8eff0bceba99fa0b988daa13fe0c7fd15d85f050a339d447581c8459db9833952e710ae6c97b50495765c8d034d821da26bb950c6d379b923ba2482ae700098579400f3337c5898f002ab7832e258e3ead08da9c71bcde6449a5cacd506b6977549b08ffcdd6d90ce4f0b06e36f0b1c61055211284292df52d88532ade8962e3679f0e313298b6a931e790e641d5176247e5dcfa715134624e4bca5dfd92259cacd907014bf9138373942890180ea291a80e2594d0c062d1826dce12d4167bb10cd30a90cd211a857b80f7c1887c1ce14aa8124ede4b989f3520cce7011978b955cf030c76902a5b22867f9c3d47e8523c9cfd8a4d97ad2592f1debea8c30a7856bf04a435b617fd6b6a78ee5bd65e354f1b6ca5a21fc66d399e4debb8c2a97acc07048967d4c0fa15746f102312f0fd1f8ea851ee133282cba0f1f439408f2742df0af040c7e3ec8e5a0deeb6e952316fa300d986c29986cfb6293b90f5dac8a9760e2c8178c818aa7a44292f3129578d6bead21638d57b020709f39379f3b579cc0ae6aa3044eb12eb2487b34119b5f3ed92fc020406eaa991cf0a51c60cb1b7789afa8610e6d02955b7063068ebc5b481d70f39e570e880cd8551eaa333670fb213", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 108350431, 287441, 815109, 135030, 91949, 103728, 50555, 7089, 11529, 2683, 34624, 10800, 3356, 1782, 675, 4043\n ], \n \"k_image\": \"6d80d9c12f1439b0a994f767d71d98d2d2cde1a54c6a6134a00c2f07135d98cf\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"e205de62fdbf4ff1b6554e3180bfd09051e368d70a5739bc039d864eea9d355d\", \n \"view_tag\": \"a6\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"dacd0606c42ee249c304393e3169de7baacc28a9ff94240b603c54b0238d542d\", \n \"view_tag\": \"11\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"24dffd0f9dfb98b459a463f1c62df0d5ec76ad3781c41d5a65bdce94dc43aa5e\", \n \"view_tag\": \"5b\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"631aff9f6291c9890f02a667c976b7e13bce27d443d00bda47d72d32ca2eacff\", \n \"view_tag\": \"40\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"60ba12b274ebbfe8f74abfe132a02a4a91e96f2f5db3b5772e5bab2e10660159\", \n \"view_tag\": \"59\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"48e42fd446643b5ab32024d193050ad976173b0ce43cc33add718167b0d99b33\", \n \"view_tag\": \"ac\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"56a51fe30876666e834c9741ee4e4fcbd08372005319f58cdc1a46bd98db5dec\", \n \"view_tag\": \"4b\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"421637f078091694b19f1d2d3691a2ba8fe38198a808d0bd38f4202270d5b47d\", \n \"view_tag\": \"bb\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"b42ff611a9b780b8e4afc2958d059013c55512102b3f6d7b6f30f4da75c5bbbb\", \n \"view_tag\": \"34\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"c3253ebf1a9125a52b9a7d4fca7d29cc41c701aefd7b8d5caa6ca02a128a0433\", \n \"view_tag\": \"cf\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"0fbe7d06b657e84ffa29bcb937a649a647cc1e967fdeb3a1c7a00f2e72faddbb\", \n \"view_tag\": \"af\"\n }\n }\n }\n ], \n \"extra\": [ 1, 107, 57, 31, 166, 84, 188, 30, 65, 65, 238, 193, 216, 165, 91, 251, 227, 99, 217, 99, 43, 86, 221, 76, 115, 147, 183, 177, 248, 147, 73, 50, 229\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 116500000, \n \"ecdhInfo\": [ {\n \"amount\": \"6dad44ee6f7e9686\"\n }, {\n \"amount\": \"a70f600a18341bd6\"\n }, {\n \"amount\": \"0dcbe99a71bc8a05\"\n }, {\n \"amount\": \"eb8f5ad01274c159\"\n }, {\n \"amount\": \"0bb0e0848ccd0872\"\n }, {\n \"amount\": \"e311b24115067be3\"\n }, {\n \"amount\": \"7fa3244c7785ce29\"\n }, {\n \"amount\": \"5d3eaff88e7bd504\"\n }, {\n \"amount\": \"50bcfe9aebd902db\"\n }, {\n \"amount\": \"f3fd4d3d5f5deaa8\"\n }, {\n \"amount\": \"f5517e8983400755\"\n }], \n \"outPk\": [ \"6c99f923978cb868d155d761085d03d11684d6d75b79e461da935fe8b8ccff02\", \"aaba98f24626e21abd6b7cee23ff35c800e99b7b8719b7adcf2987f61a51c7e8\", \"1728cfab8ac5ec5fa22a10dbd58ae64976b188860a88ea9a7ea3ce49cc546016\", \"8c355de58788139c74011a95e49b0d91e6e033be4f212573cae4ae8b77c7f59e\", \"727f4ff9ef5045de7a733c53eb98c57bf4c643a1183da886f66d7984e4559192\", \"c1de69533b7777e5da4290431063f42b9f900f82564deffb967e214eb22c6835\", \"fda6a27569c7211b2ccf9602046049c5b72ed13a70193cec07dc1336b609c575\", \"b4dd58693376a102246f13508d44c88dec158b3a9b263bf8557818979f7a833f\", \"758602ffc62037d78994082ea998cd1b4944ebed5a3cba2f3e19dcc7f9bfc969\", \"01a60c2293b2949b0e9d3b84260d2f6085c628ba1a0fbfaab1611bdad0ec39ba\", \"599342f1de56d2beb0930aea3ef3f446ebf52bf5c0cc89566a68ca68ad05f9cb\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"2acf619b5da2f005a99b98d6ab1244a4458435fdd4ba5aabab8884eede69ee25\", \n \"A1\": \"d063c5e054a0763bd8e6cad5859a1f54d5d280af664cd2b3ab6a8ba944e2858b\", \n \"B\": \"c9979f63b12e75b3a647fcb31e8796502947ada7d65526c74f70e2ff4820e599\", \n \"r1\": \"a03200a8bc6475cde069c7e90a9fda27d204a8c01c10706f1fec6c791d6e6d05\", \n \"s1\": \"707f92d85119d8b54b5818e894ddc4e5ce45612e601094aca90fac825b8f1306\", \n \"d1\": \"0b9cf42dc96310520835ddff845dd782508fd8b73d99613d425323b206d3ae06\", \n \"L\": [ \"6ae9c2bf3c383f11392df4b0cdb537c62e618cd97de259135abc4de9fc3fa1e9\", \"d64eaa642cdd7d6a6e3f2208ed76771b2be4a2416e53c15f1e3bd7eee3ec3e92\", \"f6ceb61873e03d5ac5f2bf583fb5f86453310e232a2620a6bfa4cfb4981b533b\", \"8625b08e0938291ba52a9850629e3097bfef1b3764c73643ade61a8d425d3f2d\", \"a30df56d9fd7c736efe223b158ed49114e1eb8e616a2ef47d673c8ed52b2d94c\", \"5c25ebab35e7e0512aa8d9a48c01ece39a0dd29125cafa904e1a33cca56d64ea\", \"06551895fa1f1691674216e56eca2ac8ba84da58eac436746fe39e028815dd81\", \"2f19d8236dadccbc23378bece2a78a8cf6ce42a40d7e97ac095f2fbe3c769eb5\", \"674359a42c69eba31b8952c54fcc90ef3f141acb32d9f4f7d88c0d416d541aa2\", \"300560059f8f8ed209d935b3e26185f387ebc5f0ac4a279b661a97f13da02301\"\n ], \n \"R\": [ \"fcc6f7d3542ee9511acf4ff6782d5f516ebcb08f76ef089cfbca6012001893bb\", \"8ef584c963ec50d1249f063d3a138a836e98fd3c845b15d97e819e60fc8b7298\", \"bb52ac2e31690570e777e3a18b0d2d30e9219d4c53ad018c05f9150963045dfa\", \"cd472e1d89929f4df50429794c4214bd02cf7ef7a1dbac9bd7d882748c36ade1\", \"c99a664e1ebf3b88c9f2af6b0facadce56c11ee6e7f9bb642d612952642a2e58\", \"4bc487bae947cac5301fb28e8635b282d5aeb999ac21fe3060a351649c00ed98\", \"646e663edab73c1d545771f1e85919b2d89ba44149a086e188ecaec21f5ecebe\", \"04198c506cae6da01a2413e414d2aaa0309f16c8afb17b146577f41fc383957d\", \"8ab02506d67795f41c6709c5cba79a14fa32842c4e38e9d97b0c69500f9dffcf\", \"78e6aa8232f46e7f6b2e402d0a5ee548a0e503be9b789a08803e0970f2d74524\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"9fbc34dc464b3ae4d5ba8b98946e3201fb1d9185507aae2a77e129394c8b6407\", \"1d658e5eff25510bd2becae400f2db0a865a4a6fd55fbe6ea016f1370eab2d00\", \"90eddd7545194e0b3fce7cab06c968a6bd17cc848f5d3d20a26f5dccd004e803\", \"839d366680832ed1290ed6469f0a75a5ef9a35c426381a9372d4f0cddd77370d\", \"506ec1d7573950192844f4f3fe1089077dc6faa84d6513eb75621f5ade8eff0b\", \"ceba99fa0b988daa13fe0c7fd15d85f050a339d447581c8459db9833952e710a\", \"e6c97b50495765c8d034d821da26bb950c6d379b923ba2482ae700098579400f\", \"3337c5898f002ab7832e258e3ead08da9c71bcde6449a5cacd506b6977549b08\", \"ffcdd6d90ce4f0b06e36f0b1c61055211284292df52d88532ade8962e3679f0e\", \"313298b6a931e790e641d5176247e5dcfa715134624e4bca5dfd92259cacd907\", \"014bf9138373942890180ea291a80e2594d0c062d1826dce12d4167bb10cd30a\", \"90cd211a857b80f7c1887c1ce14aa8124ede4b989f3520cce7011978b955cf03\", \"0c76902a5b22867f9c3d47e8523c9cfd8a4d97ad2592f1debea8c30a7856bf04\", \"a435b617fd6b6a78ee5bd65e354f1b6ca5a21fc66d399e4debb8c2a97acc0704\", \"8967d4c0fa15746f102312f0fd1f8ea851ee133282cba0f1f439408f2742df0a\", \"f040c7e3ec8e5a0deeb6e952316fa300d986c29986cfb6293b90f5dac8a9760e\"], \n \"c1\": \"2c8178c818aa7a44292f3129578d6bead21638d57b020709f39379f3b579cc0a\", \n \"D\": \"e6aa3044eb12eb2487b34119b5f3ed92fc020406eaa991cf0a51c60cb1b7789a\"\n }], \n \"pseudoOuts\": [ \"fa8610e6d02955b7063068ebc5b481d70f39e570e880cd8551eaa333670fb213\"]\n }\n}", + "weight": 5817 + },{ + "blob_size": 1664, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 42480000, + "id_hash": "3fd963b931b1ac20e3709ba0249143fe8cff4856200055336ba9330970e6306a", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261660, + "max_used_block_height": 3195160, + "max_used_block_id_hash": "2f7b8ca3dbd64cb33f428ece414b2b1cef405cfcd85fab1a70383490cc7ed603", + "receive_time": 1721261660, + "relayed": true, + "tx_blob": "020001020010cfc19c31d9c65acfdb1bcfb91dd6e00aa7c2b601e2b51489e01ab3cd038e99039aa705c0f806ba2ffc1da40ba002913f889441c829e62c741c27614cdbb6278555b768fbd583424e1bb45c65e43b030003ad696cce55ac392f061b7e0f1acccbd17cbdfc11d20805df2ee8efb087476ed59c000357483f9fa7379f9b72e6149fd2b319c4e60653a627d0107d73f5604159d597ebf000034360e4f9cfb1109148e0b8428c36fa62270e4aa68e6aabd768270363b53a195acc210173c64bf5cd3e9f401a3800c9624c45f8e9408163348c4e2c473b88f610874eff0680e3a01479bce24f3573dbafaae47cc85d8723551119c984e226cab7daa8193188e5b32952ea0a464812c5f1189db475fe78ad4a6351e7d335a79163d9bd1c7b680e62a0ef36e83b122965bf6042bef9cf535da9286ea586681c4bef225c49f8f54b6b1a92a4a2d53c699fd021e1d2cf9ae75729ba7cf244a0264cf901fd2a5b8b2a2016880735d531b55fa6e206cc0c0d5177c93d10e2f60fadde3bebebf7ff3558e9949869a8401f4174a6e2e6092074d44b2665008391bcef3be3b38dcbb6f45f57379ba40acdffb81ffc8a3d312c6b728eff17a8c8f15d49f1517aca615e2f8bc5257f7b7a27cd455c633a400b6ff63f58d7c66c1172e1e56ef30e4edf1f17a43bd06754bcc657391acef09cbc15d6b827e61cfa8f6e33a4f7ce0837c0edf5a192c739ca6c8da976af9a1fa2ca1649024639f6c26e09dd01489b0a08e64e75e0dd89bd999cdaf72f1334263b2daa2b27b593951c8ea7781d7f4479fa8b24c4e3e7d02de85eb0144aa0e26be30c4a283db27c5b488ee4895345f81a51b54741caf457eef595c9753aa58d82d45182325aea6d77198090e8e14fd413620fcb3a7750dcb87178431ed8b4b23f39fd893c1e500dce31de0bf1861540e212e1263d567951d7c73e3f082a9fd7086aba9b58fbbbf61783fda9d037afa96250fe234734c186422495f5d5db92690958e43e5fd5ec5e302537d3353950f5a8e2a65a13ea58137481ebc02768f12ba06d4c3a58cc3f2725567a544434acc7934c87eff281268d9495670dc73032cab96056aeeda57fb16e153074dbe5b490ee2608d4ee6684886fec29e4f03741b6cfeb7bf468eca760fdf20085df50309f6942585ded84a963d18448029d3990a43ad3ba730b70193b7195961375390863ef0e721f26495ea684c129a7bfe0b58837269c2f3b9ed5697b4e5f50205e1699086db0d89e441c287e7aff567135d73a6724f206a15909cf4505c99a1ece8d2d44197ec9c2d28ca55008ba1819f7092e694e0048502dd937674b825e8325ccf06d2257e50dccf6d450f07a040def2b0af67d7a9ca8d54179bf474b11f3e0342b92e6f0d5c2c5b01dd7769f3651c5a5b7e4af34428cfd9429ecf373cc409db223a3f81075b8d10b0e409336f9922b0acc60780ed8c7e6916eafa813e486f678133c7c287040847e31fae2ed636fc2830394ecfef0c93ae4a380394907018dfdcf9814083efcaee92919e11f36ec098917436902e5752d62d43e054178b9cb6a66e7d4087b234f9f3fff46cdec0fd531ab50b82ffbbcf22171250fd8ce8015e6ccb940022ee8c5d7a0d10186b416c51c3370b0300f54651accfbebd3ac3e4a22ebbf2409b5e7ebc40dae1be188c5208668d6216d1cdb0552541c27fc6b9f770aa6fa2e0f50eedef2110162cd67b027348beb3865d40b71dfcd4bb02caa5d85454868f8071d6fda35f3729b3fdf7041ae02302733ed00dafdbb91e81d304daa7a99e0350ed8967cea2637fddda63bb273ed406be7ee694bff1fb5a0cdbf792fe25da78d0be68bff0add3f8868ece66332a446415da39994ed247606a7951ccbe639006008a2796c33bb8d53f64de030b7618deac7e15fc6b53bea8f04b30d244007e6b805a5c6a79cfa5e75c89eb96b298fe5f3722c9ab5219b839a6a88999d288913d9025d07125c47cc210f19bf42dc7edd84d62fd4d6ebfba9d865c8909477d5aa98047b12e6430d55c9f19d9ee5fe6020cb0fd4604ff5999ef3e8904d6016f99334035c1a93efd452b75925209e0eabbefade7d92f8ed581135d87f791170b3dd7b024eb818dba8ccfb23e897c6edc986ac47cd95292156e50c06797f9d9e552e3e01ac75a709909f7650bb41125bda22a768d3673bb7a28cf418514fbde82c3cc7035c483ea223a5c017b42873aca5381b7d4926c47d6ac62e8289a4bfe1f72751049b04cbacc98c9796300170897711eab28348eb4c73fc31ba3fc82d0043a67d628e9f3dc440a410d0dbe2c8c3c46caf95165cc82f3716f56fd0a6c3b187e1f892", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 103227599, 1483609, 454095, 482511, 176214, 2990375, 334562, 438281, 59059, 52366, 86938, 113728, 6074, 3836, 1444, 288\n ], \n \"k_image\": \"913f889441c829e62c741c27614cdbb6278555b768fbd583424e1bb45c65e43b\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"ad696cce55ac392f061b7e0f1acccbd17cbdfc11d20805df2ee8efb087476ed5\", \n \"view_tag\": \"9c\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"57483f9fa7379f9b72e6149fd2b319c4e60653a627d0107d73f5604159d597eb\", \n \"view_tag\": \"f0\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"4360e4f9cfb1109148e0b8428c36fa62270e4aa68e6aabd768270363b53a195a\", \n \"view_tag\": \"cc\"\n }\n }\n }\n ], \n \"extra\": [ 1, 115, 198, 75, 245, 205, 62, 159, 64, 26, 56, 0, 201, 98, 76, 69, 248, 233, 64, 129, 99, 52, 140, 78, 44, 71, 59, 136, 246, 16, 135, 78, 255\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 42480000, \n \"ecdhInfo\": [ {\n \"amount\": \"79bce24f3573dbaf\"\n }, {\n \"amount\": \"aae47cc85d872355\"\n }, {\n \"amount\": \"1119c984e226cab7\"\n }], \n \"outPk\": [ \"daa8193188e5b32952ea0a464812c5f1189db475fe78ad4a6351e7d335a79163\", \"d9bd1c7b680e62a0ef36e83b122965bf6042bef9cf535da9286ea586681c4bef\", \"225c49f8f54b6b1a92a4a2d53c699fd021e1d2cf9ae75729ba7cf244a0264cf9\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"fd2a5b8b2a2016880735d531b55fa6e206cc0c0d5177c93d10e2f60fadde3beb\", \n \"A1\": \"ebf7ff3558e9949869a8401f4174a6e2e6092074d44b2665008391bcef3be3b3\", \n \"B\": \"8dcbb6f45f57379ba40acdffb81ffc8a3d312c6b728eff17a8c8f15d49f1517a\", \n \"r1\": \"ca615e2f8bc5257f7b7a27cd455c633a400b6ff63f58d7c66c1172e1e56ef30e\", \n \"s1\": \"4edf1f17a43bd06754bcc657391acef09cbc15d6b827e61cfa8f6e33a4f7ce08\", \n \"d1\": \"37c0edf5a192c739ca6c8da976af9a1fa2ca1649024639f6c26e09dd01489b0a\", \n \"L\": [ \"e64e75e0dd89bd999cdaf72f1334263b2daa2b27b593951c8ea7781d7f4479fa\", \"8b24c4e3e7d02de85eb0144aa0e26be30c4a283db27c5b488ee4895345f81a51\", \"b54741caf457eef595c9753aa58d82d45182325aea6d77198090e8e14fd41362\", \"0fcb3a7750dcb87178431ed8b4b23f39fd893c1e500dce31de0bf1861540e212\", \"e1263d567951d7c73e3f082a9fd7086aba9b58fbbbf61783fda9d037afa96250\", \"fe234734c186422495f5d5db92690958e43e5fd5ec5e302537d3353950f5a8e2\", \"a65a13ea58137481ebc02768f12ba06d4c3a58cc3f2725567a544434acc7934c\", \"87eff281268d9495670dc73032cab96056aeeda57fb16e153074dbe5b490ee26\"\n ], \n \"R\": [ \"d4ee6684886fec29e4f03741b6cfeb7bf468eca760fdf20085df50309f694258\", \"5ded84a963d18448029d3990a43ad3ba730b70193b7195961375390863ef0e72\", \"1f26495ea684c129a7bfe0b58837269c2f3b9ed5697b4e5f50205e1699086db0\", \"d89e441c287e7aff567135d73a6724f206a15909cf4505c99a1ece8d2d44197e\", \"c9c2d28ca55008ba1819f7092e694e0048502dd937674b825e8325ccf06d2257\", \"e50dccf6d450f07a040def2b0af67d7a9ca8d54179bf474b11f3e0342b92e6f0\", \"d5c2c5b01dd7769f3651c5a5b7e4af34428cfd9429ecf373cc409db223a3f810\", \"75b8d10b0e409336f9922b0acc60780ed8c7e6916eafa813e486f678133c7c28\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"7040847e31fae2ed636fc2830394ecfef0c93ae4a380394907018dfdcf981408\", \"3efcaee92919e11f36ec098917436902e5752d62d43e054178b9cb6a66e7d408\", \"7b234f9f3fff46cdec0fd531ab50b82ffbbcf22171250fd8ce8015e6ccb94002\", \"2ee8c5d7a0d10186b416c51c3370b0300f54651accfbebd3ac3e4a22ebbf2409\", \"b5e7ebc40dae1be188c5208668d6216d1cdb0552541c27fc6b9f770aa6fa2e0f\", \"50eedef2110162cd67b027348beb3865d40b71dfcd4bb02caa5d85454868f807\", \"1d6fda35f3729b3fdf7041ae02302733ed00dafdbb91e81d304daa7a99e0350e\", \"d8967cea2637fddda63bb273ed406be7ee694bff1fb5a0cdbf792fe25da78d0b\", \"e68bff0add3f8868ece66332a446415da39994ed247606a7951ccbe639006008\", \"a2796c33bb8d53f64de030b7618deac7e15fc6b53bea8f04b30d244007e6b805\", \"a5c6a79cfa5e75c89eb96b298fe5f3722c9ab5219b839a6a88999d288913d902\", \"5d07125c47cc210f19bf42dc7edd84d62fd4d6ebfba9d865c8909477d5aa9804\", \"7b12e6430d55c9f19d9ee5fe6020cb0fd4604ff5999ef3e8904d6016f9933403\", \"5c1a93efd452b75925209e0eabbefade7d92f8ed581135d87f791170b3dd7b02\", \"4eb818dba8ccfb23e897c6edc986ac47cd95292156e50c06797f9d9e552e3e01\", \"ac75a709909f7650bb41125bda22a768d3673bb7a28cf418514fbde82c3cc703\"], \n \"c1\": \"5c483ea223a5c017b42873aca5381b7d4926c47d6ac62e8289a4bfe1f7275104\", \n \"D\": \"9b04cbacc98c9796300170897711eab28348eb4c73fc31ba3fc82d0043a67d62\"\n }], \n \"pseudoOuts\": [ \"8e9f3dc440a410d0dbe2c8c3c46caf95165cc82f3716f56fd0a6c3b187e1f892\"]\n }\n}", + "weight": 2124 + },{ + "blob_size": 1537, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 491840000, + "id_hash": "8a6ebea82ede84b743c256c050a40ae7999e67b443c06ccb2322db626d62d970", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261651, + "max_used_block_height": 3195153, + "max_used_block_id_hash": "8d15c2bf99e9a1c5a0513dca2106c5bcd81d94aa07c1c2d17d7ea96883cd1158", + "receive_time": 1721261651, + "relayed": true, + "tx_blob": "020001020010f783dd2ee9ec9804c7ca2ed7dd3aea990fd3fa0ae8c00edcfe1bee950493de03ba8302b494048114ab6ac7b70110a7f204f932169b1b056fc63be06db8ec91a436f7188a30545bcd6a8bae817ca4020003aa62b55146dd60777113d2c62c8bc41c9339b63e758513d57d5ada453786995ab00003b71531f0476ff381570af6bff1ca3bc8bf8286a098a939e32eaee36450895bef412c019ee82c35546584a78c1713407439e107e98c4da5e6735d77f5d61a369bbf4e2b020901c8c9a284894d6ce90680c4c3ea011d42ea4108a9eb4c47d3233193a71c74ed6c841d99c6eaad2a605daac8f0cdcb6b75c30f2a4aec0e955b2fd93b2cf65bb62d9b9ae29de54a88a5157484d03bd04b300ad7516fb0dc7c647b3ce7f2b2c301e106566731515046dbcf50b5e8f6edeea6bbd2e945b4eb49bc121da137e276481e0265fb162e11730623ead0848e5abfb5007e24282d4a5dc95ad1332e383bfd3d403de9cc283ef16eac799ea9ecfffd24b0a0faed955a5ce82f2a717f5b30bba3cc22335eb0edcfc3b2af623db3309981e8f39722accfc7fd215bd3f1f6a60f476ca5eb56798e464d5791a1cd5d57fbe2d2fbf1c6824a3e80e48d3fa807240fa78029fdf2795b8bc4f0b5d5336ebaa65c179534ec8038dec26c8b5cc103230607d277a8b482900c60791c03c6a072eb576cdf8532b2da3bb493872c264559a7832f46f076e6b541c665df3baaf4486d9466655a26e3aea8f9d6e4d3f5613f4dbcb5c1cc78c9698551ed28ebaccebba9ffe10b38c4791afa73c620cf433342e45733b26f192642f6498a0d482df12e605b91957614d4a6707d0c103883104210508812038852ef55bf0ceed35382c508d19b82bacb5891bf9070c4634bbdb229c22c4e1296d55ccad33aac94342e8f75acee853fa05e69fe1f545c3b4ef839b401f7dd53846c9a9d8b1b0b04282f802fa451475b86d39edc01368a86cf9382a037075bba459244e0dc6488730bc628330b776bf8c6ababb3046ee3b103fcc2d9d544cd92ea335770c68c0e6f385105299bffd1ae17a12fe4b40e6880a9c6f5038daed1464b0fee68ecdfcdc712405936422918197dfec8f4351b82e7da04f6bcb9c765924afbd077c168ee0306cf456e1f4606086e2155260b3f5e9d8443533de0bb2fa410fc5465603b16c0137a236f761fb254e7b53c8667d8ba931e41bf6ed5cd131a4171a265158843415ca2f5c1690c9ec431091f3312b7b308420878e7e9118bed3c3f7df4e5acc3bd7d209d3539ce182e551af5f5e6369d29367dfa97c8951cfa0ba11a23b5c5e1bf6cd59db4b658b1437652f6e3f25717c9f4f522b37a0767c193fdbbdb9dada9d5d46e38a7d8a42460c0bf37c7adc0a317fd3e4a29390de895b526f0368b4737af0746647e2025a10cae52d452400710c9709e2b375c09c73b2529c97d87b8e62f15d0c776dbb0f5564cde6fb5872c124193d47d92ac007bfab088ebd9a44f194597802213475438f21112a50cb4f02c7f650ecfd3290f1c9bffdd70877dbc77cd5485cfbec143687cf605938787c1426ed42646a1f0080757ba370ba2b36b492a71708e36736db7857405cae3b128879d8e9c7dff030b3fb734e6faf49c85ed5d7940b400375b12674b3858d31b7ab56222e82ab03e00ce265d7d694571d3db08a7f23baf68007c4aa67f31185aeaba276eb3688b26018d8c904b9deea0fd68ceeebdf1aac9d967aec1ebaa4d3c6df2c3ce7c874ace05a3cb5af380be76fd67a7fc64d7da5a3ad94350b295a86cf7943c05c12f8267017e2beada8abf3e0fbb104492c222bbd8a6058dbdb05042213ed077c0d36cbf0cc6f1333a49e977ef452cfba812ba428b19cca4ea461681abce4f707e8f691b06fa6c56667fae356bcfa47f8f74f1db6bb470ce8fb96815e3fa2aaf2d22d1b00f4487e8e83e98b7d879c1699fd6f1092b1a9b5827effc22f14e3c7bf59ef2fc0b9eaf15677934da8c39016b3f771dd9227e76f541818cd4ca03d3bc78bd18cc0ee9dea19ceb194f06dc893c192932468ef107461f70a03b4d9137b72958ef68031ae7ae8bfa3fcc4d0f335959b7ac162dd86aae3da1580817d5fa6f932fe3c5511d1585d0f1383e6c1035c5ace104f8b03f27958c9160375a80498f8b73f861c4", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 97993207, 8795753, 763207, 962263, 249066, 179539, 237672, 458588, 68334, 61203, 33210, 68148, 2561, 13611, 23495, 16\n ], \n \"k_image\": \"a7f204f932169b1b056fc63be06db8ec91a436f7188a30545bcd6a8bae817ca4\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"aa62b55146dd60777113d2c62c8bc41c9339b63e758513d57d5ada453786995a\", \n \"view_tag\": \"b0\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"b71531f0476ff381570af6bff1ca3bc8bf8286a098a939e32eaee36450895bef\", \n \"view_tag\": \"41\"\n }\n }\n }\n ], \n \"extra\": [ 1, 158, 232, 44, 53, 84, 101, 132, 167, 140, 23, 19, 64, 116, 57, 225, 7, 233, 140, 77, 165, 230, 115, 93, 119, 245, 214, 26, 54, 155, 191, 78, 43, 2, 9, 1, 200, 201, 162, 132, 137, 77, 108, 233\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 491840000, \n \"ecdhInfo\": [ {\n \"amount\": \"1d42ea4108a9eb4c\"\n }, {\n \"amount\": \"47d3233193a71c74\"\n }], \n \"outPk\": [ \"ed6c841d99c6eaad2a605daac8f0cdcb6b75c30f2a4aec0e955b2fd93b2cf65b\", \"b62d9b9ae29de54a88a5157484d03bd04b300ad7516fb0dc7c647b3ce7f2b2c3\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"e106566731515046dbcf50b5e8f6edeea6bbd2e945b4eb49bc121da137e27648\", \n \"A1\": \"1e0265fb162e11730623ead0848e5abfb5007e24282d4a5dc95ad1332e383bfd\", \n \"B\": \"3d403de9cc283ef16eac799ea9ecfffd24b0a0faed955a5ce82f2a717f5b30bb\", \n \"r1\": \"a3cc22335eb0edcfc3b2af623db3309981e8f39722accfc7fd215bd3f1f6a60f\", \n \"s1\": \"476ca5eb56798e464d5791a1cd5d57fbe2d2fbf1c6824a3e80e48d3fa807240f\", \n \"d1\": \"a78029fdf2795b8bc4f0b5d5336ebaa65c179534ec8038dec26c8b5cc1032306\", \n \"L\": [ \"d277a8b482900c60791c03c6a072eb576cdf8532b2da3bb493872c264559a783\", \"2f46f076e6b541c665df3baaf4486d9466655a26e3aea8f9d6e4d3f5613f4dbc\", \"b5c1cc78c9698551ed28ebaccebba9ffe10b38c4791afa73c620cf433342e457\", \"33b26f192642f6498a0d482df12e605b91957614d4a6707d0c10388310421050\", \"8812038852ef55bf0ceed35382c508d19b82bacb5891bf9070c4634bbdb229c2\", \"2c4e1296d55ccad33aac94342e8f75acee853fa05e69fe1f545c3b4ef839b401\", \"f7dd53846c9a9d8b1b0b04282f802fa451475b86d39edc01368a86cf9382a037\"\n ], \n \"R\": [ \"5bba459244e0dc6488730bc628330b776bf8c6ababb3046ee3b103fcc2d9d544\", \"cd92ea335770c68c0e6f385105299bffd1ae17a12fe4b40e6880a9c6f5038dae\", \"d1464b0fee68ecdfcdc712405936422918197dfec8f4351b82e7da04f6bcb9c7\", \"65924afbd077c168ee0306cf456e1f4606086e2155260b3f5e9d8443533de0bb\", \"2fa410fc5465603b16c0137a236f761fb254e7b53c8667d8ba931e41bf6ed5cd\", \"131a4171a265158843415ca2f5c1690c9ec431091f3312b7b308420878e7e911\", \"8bed3c3f7df4e5acc3bd7d209d3539ce182e551af5f5e6369d29367dfa97c895\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"1cfa0ba11a23b5c5e1bf6cd59db4b658b1437652f6e3f25717c9f4f522b37a07\", \"67c193fdbbdb9dada9d5d46e38a7d8a42460c0bf37c7adc0a317fd3e4a29390d\", \"e895b526f0368b4737af0746647e2025a10cae52d452400710c9709e2b375c09\", \"c73b2529c97d87b8e62f15d0c776dbb0f5564cde6fb5872c124193d47d92ac00\", \"7bfab088ebd9a44f194597802213475438f21112a50cb4f02c7f650ecfd3290f\", \"1c9bffdd70877dbc77cd5485cfbec143687cf605938787c1426ed42646a1f008\", \"0757ba370ba2b36b492a71708e36736db7857405cae3b128879d8e9c7dff030b\", \"3fb734e6faf49c85ed5d7940b400375b12674b3858d31b7ab56222e82ab03e00\", \"ce265d7d694571d3db08a7f23baf68007c4aa67f31185aeaba276eb3688b2601\", \"8d8c904b9deea0fd68ceeebdf1aac9d967aec1ebaa4d3c6df2c3ce7c874ace05\", \"a3cb5af380be76fd67a7fc64d7da5a3ad94350b295a86cf7943c05c12f826701\", \"7e2beada8abf3e0fbb104492c222bbd8a6058dbdb05042213ed077c0d36cbf0c\", \"c6f1333a49e977ef452cfba812ba428b19cca4ea461681abce4f707e8f691b06\", \"fa6c56667fae356bcfa47f8f74f1db6bb470ce8fb96815e3fa2aaf2d22d1b00f\", \"4487e8e83e98b7d879c1699fd6f1092b1a9b5827effc22f14e3c7bf59ef2fc0b\", \"9eaf15677934da8c39016b3f771dd9227e76f541818cd4ca03d3bc78bd18cc0e\"], \n \"c1\": \"e9dea19ceb194f06dc893c192932468ef107461f70a03b4d9137b72958ef6803\", \n \"D\": \"1ae7ae8bfa3fcc4d0f335959b7ac162dd86aae3da1580817d5fa6f932fe3c551\"\n }], \n \"pseudoOuts\": [ \"1d1585d0f1383e6c1035c5ace104f8b03f27958c9160375a80498f8b73f861c4\"]\n }\n}", + "weight": 1537 + },{ + "blob_size": 1534, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 122720000, + "id_hash": "7c32ac906393a55797b17efef623ca9577ba5e3d26c1cf54231dcf06459eff81", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261673, + "max_used_block_height": 3195144, + "max_used_block_id_hash": "464cb0e47663a64ee8eaf483c46d6584e9a7945a0c792b19cdbde426ec3a5034", + "receive_time": 1721261673, + "relayed": true, + "tx_blob": "0200010200108088a024a3deeb0ef5939001e7eb0fc19401cab201aeaf01e08001cd16f4a601f52a8c8d01a106d811c83e5e987605d678e8bfb17e8d2651e8dd5c69c73c705d003c82e4e35d2b5b89c9ebe3020003507533a540f57548b44e011305bd39d6a6a64e5ccc82bac1833b6ededb9121f3690003b32a2175124a8e0a0fd95a37d51a290a397dc74637a8cdd73ba4dfdec91b2c0ec42c0159ef5288bf4ea3ba0f6780416feedec329416b7fd78af51f494f3eb89db900a7020901810cdc8e9ce7315906809ec23ab7e164d1d01bfc65b2f30e77c390a5fd68d73b4565628faee8a1f9bd7cffe31a3bdb37ef7fc670e18f6190cc20459a32ac6e15f56e257ed3471f4bbf5394501db831fa2d05b784f3e5091f322c1df20601326379bcc24fdcb0e68dbca4cbfac5fc5238e31b4ea8619c5eff8776a42e9c9a88287c0c5fc24c5a1872237e971762f9ec12853ca27eb3b1e611d3713bdd4778bc8585ef0adb3e8f144518a22d2eb4e77c526335c5ad9f5c4754740eaba4623f59cce4ea9eab890ec0976f3e03bc08f3218192c56d82c36ddc4692f5a96f8b056f11c7f15635ba12a22274a0a171999444f21079496b016867b7dac9f05f5f0141b181816c004f0f3033d7e0a9819f5a8623a637dc7d949b34c0da6eba6a7d07073c7219d079608c6ff8d04b0d4bb425b73142e84750e7e5548ef86cc02ac0b91132c3e4df24b11a32f8c30a483bcd5b903b2cd87197d8172bd4bd190c4034322dabd82600f852c346716518c93c439a799a2763ff9d2457c47f96e22371440cc1db354184fddabde2d51512556b7d05ad6be9f44fbf1671834f6fd45a6f8f1f09edf96551160fe83c207fb8eabab30ed294aca1287ea196f1640af3a183cc7891a680264c41b34d56ab4219e8175b1d847de3174d4c298cb5ac40e1c84169b0a5fbee401b237ad92093cb34b6752229e03cc7ceb2546102360ca5fe823192024b0775b50b22fe9e7cfb29fec3400ea8a03b047ca1c92af76d14da40589415116b922b080f61446f1e5a11aaaa84acd4bee2e67f25c8a0c77db1e3cf8d9a2f57a1f6a622a4c76475ecb20fb1b6221caa4be32876414e6d6b0375582fbf9c1d50403981d303b53d877af580443431499ed7a030d01a618c37139ac4fec11b3a2afa1aee51a3605abd36c3cc05c02348430bdec52a481e04249a7adde0a4b718d459ee1aa67e4ce05980a451753b0e9b7dd543b373a137cd900f81929699bfa4fd5bd51d096c93673bc035031bf18d1e153a5b62e5965f4865827d7c871403fcda46a7e38ec6b2c7c7de6f6e88e7e32514fdbcf04bfdc851a1a052ad32052ca4a74f039ca030a488b3f3160043add8d6b6f7f5275c49ca6c5f3e6556641b5d08c80f053adcb2928105c91712723592a72f37c0da5ec12f62325cf8ce9a9d98244c830715d805c79b3e0d09ff094563869a28864beaf3d2e4a257f2ac7e05b253f84801fa53fc02c27fb618a582eeb68261458ff5a24f19db62368984e35c13a9b3b10de92a20b1f757574390e8a9e6cf58a33e6f2d7425dd5cff36f15b5992cca1690cedb97159b128e4e509bb7735bbfab47d0e925103bd69585fd60a772b9fbf300bde8d80256510c267a7fba688969b4e89e06d8faaa7b1369cdbba78b19520fd0a8e3e0858e4ca6457e9444053ef08c62efdf1eb2176e81261c5f2b1febf72f4047ef2981ea380eea8a4429fc107fa6f96bb9a60feb70db7cda063be92d3f650098f85e14b1a45dab1687f1e2d4e3b11099286945c997c1cc2f9f908367ce5af0d7fc4f2f770cce3184f98be164dfcd15b896c5290e46dbc2e1458683022d9980d87afe5ff98453b33f1ce50617b33ebe0565bfe0166c1af28cee2ff788e623c0614b3db3ca8b04ae0ca9fc6a036ba40269a3da9cbfd2cd1ce2c8690a6df13ed00cc5527e60edd183603ffc5fb3fec14ee3e6c24bd708192756a2f10d6c3d54102ba4b38a06f9ca37fe3e10c68a4a6bd3858015f2b18f82e7e446a347c488890046b48a02d406ce2f4ae39ab70fa019f4c5086aa861f73f8b343eb433873c52e0901d1794138c64ea8943d6763d334920c5d57014a4ff9b2e163abcd9af5da269cb8dd672e123f737a896c99551b4c610bc68bb68b30ba5206881e18fd2288a42f", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 76022784, 31125283, 2361845, 259559, 19009, 22858, 22446, 16480, 2893, 21364, 5493, 18060, 801, 2264, 8008, 94\n ], \n \"k_image\": \"987605d678e8bfb17e8d2651e8dd5c69c73c705d003c82e4e35d2b5b89c9ebe3\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"507533a540f57548b44e011305bd39d6a6a64e5ccc82bac1833b6ededb9121f3\", \n \"view_tag\": \"69\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"b32a2175124a8e0a0fd95a37d51a290a397dc74637a8cdd73ba4dfdec91b2c0e\", \n \"view_tag\": \"c4\"\n }\n }\n }\n ], \n \"extra\": [ 1, 89, 239, 82, 136, 191, 78, 163, 186, 15, 103, 128, 65, 111, 238, 222, 195, 41, 65, 107, 127, 215, 138, 245, 31, 73, 79, 62, 184, 157, 185, 0, 167, 2, 9, 1, 129, 12, 220, 142, 156, 231, 49, 89\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 122720000, \n \"ecdhInfo\": [ {\n \"amount\": \"b7e164d1d01bfc65\"\n }, {\n \"amount\": \"b2f30e77c390a5fd\"\n }], \n \"outPk\": [ \"68d73b4565628faee8a1f9bd7cffe31a3bdb37ef7fc670e18f6190cc20459a32\", \"ac6e15f56e257ed3471f4bbf5394501db831fa2d05b784f3e5091f322c1df206\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"326379bcc24fdcb0e68dbca4cbfac5fc5238e31b4ea8619c5eff8776a42e9c9a\", \n \"A1\": \"88287c0c5fc24c5a1872237e971762f9ec12853ca27eb3b1e611d3713bdd4778\", \n \"B\": \"bc8585ef0adb3e8f144518a22d2eb4e77c526335c5ad9f5c4754740eaba4623f\", \n \"r1\": \"59cce4ea9eab890ec0976f3e03bc08f3218192c56d82c36ddc4692f5a96f8b05\", \n \"s1\": \"6f11c7f15635ba12a22274a0a171999444f21079496b016867b7dac9f05f5f01\", \n \"d1\": \"41b181816c004f0f3033d7e0a9819f5a8623a637dc7d949b34c0da6eba6a7d07\", \n \"L\": [ \"3c7219d079608c6ff8d04b0d4bb425b73142e84750e7e5548ef86cc02ac0b911\", \"32c3e4df24b11a32f8c30a483bcd5b903b2cd87197d8172bd4bd190c4034322d\", \"abd82600f852c346716518c93c439a799a2763ff9d2457c47f96e22371440cc1\", \"db354184fddabde2d51512556b7d05ad6be9f44fbf1671834f6fd45a6f8f1f09\", \"edf96551160fe83c207fb8eabab30ed294aca1287ea196f1640af3a183cc7891\", \"a680264c41b34d56ab4219e8175b1d847de3174d4c298cb5ac40e1c84169b0a5\", \"fbee401b237ad92093cb34b6752229e03cc7ceb2546102360ca5fe823192024b\"\n ], \n \"R\": [ \"75b50b22fe9e7cfb29fec3400ea8a03b047ca1c92af76d14da40589415116b92\", \"2b080f61446f1e5a11aaaa84acd4bee2e67f25c8a0c77db1e3cf8d9a2f57a1f6\", \"a622a4c76475ecb20fb1b6221caa4be32876414e6d6b0375582fbf9c1d504039\", \"81d303b53d877af580443431499ed7a030d01a618c37139ac4fec11b3a2afa1a\", \"ee51a3605abd36c3cc05c02348430bdec52a481e04249a7adde0a4b718d459ee\", \"1aa67e4ce05980a451753b0e9b7dd543b373a137cd900f81929699bfa4fd5bd5\", \"1d096c93673bc035031bf18d1e153a5b62e5965f4865827d7c871403fcda46a7\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"e38ec6b2c7c7de6f6e88e7e32514fdbcf04bfdc851a1a052ad32052ca4a74f03\", \"9ca030a488b3f3160043add8d6b6f7f5275c49ca6c5f3e6556641b5d08c80f05\", \"3adcb2928105c91712723592a72f37c0da5ec12f62325cf8ce9a9d98244c8307\", \"15d805c79b3e0d09ff094563869a28864beaf3d2e4a257f2ac7e05b253f84801\", \"fa53fc02c27fb618a582eeb68261458ff5a24f19db62368984e35c13a9b3b10d\", \"e92a20b1f757574390e8a9e6cf58a33e6f2d7425dd5cff36f15b5992cca1690c\", \"edb97159b128e4e509bb7735bbfab47d0e925103bd69585fd60a772b9fbf300b\", \"de8d80256510c267a7fba688969b4e89e06d8faaa7b1369cdbba78b19520fd0a\", \"8e3e0858e4ca6457e9444053ef08c62efdf1eb2176e81261c5f2b1febf72f404\", \"7ef2981ea380eea8a4429fc107fa6f96bb9a60feb70db7cda063be92d3f65009\", \"8f85e14b1a45dab1687f1e2d4e3b11099286945c997c1cc2f9f908367ce5af0d\", \"7fc4f2f770cce3184f98be164dfcd15b896c5290e46dbc2e1458683022d9980d\", \"87afe5ff98453b33f1ce50617b33ebe0565bfe0166c1af28cee2ff788e623c06\", \"14b3db3ca8b04ae0ca9fc6a036ba40269a3da9cbfd2cd1ce2c8690a6df13ed00\", \"cc5527e60edd183603ffc5fb3fec14ee3e6c24bd708192756a2f10d6c3d54102\", \"ba4b38a06f9ca37fe3e10c68a4a6bd3858015f2b18f82e7e446a347c48889004\"], \n \"c1\": \"6b48a02d406ce2f4ae39ab70fa019f4c5086aa861f73f8b343eb433873c52e09\", \n \"D\": \"01d1794138c64ea8943d6763d334920c5d57014a4ff9b2e163abcd9af5da269c\"\n }], \n \"pseudoOuts\": [ \"b8dd672e123f737a896c99551b4c610bc68bb68b30ba5206881e18fd2288a42f\"]\n }\n}", + "weight": 1534 + },{ + "blob_size": 1535, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 491200000, + "id_hash": "63b7d903d41ab2605043be9df08eb45b752727bf7a02d0d686c823d5863d7d83", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261665, + "max_used_block_height": 3195155, + "max_used_block_id_hash": "c8ad671ebe68cc5244ea7aed3a70f13682c3e93fc21321abcb20609d42a5b6e7", + "receive_time": 1721261665, + "relayed": true, + "tx_blob": "020001020010d6e18c2ffcc1e40483f90be9b30b9af70af8ac0dc3bc099313ccbb01c79f04aa25946bc7a102ac37c30a8d1d563cd0f22a17177353e494beb070af0f53ed6d003ada32123c7ec3c23f681393020003416ee9d85c13be6a1ad2a0b9a5c3fad790bc3c266cd0eb55f1d38959a2eef8a49b0003d6292302f486945eb9bab6beaa2564a4dbe09cf8e92a107eb8734f0f8a09a1c9052c017ce27d3675d2db5af9968b93733350b10749e9fa4a0c1bfcfdc86c550088fdd1020901c6821af937903f5f0680bc9cea01e240eebed50fd3fd162b7b3b54185d5ecb8253de6123d50449a2746c6a82023347509df7efd5f8106b4e3d60dd0d7f86958d05ee5bc8a6963af900849d2a4118e031bcaeb2788aa2a1e56b036ebadab201d92af1585cb9cb3db78d3da41c66221180e1c8128a35b304ae96de328ce538d160dd66e00a9b57988589eb48c4e131b24bb266de41540a164b34008c591871cd93fe7c3fa4aacd67f35315c0927e7b4add9c63e94a95732b7f12cf1346b7b26e1d1d893d582b2b0787f00f331e787749b14a3e0cb5363537cb4654d90a4c90001e08066b76e5b7cb3d4a9a58b88c613f5b4ca2fb0a875becee6a26f287d14906f3be11a310ff9a3bdcb579818bb958229c1b0ab3d4ce9b935723bdf8c888190d071089e5724f75ce14fe58360ba419f7e7bda58a0175b9ce9b12627b599ae896d060e7a21f3b9d65cf4d386aa6ac044e2283c64b92f4b3f4234ad7e1036fa96f53f187ed53d0c753805df8de748339f72e901ca157aeabff55f14c451f0084e725a55ffc85d7ba5d26a7f678bbbcd1e40e8a3a5400620ea4eee86796b7ec2266b20f8fd361a94759654d7d39ba8cdd6b3a002f140dcefe0f498a6b33655bfb2981ac38db4dd0cc6b7c25d408bbe69fb7cb86114fd7aea4136aacfe15475f20f3d4f7cd51eedec4e4a1890fcc0d29f18d6270a84d9c4aa6ba6bf9ec3900016057240713ad161ed5d9dd3a852ad64f7183a57b83015ab461f36e5ac1ed0bb0d2f7e1ff6c37cb4ef43c6e65f11858490d29b6fff50e91acfb2799fa5b8c0c9e9f913f2941ffa2414ca459db6de293f9e8a231152f6fbdb1dcb2a79bebb5687870f69ec96254f56d963bc8e283a7fdf1fbdf4f60d5d97d4224d9f1a9d4c52c14ba4ffabf5dd9955c0e075da49a8f8d84686d4213331b64a5f770fa35260ff1e1e13ec9da2b7aa35b728810febb734410cb117e37040d2a3c3198b816272e8e76fca9199064b01511b86f7578718590b6ffedb78a5ae175e4efeb3ed71028913deb2e52e5f17d3792990cec53ff4b834616d77cd50e32c84b95422b8436645c304b22c2018c6308ab20f4f7e06c5f67bd59c6f106fabe2b14dc1713a4b13f6522aa74f40410fd14ef905febb4e95ccd80265803c37e285ed3939d43e368b20b9e49b955041dd3d895f4885e8264d99f574d4da3e94b785e5679e300b817e15f1c60228f0745d6faf106d0d36d000be6d5779d6d83165dec7ae167ed7fe6ef7a688f391b0e93c15448214aa121b110d4573e9f386b432451f5689b29a81cec1497ba9c17022f3c310407533b549210dc47186bf117beb821a3ac0d255a31bc57c795b4bd02982a42b4ce5d5a86dade14e37ef9a9a4870687800d2d8ae637cad458e3c7a80ed3e47cc7f6af7523f1ed6bdbbd3f4eaeb6fb9cc9ae13a905618821ca41f13902430b77134bc29f5319b96b17c9fda24cc72af61a890b422bbcd10fd323001605dd25ae17c9c1492fb180e3ac206e780db485c3f8940e1d80301e962fb3384d05d187fdf65cf5b1e2ad3e379f5a765773a3f196bc0b835ad305946328ace26502dc43c5790a5ca6c3076206b56bb1c4bc53bfb4b0fba11dfa24809087e6a6760d7475a3c2465b02f2068d356289d4f17ee35fc0956deb061a737fb4bde998320cfd3e85d3e5113062b0b49f860318d909c9cb714758e6203a2af2ee69e5bf58032014aafdf920276f5a50ca4c3a1b4b7c7f7099ea414e0982bc806beeb68bd303b51723220ce63175d0ba8577400c59109d97357692b9ace8ec0a57992585ed07bea256456a8600ec85716f9bf7dae9cd0cf88ccc2c542600e12c41c7403cf4056db57e4fe2422adcc28c3ec0a02dbb6b1b5926d4012ec3f27ad757faeae60cad", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 98775254, 10035452, 195715, 186857, 179098, 218744, 155203, 2451, 24012, 69575, 4778, 13716, 37063, 7084, 1347, 3725\n ], \n \"k_image\": \"563cd0f22a17177353e494beb070af0f53ed6d003ada32123c7ec3c23f681393\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"416ee9d85c13be6a1ad2a0b9a5c3fad790bc3c266cd0eb55f1d38959a2eef8a4\", \n \"view_tag\": \"9b\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"d6292302f486945eb9bab6beaa2564a4dbe09cf8e92a107eb8734f0f8a09a1c9\", \n \"view_tag\": \"05\"\n }\n }\n }\n ], \n \"extra\": [ 1, 124, 226, 125, 54, 117, 210, 219, 90, 249, 150, 139, 147, 115, 51, 80, 177, 7, 73, 233, 250, 74, 12, 27, 252, 253, 200, 108, 85, 0, 136, 253, 209, 2, 9, 1, 198, 130, 26, 249, 55, 144, 63, 95\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 491200000, \n \"ecdhInfo\": [ {\n \"amount\": \"e240eebed50fd3fd\"\n }, {\n \"amount\": \"162b7b3b54185d5e\"\n }], \n \"outPk\": [ \"cb8253de6123d50449a2746c6a82023347509df7efd5f8106b4e3d60dd0d7f86\", \"958d05ee5bc8a6963af900849d2a4118e031bcaeb2788aa2a1e56b036ebadab2\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"d92af1585cb9cb3db78d3da41c66221180e1c8128a35b304ae96de328ce538d1\", \n \"A1\": \"60dd66e00a9b57988589eb48c4e131b24bb266de41540a164b34008c591871cd\", \n \"B\": \"93fe7c3fa4aacd67f35315c0927e7b4add9c63e94a95732b7f12cf1346b7b26e\", \n \"r1\": \"1d1d893d582b2b0787f00f331e787749b14a3e0cb5363537cb4654d90a4c9000\", \n \"s1\": \"1e08066b76e5b7cb3d4a9a58b88c613f5b4ca2fb0a875becee6a26f287d14906\", \n \"d1\": \"f3be11a310ff9a3bdcb579818bb958229c1b0ab3d4ce9b935723bdf8c888190d\", \n \"L\": [ \"1089e5724f75ce14fe58360ba419f7e7bda58a0175b9ce9b12627b599ae896d0\", \"60e7a21f3b9d65cf4d386aa6ac044e2283c64b92f4b3f4234ad7e1036fa96f53\", \"f187ed53d0c753805df8de748339f72e901ca157aeabff55f14c451f0084e725\", \"a55ffc85d7ba5d26a7f678bbbcd1e40e8a3a5400620ea4eee86796b7ec2266b2\", \"0f8fd361a94759654d7d39ba8cdd6b3a002f140dcefe0f498a6b33655bfb2981\", \"ac38db4dd0cc6b7c25d408bbe69fb7cb86114fd7aea4136aacfe15475f20f3d4\", \"f7cd51eedec4e4a1890fcc0d29f18d6270a84d9c4aa6ba6bf9ec390001605724\"\n ], \n \"R\": [ \"13ad161ed5d9dd3a852ad64f7183a57b83015ab461f36e5ac1ed0bb0d2f7e1ff\", \"6c37cb4ef43c6e65f11858490d29b6fff50e91acfb2799fa5b8c0c9e9f913f29\", \"41ffa2414ca459db6de293f9e8a231152f6fbdb1dcb2a79bebb5687870f69ec9\", \"6254f56d963bc8e283a7fdf1fbdf4f60d5d97d4224d9f1a9d4c52c14ba4ffabf\", \"5dd9955c0e075da49a8f8d84686d4213331b64a5f770fa35260ff1e1e13ec9da\", \"2b7aa35b728810febb734410cb117e37040d2a3c3198b816272e8e76fca91990\", \"64b01511b86f7578718590b6ffedb78a5ae175e4efeb3ed71028913deb2e52e5\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"f17d3792990cec53ff4b834616d77cd50e32c84b95422b8436645c304b22c201\", \"8c6308ab20f4f7e06c5f67bd59c6f106fabe2b14dc1713a4b13f6522aa74f404\", \"10fd14ef905febb4e95ccd80265803c37e285ed3939d43e368b20b9e49b95504\", \"1dd3d895f4885e8264d99f574d4da3e94b785e5679e300b817e15f1c60228f07\", \"45d6faf106d0d36d000be6d5779d6d83165dec7ae167ed7fe6ef7a688f391b0e\", \"93c15448214aa121b110d4573e9f386b432451f5689b29a81cec1497ba9c1702\", \"2f3c310407533b549210dc47186bf117beb821a3ac0d255a31bc57c795b4bd02\", \"982a42b4ce5d5a86dade14e37ef9a9a4870687800d2d8ae637cad458e3c7a80e\", \"d3e47cc7f6af7523f1ed6bdbbd3f4eaeb6fb9cc9ae13a905618821ca41f13902\", \"430b77134bc29f5319b96b17c9fda24cc72af61a890b422bbcd10fd323001605\", \"dd25ae17c9c1492fb180e3ac206e780db485c3f8940e1d80301e962fb3384d05\", \"d187fdf65cf5b1e2ad3e379f5a765773a3f196bc0b835ad305946328ace26502\", \"dc43c5790a5ca6c3076206b56bb1c4bc53bfb4b0fba11dfa24809087e6a6760d\", \"7475a3c2465b02f2068d356289d4f17ee35fc0956deb061a737fb4bde998320c\", \"fd3e85d3e5113062b0b49f860318d909c9cb714758e6203a2af2ee69e5bf5803\", \"2014aafdf920276f5a50ca4c3a1b4b7c7f7099ea414e0982bc806beeb68bd303\"], \n \"c1\": \"b51723220ce63175d0ba8577400c59109d97357692b9ace8ec0a57992585ed07\", \n \"D\": \"bea256456a8600ec85716f9bf7dae9cd0cf88ccc2c542600e12c41c7403cf405\"\n }], \n \"pseudoOuts\": [ \"6db57e4fe2422adcc28c3ec0a02dbb6b1b5926d4012ec3f27ad757faeae60cad\"]\n }\n}", + "weight": 1535 + },{ + "blob_size": 11843, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 236860000, + "id_hash": "b8a15acb832330b5070c7615fa1bb5142e8a45ecca022c4136f61dcbcc493986", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261659, + "max_used_block_height": 3195160, + "max_used_block_id_hash": "2f7b8ca3dbd64cb33f428ece414b2b1cef405cfcd85fab1a70383490cc7ed603", + "receive_time": 1721261659, + "relayed": true, + "tx_blob": "020010020010f081a521c5a1d41186eb5698f516b5e818bbba1bedb705a5d00df89f039357c73b890bf54f9217b344cb19dc006e92fc1e623298b3415ddccfc96a8cae64cb7c9199505a767a16ddd39bb9020010f3eab531b38b98029ff116d1ec1faad00187d521d51bc7e80487c304913cbaac01629510e861df07da0fd656ac13a64576e7af5ca416d99b899b0bafef5e71d50e349e467fa463b13600020010e6cf81308cc3e702cbd661ecd64b8650a7bd029cdb05b7e5019ae20cd8f801e87ca479ba3c8a24d08c01b667ca559feaf79de4445ca4d2bcc05883b25ecff2f6dd8fd02a9a14adea4849f06f020010d5989d32d582698beb2499c931dcb2379ae705fcad02ac9e0e85a902f2f30193ed019c0796d302a85ec50e872bc5b7d94e661c5eb09714b243f3854cc06531b1085442834c9e870501031b73da02001084f7ef27b2c2e20acbd767c49a16a092298fe60a8a880286d903aa8d02b2bb1ecb8d02d4d306a814d612f25ba605a5ebf4914f887ecdfde8e7ef303a7f2cc20521a2a305ba9a618e63d95debfb22020010ea98ad1ad2e88618ddf413bbbc9b01feba1d908508938e15eca804fbd803f4f50bc154c661fd60a706be1dae03a2b08a090f611ea1097622cc63a49256a2d94a90b8dbaaa5e53a85001c86d55a0200108acfff31e1c28e02bfd8139bd603e09604e26387af0586f501c79401a059800a9e24e10d9629fb010288f7594b26dcbaff22f7e7569473462c49d8fb845aa916d7a7663be8b85b85530200109c83ed30a19a25d3a9bf01a08c1cbccd14fbc90cccb59701b953c0e403df41e69608bc1dae33c40edc2482037d805459f05d89c92443f43863fa5a4d17241d936fc042cc9847a33a461090c5020010d4a2f828fda6f308b19f8101eacab90199cc02ffae01eff701a4c005d320b32cc10bca9001b101ff59a906cd0365bb760c9a31da39911fa6d0e918e884538f0a218d479f84a1c9cca2f9a5f500020010b8b18c2f80a3c704dfa34e808b02d0a704daa501ecb2039a29eef503a19102ef07669343c20ed525a10452418ac25be58fbfcc8bd35c9833532d0fa911c875fa34b53118df5be0b3ba480200108cf3cc3296f472e2e75184ee0aa3cf0483bb01fec70a96c602c562cb59f6cf01da07d861c14ac411ea0c40e57cb9a9f313f864eef7bf70dea07c2636952f3cbff30385ac26ee244a4349020010b9a4e12cfa958d02b4e18705e6831da49807c8a007b7c202d3ba04b40ba7ca02edb402b4b803d38c01b431ecdb01e50438d739cfb68aba73f0f451c7d8d8e51ae8821e17b275d03214054cc1fe4f72d60200109ccba132acadc401c98731a0ef0ce461b9ad0895f702a24ec955c68401f975da32f616bf07e530c81c1eda8e08b1024028064450019b924eca2e3b3e3446d1ac58d0b8e89dc4ba980d020010cf888128c1f8fb0ac0f221efe24fd7bb14e3a40fddca0bfdcc0fb78f02d7ed02b5a0018e09be10cd1eb10d89051cccfcece29fbd7a28052821fdd7aac6548212cab0d679dd779a37799111f9ec020010db90af22e0eaf40bcbd19b04b6d0b401aec905f48402e5bc0f94cb08bf9808b9db13809701b817d49902fe629903db1b05138378dedfae3adbd844cf76c060226aaeddcd4450c67178e41085d0ae9e53020010a7bbf4299ff7fa05ff9bd201b1fee801e1d529b38341e8900f81a908c78b03b0e101b89201fe6faa5aa701d501ce050007a41ed49aa2f094518d30db5442accaa7d3632381474d649644678b6d23c00200038c2ba531d06e4ac990213d765751e89981303d4714d81229ac09e385c8ac1cd3c900030c9d3c41171e04e42f6ed61a932c2ecdb5c0103d7cdd909fa0ab15881469c66af52c01b848c2cbf8e0ee4a984bf645c0e6118450971072a18d23b6cf716f0bf681fd40020901d0fecbabcf50dc5706e0e4f870dc9c59cdde692b10e76b3405d76aec5c479df6fa3357cbddcb22e20d2b458011dd31dfc1c78316bad7ed9ffd39ba12b86a8ad571f7f5aa9668806d10d2b03c2c84c90690e4ddeb3dc6f870fbd5df5b0901c55abb99bdda211773d14dcf7cd68ca2c2e1df64896976c36da0b72b9b0b5abbf68499752558d3b7b342a554f91ea3171cba93075d35968565e37020bd60f6bf0086548c6fd600cc04f704607fd008bdd1ef83ed341109114d9301c30a86d3d87b9a9a3a6cf62d4aedea2cbe4f48af3a830c33486b3e3c7942ad7b4dfbc322076cbaf3b5e0afffc36549482dc4e5220ad7be9284470405e2a889dc4b0eda950b9eacce92f4b6fe9bbb9f853cd5a68b1ea882a66fc1f86df6b7f21ed49e21fb07070c2330673beaf7961285d721e00c55e6d97b77efb12cfcdac429cbe92fe7ed641ae9878d713edc784573be96c47e80dec59c487590f721dda777066a66a388a8cbd007967d90e323d91cae997f9449518ca71dd5e0473cd88d52a1b63f4994cb355fc68976cdff244295ea25e9d579eeee92211f52ed942307827480bf639e9b945e1f12d5a0369f990945dc143dcbb3e7bb7eada7b4e10ccf65128ab3a203d27441035465a96cac073e12db24bb696f38587021576582596bd0351ea8dcd30123af3e518560a843188e19aa666d9b70d7c55b2072c3813f05084de714988a9a07e21b543d2d50a2b7949e9c6ee3638731aa1f19909ac0978ef10ebe83e52558f6e96814d9b64fb0344d8a37e43e9933a9bcb087173ad895400a680f3f7919a585572b4c0639855b4a146492b00c82eb9d7bd9566aa94387e79f80561676a045e4be44d3a42061a710a8c5be6ac2af7531504911a4e17d27249f7395c40ca90ac64e8a0eaee615e0a23be18c2a2b74e5afea867b60acf8ddf7301055d2e9b6556e2b8750caaddea43764d08b5dbe1497cc7db2be2ccee30629fb313078fdb9fc4f9f01b9d7e2e9cb90fdf2654411be9914ac32fe797c427f2a3f5e0ab974226f243363e401c6214a87d6d33bd5d66a6f74f273c9eaeba1210b64089c7ca7418d0ae9ea96a6e3608b004341be48b3fb658836e80e9c3c2786764db9e239d115c206e2c5d846b20c33614aa533f068a9363f9e78b728faedd5fd5ac6c2b427dd0b00278d9000c2872ff0d2506b842847a65eaf93d1f5ed3267d63cf9128158e1d307aa6a0c69a73c3a1b9914c285981e40de53329e666a98e065354dae1c92765d0af2a1b19b7a3f102fe8a7ba53487879d77c8df704ee4785bd27a6083ab60ec00681a849c4a7064e6265c60e06924fe32cec542a40b1c89d81356cdb200e8429033f75ee3e3749acce3d130cca4e7892f68cd7cdfa8e122bc78530b2e8d0176d07ebae1b0281338c2b2ec14d71d211f1e5500ea3bf298e88b2bb39edc1a4614d0d5048bbe60650d351ec2ff4cafb7f29ed646885572bab7ca50503bd7cb1a6e40775276f4e3914a473acc0a6520ea22947173052c525ff7f69b4161bab3f4e55097adc053156eda0d8d255298da99b7612756be610b5d4c2d4c5622c2e3d45a00a2847e2e122a38f797072440f3b2860dc4a184d42f64b1a3db4c26d43cc3c6a040a1df4fe0cb81346469ee48b4c64ca3a163a234dd107cfa155d610e859d04405d90e1d6a6c4f5b5cd93fa65fc80abff6607d8dc1dd932abb1a760e5a05b8d90003f7429d83df3df24cad2012bd01134cf6ac445145c54434b6e12aea99ed030e66902fd5bbbefe4ec9d59d9e38da2e3e182da804b9cd69fc6516e5c42d12e409807e7e4661a8300cc35c9a5aaf05c74f315d301d3b58af8423e8b3a9dab2a5f47cb7bac961689da9c8480fdcd7699abfc7b1846b5406095930e305225408a80fff283d2ff7b05766873ea25e37a51af6e80fe42c040c09e1254fec0c561d7005078faf699880bad1bff0adcaf0fbbed6a6b708e993738fcf3f51307f49427b0dca0a31c36ab9453b909cc184d4c63908b94427deeaac5521a838ce0e7c6547082dac768455bb669ab5297535a143999b247d8999b9c8d041a2eb3bc3d152c50ae586639c731aa945cc0ee851bcef18a27d942354cb8d66ea356d8c2c04eb490e54f4fd74727b0237fee689ea8802a8d8c885884c216702e32c8e8769430e8709954ea828466f98d09e756e5ed6c12ef5eff3a4ee36c990542f935a460da469037bb52fb8e07f50801ab6a9b81b8eab75b73a7fe93b9d65b75679b3103d5e880aa1d7c47088f50c690def8612384bfd2f01dc17e184a9d0529735d34bba8e0704c1d20036078d9394a593de43e28afeed8e9f9e37a283de8bfa383f333281b502baab61d08ee55dc83a7a7bf7b9be015aa90cfaa3cf2c7976eb1616b6a53277079c48c0cd37af84fd9804839055052758c47dcbf861d6f481edd26426abec9b0f23396a7e14bd9f6ce9168c88eb5cc00c16e92c7133ef6cf66c16ab51d6209401f49e4a023ecaac0ec7ffac5f168d4e7d4ccf098bf7c59a769430b9131bad6500fe9c562cafea11ce248b40a51faf4a9a380b9abf6407f1184c05ab6f1c2caf0d83d399b9afb3f50c7d1b305bb321c14df0787e226ecd2def7bfc6b7c312dca0b255e606d60022c07faf74422223d45772072dc8a7452802ec6e0a208deee8d7da374add905f27f162b82bd779d88d0f533b9cb550de4b7a51679ba3469bf5b0d003038393e91408af3ab39972995190a27c377fd568695b06cf0a2c0f374680c7e6b2a71d81eb28f336edf96840978b03a9c42bc72c9004dea1ed37d1208670da75ed7d6209b278e9362c880ad85e32253589c0ba3fb6a613df14128040e4d0b37dcdb26edc3ed3d88b1c2608439d2b930cfff4eea3c5386d27928ad0f8af70b6e81677aa7c2ceb9280628cd810e5b7e175393342d5cd560ca14ead9574bc20cbf0d7268a249e56526af3029493eb31fd3d20160aa32d30a0f53c44f9d82970105a30031d96eb6efa0bca4d61a6739c37cde501a33ed7de3b8f1fc3ea696830c52fa1350698901ad198fa276f73406da4a2d4cac670860fd0da77c42d9c67305e4044076d2fb8bf2765ecb825e468e698130674102dc58f3c7fcb8194a6ad60546555d48c63f1354f34db8b6c6a381a4052cf4fd6a30ee2274af2da5684e3d0d32f2ad376e21aed1ffd8b3c0e48032a16283c3ce6f12ac2cc90b389e81f59c053fc9310d8c947f78ea37f66d2df64ec8c4e04b8bc58801a6a362802ad8c26106743aa699d067999feb437d92b438ca45bf1a8d1ca5106f6c8609f277a360f70b9237b6b9a393af4c7af6da7647c4bcb7b50da483f82478af3d99bcdbe4ef5e00464e5e541e187854dc98a69aa48fc444019a803cb71719f44b175e968330850f4cc9c96f6c2c24075edcb03b79a8d82d83b49db4a33b7eb993d48e4ece3d2a0f524c847d7f15c03e03708b96b6ea8eaca6a4657c3af0244e6401779a0ee21221fa92f13efd7721c5cc4500e7cdb8254997d8b73a7b2243f580a5e91cc246910e7fa7d13567b4164e6911f6722ed13aadd2a76f127a3435a4bd919836c363cb09ebf110b68ed56b28b4e8bd5d3e4568adfa509e914d98eb21aeaafb6734e42f0710aaa746fe4ea17c423f38ac951dd1007dc17486d2b823ecebf9fb9dcb62dd0de523f315c266de82c74f0378fcbfd2b030c0e1e30b034ec8f7d27946b8c86403863350520c89e34975e27b05df44cbcb017e0abbb6a756faf0b34f43f27529079fa6b98c8b7a26493a215c954448e1958f6f1aa558cee11352fceac48273440c199f152d34e7d3c805b35e61a2ce914b5e03dc5d7858ec15c44413e5b2569c00f31567df8324d01565d4e4e9e04d86f75f24dea9d9d24568cc0ff02740c50c06e0c1712ec5b711aa78a21e7433da47430ed76a9e5a20748ff5df632a9cfdce0d1e54ab98f2bf59db008b5c6a03c2d47fa12540d1c927fde4da851325e28afc0efc1fdd8a0cb2544e57306a0b5f486299a713a8cd4aad16b38f3c1b789937690b35287cdb337f33ce203d6303892c64e8be17016436717ff0c5c1af6ad7082e0b0bb3ca7bdf4a6369598e25d28c42189fc879daef22144424854ba28fef20680b5d32e9ae7872d9c134d6d9fe5b0ffe9352ceabd3805a6af5ac2b7b6234af960a77523d6e80b27849bd7f784a98d2422ff79c902f9dcbe4609c429e5b4a9fc90b15bac05d97ba07fe3734d3f7491c6a962ae942f382a7d28ea1fa5302c51bb40ccfdbd190f3286cf798bb345b51723f6e8ea8515534773a340613d8612db8f9adfff3278d190f7894a6ae534e2ef7a0a8b7b2fae577aaf438b62ff0e4d79b8a0856a76827f3e29042f3cb444e67dc16633c30d8db17c541ab4db7d81fc5a32f00b3609aa0b8d2544843206117a826e456a41aa87d80d320c09f1ac0f93ed8e30a22b70ab8b6be5dd1288e09f9c20906d6bca5f0eff1e2ed57da9465ce60ecc00faeebd450c3c3ad2453b75c18bdb725cca06d8cee657c77564ef03cda16d06f0d8e5ac1cce961abddbb5879738d04997263622263d9f27ee8d5868814afc7a2024e8807bfbf6943fd0792d13b9bef4ff83ed82f0c34df5f9c42aa472cc2c1ac023e152321855eb77e8f762d5b6937d9e7cb3a9f3c2419b806203aa2deb57ae90680a3c83cdcee63d0bf296027ef22cfac0938f9b115359e6c6d91306dadb2fb0441107ab8a88b18f0bde29fd68432767c7ef193cb6815c4456eb0d917b9a9e80f4cf02dee189d4b8accb839002f8b6eba365c9f3f693e156932c09941bfc64600fa9a1ed117e99068c3220c9f0e94b9d7c122c6c391e8f6dc0ddd0ec8097f4d0da7e3987fc86c5523fc788c68b25f86ff5e347849e71171ff0f5c6b15a0b501076b1dcd3e4ba22639beba069d6c813548c99db963077c1f2bcdd173f38e79bb05b450a4748c5959889f22d53f62476626e22587a023dfc4517d49396b0c2efc05817d8187aee15d20ef0f623615bf4da108d2429e596dce7d8e9fe6e8596c06009824b7bc0d7dc58a097658babc3701f19a7210f2c197eea886c5d00dfc50c108570002d437713df562fa1e3d67f70450db016570e1119532ed0e3e1bdcfd3155792e33a1110e87d24c8eedbd1b5d731c4fdea67a7d31168432e7d7cfd6564706d6a5c45ffc307daa0cbc600a5e6e3c9c82d493388ada75deffd3c83906dba60d6de0af97e952ddad74e59b92013a6e55d63a0289e09feb33641ae2bc1b2fd60e33ebb741b522f6c0d47a331846edd4f4d82223d193e0e5aae4b88750280b5702afb63b883a67e6a442ebaf9c7c60be8037a040c0a6e1814696ca2238717ecc0ef670db23a3063db45d74557d715d643255f5cc2b02ce024a42a2c4f48d0a020c4fe82d187b3b6edb7e2e9444cb2cb3b8862523febcd7bd5e7549b5031eb8c90c27907eadeef4328d325dfa862735dc9ba8ae26df2fbf9712ac4fc813122ea10c0ffd43cf70c217fbfed3edd012d66d959d03aba43873f87634a3ec91efdc920986321b3ae6152c4e1688b81775c069fd5a41e71e2dae304bd5c479e3247ee20a94266393a0761debae592b659f32e71c39343636ac1ab2d3732af1fa9778d701fceb0b71bad10c860e51e637430e6520d5639c6a1c430aaad3802c30a92b500fe55616cfb92ae1ce65f3eba6285da428c7849d24918f6d3bf5bcd8b37db6a403a3220064e82e94bc8a3df93e39332b133bda28c866f30e45c824039e448ef70459086810004d7613201583e9497b5c17e878238bc1280c87ed2381b8fd30860656df5be83e02f18e2308f2546a553299ca285b53c7f66ad0297c907e01dccb021d0e21631768d826068eeb457b877f4009977de0fda3d981dc3887515060c50b6cbeb0704498abea125f623107f97b33daf3ba02d619b6edb8223167fcc1df4c1909cb506805d1f464b1ebeaa69a1b1a0cf02387504296c7ce63c61e0385f00036dde496366c9988b48d904c4111379548fa829d55787ed9c629ca873862a40648f6f8e24387826471f8e6047baf0babff8f21cdfb81d4930749638856f2340d306f636d277ae1792f84a293e4f66350c75023a1b5967eb40a43c440a246c60342d8e7ebaa4ee68b8551b9807d7b308574a90a7c54134112d6fdb3151af2c90a2463bad24e336dd34bffcb34713b6802e711a168e4afd30bfc3dba82550fba036a8d9f5070875b622865ce43fa773e77ebcca52dd7fb846aca9c133eb93bfd08030bf7d2f95f541cee1f8ede2d0099177bc447d8c67a6d3b05ee3183498b1b0a9e8c271f40e36941df4924502e31d8122d28fef01063e9692152371ffbd44d0d54b527ba4a550b6191a40192e1735f9e9634df5647c3580179ed7f11d893890e968845f97aa642bccfb6b9e84a4d68189d59fc729f23059dead12b5bce438403aa7a1ef89da4ac0e4617a978acd99ce57213509702e31941dcab7530c902690a7da1e1c0ae121c31fa19cf8889ecd8b61cf086be7cfe695b172a36cdc326d504e55918e88a21efc120a54ffb6f852d5b97ccd7cba3ed1e44b6fda91be7b5de0589ee42794cc797c25249a9a6d40f873e52b491389065a950c8aed5b6675f5c066d1dce8ae2cee39a8f7b1c4adf5fba44fbb41a81b06a5ca9b597ff6081d89a0edf9d832bc866de4095119e7fb0eaeb8930b705d0712aa17e11754ee27da80105befbd6dfc2d3e6e8a040c080a7d4508621474e11a3ba7fee3a2d6ed428324c22d723f61c46b0dedf57ed8a1f5b4588f9922fa5bcbfe9cb9d97d558a4030e7809c1057838edc60be81f357bf466bc5b9fe05d58ce8009fa509db7d24e0404f609b7ade4df5611ef00761069d18aec275e33941db96feb81abcf87171b7d54f0013d88f8d39dbcb57df1b0a3a3463bc75995e8193afb08815de576c1de957b700aba945788360754f9575a3d9c3909e56b171e193da4dec2789cc41ce933c82904e5c4c01c218680c44c46ff1d501ffef20d6a5b067c523ea161e115e7b0179007835c82d226c6d3f7615d13ee729f7c926d429a33302cc61febb9ec53d24dcc00322740613d4714cdf380dec0673929b91f0c173455c1d5f605b3c2a2998e970309c59130a49a6dfe6b9f56d4ae04e21e9b14034e54e0b8745ba2c918f39d2f0f95e48323f664da395a8e2d6e294c202a51e5cb513a664f947deb2ce6c270000632cf0c9decc4857d801b939cb2e97498832d469e9487978a447e40b25904f80ce5d6e7d6a32ea5fdbaa470d134b318b41c8ab7e8cdffec513ad4cc2abe280406081827c389a2c247d0ee40235dd62f1ffecbf6aa3389abda43298ab752f9480f898f59005b4d8a2fdc4631bf7f2cb10a6cb789dd028e5ce71f0cb3512b3ed30a497bedd241b6bbef8bc099340e859f20d47d928fd1bd804eed2b41e266dcb00e3ba7ab8b1d442ef9200458654d613fa08ced2b8d86cbc27260e099bea23e4904e5ce096899cee734b4a524e714c1b98e9acf571563328ef71d45e3bf6fca2d0bafe7b32ad8919db80210eaef8c2c89bcbbafabcd6d5292cdfe439c1a9f1fae66090411640c6ccf905547102b49f7a025a3c7e095e9f7eadd457c147d96210208962f737018ec057b427e2d31ca7eb58b25e9fc4cf7cd9550129035d6b98da60ba7919d4fe9b99e4da7598fe85015c43be4eca45d1913458e9db9bb8a155c1a0eb64774939673b8914c6bfe98348182ec3a222da702175ebd2b8c970bb878d807858ebb7d35f6675bd2f4ab35af156defc38ca3fe0ec5b305f2d13481bdfaf40b09d0076aa6d6e2de0a284fc8027f7ef58df0eba11a524764b4e939371566a50a2f1c2171207626099ea70cbb62112001ce82353e7640b91370b2a277047b9903e6aac7bdc67db5d1e316b7ecd9d26d9efa52a1427aef62ffe91d19e5549f9106059a55d8085e937920beceb69b6233a950913aedb6c9913853257d80a7f2df09240a0b0e2265f861f25f3b3f020295e4c3a557d8f210a19713fbea955e2fbc00aee13bb44e9d935d547cf7b071b05d94292a4d753018e4b2058fe919e98fca0209f9e3815440b6b34990f49af2210cb6074f74dc7bbf5804a6a2986c2a1fe60c70493fb69d8a0a256d3ad34313bb2d88cd9e3dbee169bd656369c2ad0b598e03e2d43843317dcd53af92eaf7de73f89d0aae108da35e9639a951cc4e6bb6bc08478d9108aa862add6731184656515718c3bbe4c666bd3730ecbb150447fb2a0f6a1cd0c4c26f836163f5bd045d30243b49e10bedad9c3a974faed2469b128d02025202ee0e8e262e50081c08bf278b9de30afe9fa80ed99f0b50915bb83752051ce7b914a5b6c8eb0e4a2de2f412a377ebfbe8e3f409ef8580b4878a380c11e476588b8804b369130947a7b805aaf563125b08d79fc2a7a6dd59edbfcac7aa08de4ec98ad49fa4c88de7702238c883b2790c56b5752326843113fc7a3e860e058ac662acfecfe2f803960414b6c5a3f0e16ba8bc45d2e9442b74d13f7774f30af7f4cbb7eb1a2545914dd2562cd1cc69ca12a46b76e4b3d0fdaf520e4a4a7f0dd8ce412d8b0567f994a8c1684e5549402078b59200b8b697c776e6e58d34cd0b797938f4eba967963055852af7194e1d776f455e2e84402598e1ce415f8720000531b2935021bad2348c4b5976d3cf2d89626138a5f204102559c592a33be507dd596db1cbc03ab91f1f5a80729619bfb1bc830d88188e3f9593fb430d4050092f9064ce03ebf328aa662c7adf612240056e0418f531fe086f19509d44cd8e0c83393059bff678437c3d61acafab248c7b41119bb777354b836e9752849c80054fcad749c5660627ac7a24adce564b62426563b0caeb16769ffd5afae42643071d211d06f860445b0a384ea4275a1f6f2b0caf11ec2fead1b8a8abd71215fb0ba65cee1f28aeaf4fb54685c4b9c1d4583d05be800fb3b392c9c9b16d45b4740589c45a1d7adc9fbc4a8a91a6d95a256954408f1e1794b9b73c7fa92d9d1d3d0d62e035bd6a3c91f354608ac7d002f1ec1481b92fe1efb47dd717c58388432e0821b4642894dc1d95bbcb4bea812edd2be509e3723eb27dab02f0aa5174bfda011bac757ac05198bfae729198b21ad83ed77d7099933ca8d438223eb5172e29005f8d4bb439e9f1fd7d9319bc235a800ad8cea46cf72aa161403411fcd239c2ccc1caf86b559d263ec57ba80793cb6045195b2306dc2d3d1545b4f9f628733f0150a751cc8468d64e4fd87d8f6808c387112ea6cbd2747347a2702a1c53d036061c256f19897dced055dedc2f6000444b891b2f0f103ec52169bef5620ecc8802b9c40573c76e1a18907d9aaa82cfde4f69fbe29a031ae39e9d47161b52561104b9c4272fd23a758596329701d313cf2a2a5a0783b9f39b0d9d376b1a53d9aa02008e31c110ea76d7f3dd3b6119d18436d7d87638aaa30c633eef1fcd5923ef0f702f504ae3e3157382c50c022725f50e0de515d1bef117ca08d832e098b0240e6c2be19c7e25a8f32f876f4ea74809a659e02b8f2ec908dbd119ccf0edcd790994de8034f11691ac2cd6500c32e20287e968196dfd93de98d31a5733b19e7a0735c541ce2da123a622e10b79abd68ed242218fc198d3fd9bbe3dced2aabf070750593ef0ddbcf3da82d63d30faf24289e054434fe9b2547de07056c2ae927f00ddf5820c7620d2b9cc0f5047fe4a74d163cc3813f5465bcad682ea0be2d0070d34b1b382dc98e343b261a1a605ce916498f476b2131ff2353e1f9a3ce3e71902f2820f6f514b72f32b2a3e2d50fad8c47129849e2643ffd978f3d6a8893a6a0c1dfb540d9405f41c1b62bba4b0893273535b223deb4bfbf64c2a8b6ac040f00610d5d9beda68f687ebd4035c39c45cd80cef2c15ab5f35f24af9c0560ae08f07b50751c330e15f3ee3a8e07783dc00ca6b8f78d465d7dea2c8c83de74d5f000e240a2046c4d4346fde8802305e568074e94ff89cf0a3d5346eb77564c35b15b6b5746f3412f0a78c40323c98a985c255d63656bf1115c4c826622d462b26f608130852be14f740b8570f5f4a5511b86a784c51a5e4fb8e945e31c8040628da04fcadb8cc0c23e4b24bf2222932c7893ca6b9904dca2ff8a4968d35a0d368ac017496afccec6a815cb767f0b779ae95e3ba5260724a34f18cbc9204840414e60344dedaec24d73ccdbe4807b8f1555c074c94f53d94177fd1698997c431bd100d9a501dbae938247f5b77eb42505ef9cefefd4e7f42287903b316f036dacf80089174621ca177d5a71c9aed771de721522b0f72c815e97b130f1cbcb83877010f56c9926851bccc1f48abeb9972a7cd765435ed59666c643e8bf9f6f0dbf91e0c32da0d9416c4c27b002fe1547ad2406532ecf44c89e6245ac041ac0ce4e4eb0d7ca53b3bab439481930d4e214b3b8460354614c9fc4a2c5035671c4fbc522d0b0ea983f506dcead626b4f7932bc9b7f497f8233a1556478c0786218bac666e0ea9e7ac6a29eba7c27edc4f25dbf6906883bbf4ec47d1ebf38a3fb70c0df46f0b0541a8b5e21a9ebfa0a08a660daf10a2dac0d7c8bba437f3b931c96455b58805b0d522d58f81354805dc073ae2dfd82bc827ae72807fd533687f78a450eba70ef15be34213fc5ca3b3762944f1d018c46e7bb0b9829132633e54528c7178b7000c3852eff49d00338308b216cf77fa36f67130ca7424138560ff9adbbec1ac0f3c300aa9725662614ee790040e3668b7b512e4953b5a68a3df04f881978dac0e77be03f07de3ea414ab5de8b4148df7b2c7548de2392fed2d26dcb61deeb329a434e34961ba8fc0a8533731fbf13923c60ecaece5ac0633100fd18b1a47ab40a9c3d10e14fdd5f1c448479b933db813f16c87d74e925e91a370be9d5cc5f550f3bec745edc71c836a48d90e51777f2c480f5338917fa9ab9ce258bcf6eae6708542e1d2ac11c524ee41c307b7deac63d13dcea39ca2a3fe38f4fe0ba59a6f4052485619c52efba1b68fb0f0ef5aa921cc57fd1d341f3556a1d6b73d17f6db60b496621f542826cddad8de2e517258ab046842f696b9c4f2835506522f9d2b50093ed535605c4815979bac5627c103f51e4ad001e6a600f0d0b4ca4b81f06390b14ad02ea0c3b0349824c1e0573549bd853d20cad9fa1edcca9bc566014f0af06528ffe2ecbaa066240c9bff71fd5ddeff863b27cb00a74fccfb1db5499a8bc06eb01cdeb3b01b261e61f3fdf0f3973a4e1104fb997c8b6e3ae436f1ce0353c028c292daef3187740a653da65cf9610bc22e0fdd7dd5ae16293b057f31e8db002edb9e8b76d70aeb7d1aafa772a757c790590260f51c7671871f0df38f6cb230d84ab59ad81a924c2c8e28a7537ada86ea383b9476311c1ba4fc3ab2cdfec0205637c21cdb13444bc4b3a74e18f399b62fdabea074a3fbf866486b4ed86a6a4068b85c656cca6ed3e68b35175e1924973e82d462e4c2704e06609d552f5e81203d04fbbdf96911fd3fef07f359a8ea935d5a3657bb773783cdc060395d0081102f50063354fae3f6e43ab851dc5c29fc36d48b1e654b09260aa5e62c743cc6b0861f9bfa62752561e2a80f8275dc0e2abaf8f65572fe4fe8e8ccc2d91bdd7c6468783932798cf483e2b6793f0fbc08495d135ae16abd9d334a58bc97de071d10bf13f1dd379ef24f8e41f63787166417e18e5960f38163fb0b92a7127fa22ee03db3562c139a0887a8820e0f85f1714fd29a59d500474f92e1187bf9729c7210cf85a5b1f13d581dd7b8b25a709888f147420a493cc6e036ab9c6366c0e4ebb07026b0e3fd5b34eee445669452cf92bbab5fad18b9b164179b2bd489155c017091a448388d6ca40cea2523618c4a992c37d3c20a7139b940417cdda9c7852bd082bc30446d38f51d4d3ef9b4a8d2482dc05a1efe359d65e9fac6d673fc3a9570b123f5a609851be3b90461ccdd602d85a251164e1c757ed256c4d92b10bec9302a5e9f89069c73bbd2266ab755a4575254a43604b69965de8d131cb812e6a6e0c56a983915850fe148d329c29d15eec67464427da9d763b89c52995b32c1c500ff81d35fa348ec90c2f4680c1e073fb259df6a4dd145b8951e017948c2976050784ed0e7207997ffc619ef2ee4e53fa88af9aea63cd2f1a761151bcab58025a07e3a2b2ce40f9c25e0a88d82efd67c0fefa4cab7cfaa290865508e2e17edee80898d4eed281cd7a8d0e4090a97ff3cbc0ac62712528b367a32bea105840701e0c6588cd1a118bc1b8c4625a3e48f69b779f0f575cd2e505b362051021df8c1b04fb273a52cfe42e7fb16164d3218777622f89580bea14ed586073df23c6c9da01574ae621f62af3728281a4c3cffcfd6f192324ea9eece0755366bde98b5daf0e2b9c58587c15f7b76023125dfbd5fadb374d71481f4427e8a15eb189ee2bc4cbe3bb3b0fdcac5fce8f46a31e9a33a1ce34b8dcf22990f9e15218f67d8ce5470ae819fbd3781424adda93c5a443cbb6c209b9ca424c0eee28ab168f05331b610606f7a9f33ed3ea477451373c0f3578017beacc7b15fae66c0b11abace6d3860bc48977b4d2a9ecaca3b0e3f43b4583f86d560ae1ff0e2cb77edb5419deea3f0cacba39780fc58f3ce0c29c66a3591892c176e1a7d3bb54fbcec8f79fa2bbc004229afd6fdc18c4cb89edf1459ee314763cafe1e75287615c3dbb7f5b05fbab0a9adf444c01a3227b51509dc5bcbf4e6a2eec548d68d1fc6d1bb52673c7efe303c80b9b1cacaac0cfd73dcca1618f30116410c8e6a1475fc191bd6a080b0166087aa6531e59f9d96dc30101580f2cac5efda8fcb7996e085fac2ae03f7b0a460d5cab710600c77c4d3f7f6af775a422b9af2b687d6cab4ddff8e6190e0d9985035f6529e1a277a089ad1b5894cd8f1261918587b89e66dd9bf5891943a319b607ebf127991ac2145696f4b8489d14fe076983381b0ceb983014bea17aed90d90a79d2c9c6e58453483488bf5c892cce23273e98ba4847f39250b126885643ed00540fcb83defd5588e0a342a4d5f73b815e8b18c7d0c7a9aa32b86e65783d2b07e2465870e9a8e1364e6baa88dea32a47f215f661a692d52c0ddada551bbc8d0db7a4e1e987f9f330b0abfcab7318c30fcb4d801f008bbadad93172b8d8f0ce085648a5b033e7f5921900756f12d59d70334cfe24a848e1b85d83e316d303a100add604567b2f5703238410cb63177a22dc934864af6763498346f6039ff360352519cca87dbad1d3f8003bb46a9072b8b396114731d0e1f0df03fcdfee3a0401435fcd82fb448d96064ca438928ab3f2dbc705ef16e9b9dfd65ae8d800a49c0162c35f8ab595be1da87c6c3a8e10886b16519589f22a260d1ef3c2ab11ee9204bda92882ad266b29b1eb64f959c3cf828b27afe849a61195870c65d34ea70706c0944f8091ea483dbc06745a8658dcf5749cbb4887a079399af13cfa69a306088e5bbc7389fd33bbedc0f8b2bcd730b8fa0fb7d5c957a4551e9dce29c4b15f04e057cd99771aab75a7887b82cb12453cea07f785b4ef560e654eac26a079e90a9d3a60c39c1a3a301b9b3a2a0bb7c7071c96ce37bf59b1b847632ceba30b5d044c532e896c29fb2b3769aa0f7495f97a3475a718ce17479f805ede3c893c8d0c0e362207bf9a4c5a48ca988550185e3d010a52b4ad65bce64419142d7238c80020e43b99cb80ceb18bc6a2b045333f8ec169211dd26dc8dfa398691f1d76d30f1b4204a9113a1e353594033e50c908cae266ccd449bae6e030a0acb6619b360f92833140fe243c5be4bd1fda19455a96818f3cbee2232383fbb909c615d1050b687f4451524d507cc9130149899e040927a553b90dd324d57db73c08a5fd0e013f08be740ff23243f87b2c0f4fab5d65edf4e7297ed774852e423e9412e7be0f99a677719928d879cd56bcabfd4dce3ff96a7c5ce34f23471268f42d4b73990295b03026c77645eccf963218535168fd08ab0969ba97b812b6bf3a598e710f09450dd5123579658951b21eb143e4b76ee660a32654a0723e48d2069dcb52edde0690dbbb2bd0c2de2536eb40c89238127c48085de1eb89c8f5bd3208e2c2292422bf268ade80cb5331cbf74cb7338f5d8c99b106b837d0b7679b40907ce4fc4a054bf6418951192254108b6765270f70cecbc01f3e145ca05f1123ad79f8f09bbf7320af0e1802202b1fccb190fa8750d845bd8ea324bcbf558233ae7b65835aa84a144c11883a3a19c084b9c9a9e4a4fbc4a722294a0bd35a8374fa6910fe4b635c8ab7bc1a7951f0f16cfea09a52ab31e5c85848273df135cfacf630e2e8db9728ef733e2adde7dc65b29d6816743ee4b57cdbcc507ae8156edc3f5548e616439cc02daa04a7e0edfc5755b0463072cd9f1480c33e7730e098e6201dd68c35574847b37652d8dfc7ab8eb88788fcc3a54304f69287396bd6783c33dd2f9d92e6bf19bd48cd062b5a5b222e8cc9a84883593261ee7308701453e7250749a7b0f26a28be17dbcbe096e717476b12c1093da5718771a43de20d9b51b76c3a206946f097408973c8441e5a94cd160192ef7e6aac57b8b82ca809f7e504fb6a48dc58a18f5fa5952de6df1f3af19d3041b908946d480ad2328876bc22fc174762e475a5502d85e878e536a78f06574df2119e5c514f8e78910659bf079b189725d9ee0918829b2772828ea6691664868283974656827c44687c47b6164aca68817a888859e80063da37ac442386181fa238fb2db123b49d3f158c2b8a444c80c9c9", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 69812464, 37032133, 1422726, 375448, 406581, 449851, 89069, 223269, 53240, 11155, 7623, 1417, 10229, 2962, 8755, 3275\n ], \n \"k_image\": \"dc006e92fc1e623298b3415ddccfc96a8cae64cb7c9199505a767a16ddd39bb9\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 103642483, 4588979, 374943, 521809, 26666, 551559, 3541, 78919, 74119, 7697, 22074, 98, 2069, 12520, 991, 2010\n ], \n \"k_image\": \"d656ac13a64576e7af5ca416d99b899b0bafef5e71d50e349e467fa463b13600\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 100689894, 5890444, 1600331, 1239916, 10246, 40615, 93596, 29367, 209178, 31832, 15976, 15524, 7738, 4618, 18000, 13238\n ], \n \"k_image\": \"ca559feaf79de4445ca4d2bcc05883b25ecff2f6dd8fd02a9a14adea4849f06f\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 105335893, 1720661, 603531, 812185, 907612, 95130, 38652, 233260, 38021, 31218, 30355, 924, 43414, 12072, 1861, 5511\n ], \n \"k_image\": \"c5b7d94e661c5eb09714b243f3854cc06531b1085442834c9e870501031b73da\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 83622788, 22585650, 1698763, 363844, 674080, 176911, 33802, 60550, 34474, 499122, 34507, 109012, 2600, 2390, 11762, 678\n ], \n \"k_image\": \"a5ebf4914f887ecdfde8e7ef303a7f2cc20521a2a305ba9a618e63d95debfb22\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 55266410, 50443346, 326237, 2547259, 482686, 131728, 345875, 70764, 60539, 195316, 10817, 12486, 12413, 807, 3774, 430\n ], \n \"k_image\": \"a2b08a090f611ea1097622cc63a49256a2d94a90b8dbaaa5e53a85001c86d55a\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 104851338, 4432225, 322623, 60187, 68448, 12770, 87943, 31366, 19015, 11424, 1280, 4638, 1761, 5270, 251, 2\n ], \n \"k_image\": \"88f7594b26dcbaff22f7e7569473462c49d8fb845aa916d7a7663be8b85b8553\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 102449564, 609569, 3134675, 460320, 337596, 206075, 2480844, 10681, 62016, 8415, 133990, 3772, 6574, 1860, 4700, 386\n ], \n \"k_image\": \"7d805459f05d89c92443f43863fa5a4d17241d936fc042cc9847a33a461090c5\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 85856596, 18666365, 2117553, 3040618, 42521, 22399, 31727, 90148, 4179, 5683, 1473, 18506, 177, 11519, 809, 461\n ], \n \"k_image\": \"65bb760c9a31da39911fa6d0e918e884538f0a218d479f84a1c9cca2f9a5f500\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 98769080, 9556352, 1282527, 34176, 70608, 21210, 55660, 5274, 64238, 34977, 1007, 102, 8595, 1858, 4821, 545\n ], \n \"k_image\": \"52418ac25be58fbfcc8bd35c9833532d0fa911c875fa34b53118df5be0b3ba48\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 106117516, 1882646, 1340386, 177924, 75683, 23939, 173054, 41750, 12613, 11467, 26614, 986, 12504, 9537, 2244, 1642\n ], \n \"k_image\": \"40e57cb9a9f313f864eef7bf70dea07c2636952f3cbff30385ac26ee244a4349\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 93868601, 4410106, 10612916, 475622, 117796, 118856, 41271, 73043, 1460, 42279, 39533, 56372, 18003, 6324, 28140, 613\n ], \n \"k_image\": \"38d739cfb68aba73f0f451c7d8d8e51ae8821e17b275d03214054cc1fe4f72d6\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 105407900, 3217068, 803785, 210848, 12516, 136889, 48021, 10018, 10953, 16966, 15097, 6490, 2934, 959, 6245, 3656\n ], \n \"k_image\": \"1eda8e08b1024028064450019b924eca2e3b3e3446d1ac58d0b8e89dc4ba980d\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 83903567, 23002177, 555328, 1306991, 335319, 250467, 189789, 255613, 34743, 46807, 20533, 1166, 2110, 3917, 1713, 649\n ], \n \"k_image\": \"1cccfcece29fbd7a28052821fdd7aac6548212cab0d679dd779a37799111f9ec\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 72075355, 24982880, 8841419, 2959414, 91310, 33396, 253541, 140692, 134207, 323001, 19328, 3000, 36052, 12670, 409, 3547\n ], \n \"k_image\": \"05138378dedfae3adbd844cf76c060226aaeddcd4450c67178e41085d0ae9e53\"\n }\n }, {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 87891367, 12499871, 3444223, 3817265, 682721, 1065395, 247912, 136321, 50631, 28848, 18744, 14334, 11562, 167, 213, 718\n ], \n \"k_image\": \"0007a41ed49aa2f094518d30db5442accaa7d3632381474d649644678b6d23c0\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"8c2ba531d06e4ac990213d765751e89981303d4714d81229ac09e385c8ac1cd3\", \n \"view_tag\": \"c9\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"0c9d3c41171e04e42f6ed61a932c2ecdb5c0103d7cdd909fa0ab15881469c66a\", \n \"view_tag\": \"f5\"\n }\n }\n }\n ], \n \"extra\": [ 1, 184, 72, 194, 203, 248, 224, 238, 74, 152, 75, 246, 69, 192, 230, 17, 132, 80, 151, 16, 114, 161, 141, 35, 182, 207, 113, 111, 11, 246, 129, 253, 64, 2, 9, 1, 208, 254, 203, 171, 207, 80, 220, 87\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 236860000, \n \"ecdhInfo\": [ {\n \"amount\": \"dc9c59cdde692b10\"\n }, {\n \"amount\": \"e76b3405d76aec5c\"\n }], \n \"outPk\": [ \"479df6fa3357cbddcb22e20d2b458011dd31dfc1c78316bad7ed9ffd39ba12b8\", \"6a8ad571f7f5aa9668806d10d2b03c2c84c90690e4ddeb3dc6f870fbd5df5b09\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"c55abb99bdda211773d14dcf7cd68ca2c2e1df64896976c36da0b72b9b0b5abb\", \n \"A1\": \"f68499752558d3b7b342a554f91ea3171cba93075d35968565e37020bd60f6bf\", \n \"B\": \"0086548c6fd600cc04f704607fd008bdd1ef83ed341109114d9301c30a86d3d8\", \n \"r1\": \"7b9a9a3a6cf62d4aedea2cbe4f48af3a830c33486b3e3c7942ad7b4dfbc32207\", \n \"s1\": \"6cbaf3b5e0afffc36549482dc4e5220ad7be9284470405e2a889dc4b0eda950b\", \n \"d1\": \"9eacce92f4b6fe9bbb9f853cd5a68b1ea882a66fc1f86df6b7f21ed49e21fb07\", \n \"L\": [ \"0c2330673beaf7961285d721e00c55e6d97b77efb12cfcdac429cbe92fe7ed64\", \"1ae9878d713edc784573be96c47e80dec59c487590f721dda777066a66a388a8\", \"cbd007967d90e323d91cae997f9449518ca71dd5e0473cd88d52a1b63f4994cb\", \"355fc68976cdff244295ea25e9d579eeee92211f52ed942307827480bf639e9b\", \"945e1f12d5a0369f990945dc143dcbb3e7bb7eada7b4e10ccf65128ab3a203d2\", \"7441035465a96cac073e12db24bb696f38587021576582596bd0351ea8dcd301\", \"23af3e518560a843188e19aa666d9b70d7c55b2072c3813f05084de714988a9a\"\n ], \n \"R\": [ \"e21b543d2d50a2b7949e9c6ee3638731aa1f19909ac0978ef10ebe83e52558f6\", \"e96814d9b64fb0344d8a37e43e9933a9bcb087173ad895400a680f3f7919a585\", \"572b4c0639855b4a146492b00c82eb9d7bd9566aa94387e79f80561676a045e4\", \"be44d3a42061a710a8c5be6ac2af7531504911a4e17d27249f7395c40ca90ac6\", \"4e8a0eaee615e0a23be18c2a2b74e5afea867b60acf8ddf7301055d2e9b6556e\", \"2b8750caaddea43764d08b5dbe1497cc7db2be2ccee30629fb313078fdb9fc4f\", \"9f01b9d7e2e9cb90fdf2654411be9914ac32fe797c427f2a3f5e0ab974226f24\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"3363e401c6214a87d6d33bd5d66a6f74f273c9eaeba1210b64089c7ca7418d0a\", \"e9ea96a6e3608b004341be48b3fb658836e80e9c3c2786764db9e239d115c206\", \"e2c5d846b20c33614aa533f068a9363f9e78b728faedd5fd5ac6c2b427dd0b00\", \"278d9000c2872ff0d2506b842847a65eaf93d1f5ed3267d63cf9128158e1d307\", \"aa6a0c69a73c3a1b9914c285981e40de53329e666a98e065354dae1c92765d0a\", \"f2a1b19b7a3f102fe8a7ba53487879d77c8df704ee4785bd27a6083ab60ec006\", \"81a849c4a7064e6265c60e06924fe32cec542a40b1c89d81356cdb200e842903\", \"3f75ee3e3749acce3d130cca4e7892f68cd7cdfa8e122bc78530b2e8d0176d07\", \"ebae1b0281338c2b2ec14d71d211f1e5500ea3bf298e88b2bb39edc1a4614d0d\", \"5048bbe60650d351ec2ff4cafb7f29ed646885572bab7ca50503bd7cb1a6e407\", \"75276f4e3914a473acc0a6520ea22947173052c525ff7f69b4161bab3f4e5509\", \"7adc053156eda0d8d255298da99b7612756be610b5d4c2d4c5622c2e3d45a00a\", \"2847e2e122a38f797072440f3b2860dc4a184d42f64b1a3db4c26d43cc3c6a04\", \"0a1df4fe0cb81346469ee48b4c64ca3a163a234dd107cfa155d610e859d04405\", \"d90e1d6a6c4f5b5cd93fa65fc80abff6607d8dc1dd932abb1a760e5a05b8d900\", \"03f7429d83df3df24cad2012bd01134cf6ac445145c54434b6e12aea99ed030e\"], \n \"c1\": \"66902fd5bbbefe4ec9d59d9e38da2e3e182da804b9cd69fc6516e5c42d12e409\", \n \"D\": \"807e7e4661a8300cc35c9a5aaf05c74f315d301d3b58af8423e8b3a9dab2a5f4\"\n }, {\n \"s\": [ \"7cb7bac961689da9c8480fdcd7699abfc7b1846b5406095930e305225408a80f\", \"ff283d2ff7b05766873ea25e37a51af6e80fe42c040c09e1254fec0c561d7005\", \"078faf699880bad1bff0adcaf0fbbed6a6b708e993738fcf3f51307f49427b0d\", \"ca0a31c36ab9453b909cc184d4c63908b94427deeaac5521a838ce0e7c654708\", \"2dac768455bb669ab5297535a143999b247d8999b9c8d041a2eb3bc3d152c50a\", \"e586639c731aa945cc0ee851bcef18a27d942354cb8d66ea356d8c2c04eb490e\", \"54f4fd74727b0237fee689ea8802a8d8c885884c216702e32c8e8769430e8709\", \"954ea828466f98d09e756e5ed6c12ef5eff3a4ee36c990542f935a460da46903\", \"7bb52fb8e07f50801ab6a9b81b8eab75b73a7fe93b9d65b75679b3103d5e880a\", \"a1d7c47088f50c690def8612384bfd2f01dc17e184a9d0529735d34bba8e0704\", \"c1d20036078d9394a593de43e28afeed8e9f9e37a283de8bfa383f333281b502\", \"baab61d08ee55dc83a7a7bf7b9be015aa90cfaa3cf2c7976eb1616b6a5327707\", \"9c48c0cd37af84fd9804839055052758c47dcbf861d6f481edd26426abec9b0f\", \"23396a7e14bd9f6ce9168c88eb5cc00c16e92c7133ef6cf66c16ab51d6209401\", \"f49e4a023ecaac0ec7ffac5f168d4e7d4ccf098bf7c59a769430b9131bad6500\", \"fe9c562cafea11ce248b40a51faf4a9a380b9abf6407f1184c05ab6f1c2caf0d\"], \n \"c1\": \"83d399b9afb3f50c7d1b305bb321c14df0787e226ecd2def7bfc6b7c312dca0b\", \n \"D\": \"255e606d60022c07faf74422223d45772072dc8a7452802ec6e0a208deee8d7d\"\n }, {\n \"s\": [ \"a374add905f27f162b82bd779d88d0f533b9cb550de4b7a51679ba3469bf5b0d\", \"003038393e91408af3ab39972995190a27c377fd568695b06cf0a2c0f374680c\", \"7e6b2a71d81eb28f336edf96840978b03a9c42bc72c9004dea1ed37d1208670d\", \"a75ed7d6209b278e9362c880ad85e32253589c0ba3fb6a613df14128040e4d0b\", \"37dcdb26edc3ed3d88b1c2608439d2b930cfff4eea3c5386d27928ad0f8af70b\", \"6e81677aa7c2ceb9280628cd810e5b7e175393342d5cd560ca14ead9574bc20c\", \"bf0d7268a249e56526af3029493eb31fd3d20160aa32d30a0f53c44f9d829701\", \"05a30031d96eb6efa0bca4d61a6739c37cde501a33ed7de3b8f1fc3ea696830c\", \"52fa1350698901ad198fa276f73406da4a2d4cac670860fd0da77c42d9c67305\", \"e4044076d2fb8bf2765ecb825e468e698130674102dc58f3c7fcb8194a6ad605\", \"46555d48c63f1354f34db8b6c6a381a4052cf4fd6a30ee2274af2da5684e3d0d\", \"32f2ad376e21aed1ffd8b3c0e48032a16283c3ce6f12ac2cc90b389e81f59c05\", \"3fc9310d8c947f78ea37f66d2df64ec8c4e04b8bc58801a6a362802ad8c26106\", \"743aa699d067999feb437d92b438ca45bf1a8d1ca5106f6c8609f277a360f70b\", \"9237b6b9a393af4c7af6da7647c4bcb7b50da483f82478af3d99bcdbe4ef5e00\", \"464e5e541e187854dc98a69aa48fc444019a803cb71719f44b175e968330850f\"], \n \"c1\": \"4cc9c96f6c2c24075edcb03b79a8d82d83b49db4a33b7eb993d48e4ece3d2a0f\", \n \"D\": \"524c847d7f15c03e03708b96b6ea8eaca6a4657c3af0244e6401779a0ee21221\"\n }, {\n \"s\": [ \"fa92f13efd7721c5cc4500e7cdb8254997d8b73a7b2243f580a5e91cc246910e\", \"7fa7d13567b4164e6911f6722ed13aadd2a76f127a3435a4bd919836c363cb09\", \"ebf110b68ed56b28b4e8bd5d3e4568adfa509e914d98eb21aeaafb6734e42f07\", \"10aaa746fe4ea17c423f38ac951dd1007dc17486d2b823ecebf9fb9dcb62dd0d\", \"e523f315c266de82c74f0378fcbfd2b030c0e1e30b034ec8f7d27946b8c86403\", \"863350520c89e34975e27b05df44cbcb017e0abbb6a756faf0b34f43f2752907\", \"9fa6b98c8b7a26493a215c954448e1958f6f1aa558cee11352fceac48273440c\", \"199f152d34e7d3c805b35e61a2ce914b5e03dc5d7858ec15c44413e5b2569c00\", \"f31567df8324d01565d4e4e9e04d86f75f24dea9d9d24568cc0ff02740c50c06\", \"e0c1712ec5b711aa78a21e7433da47430ed76a9e5a20748ff5df632a9cfdce0d\", \"1e54ab98f2bf59db008b5c6a03c2d47fa12540d1c927fde4da851325e28afc0e\", \"fc1fdd8a0cb2544e57306a0b5f486299a713a8cd4aad16b38f3c1b789937690b\", \"35287cdb337f33ce203d6303892c64e8be17016436717ff0c5c1af6ad7082e0b\", \"0bb3ca7bdf4a6369598e25d28c42189fc879daef22144424854ba28fef20680b\", \"5d32e9ae7872d9c134d6d9fe5b0ffe9352ceabd3805a6af5ac2b7b6234af960a\", \"77523d6e80b27849bd7f784a98d2422ff79c902f9dcbe4609c429e5b4a9fc90b\"], \n \"c1\": \"15bac05d97ba07fe3734d3f7491c6a962ae942f382a7d28ea1fa5302c51bb40c\", \n \"D\": \"cfdbd190f3286cf798bb345b51723f6e8ea8515534773a340613d8612db8f9ad\"\n }, {\n \"s\": [ \"fff3278d190f7894a6ae534e2ef7a0a8b7b2fae577aaf438b62ff0e4d79b8a08\", \"56a76827f3e29042f3cb444e67dc16633c30d8db17c541ab4db7d81fc5a32f00\", \"b3609aa0b8d2544843206117a826e456a41aa87d80d320c09f1ac0f93ed8e30a\", \"22b70ab8b6be5dd1288e09f9c20906d6bca5f0eff1e2ed57da9465ce60ecc00f\", \"aeebd450c3c3ad2453b75c18bdb725cca06d8cee657c77564ef03cda16d06f0d\", \"8e5ac1cce961abddbb5879738d04997263622263d9f27ee8d5868814afc7a202\", \"4e8807bfbf6943fd0792d13b9bef4ff83ed82f0c34df5f9c42aa472cc2c1ac02\", \"3e152321855eb77e8f762d5b6937d9e7cb3a9f3c2419b806203aa2deb57ae906\", \"80a3c83cdcee63d0bf296027ef22cfac0938f9b115359e6c6d91306dadb2fb04\", \"41107ab8a88b18f0bde29fd68432767c7ef193cb6815c4456eb0d917b9a9e80f\", \"4cf02dee189d4b8accb839002f8b6eba365c9f3f693e156932c09941bfc64600\", \"fa9a1ed117e99068c3220c9f0e94b9d7c122c6c391e8f6dc0ddd0ec8097f4d0d\", \"a7e3987fc86c5523fc788c68b25f86ff5e347849e71171ff0f5c6b15a0b50107\", \"6b1dcd3e4ba22639beba069d6c813548c99db963077c1f2bcdd173f38e79bb05\", \"b450a4748c5959889f22d53f62476626e22587a023dfc4517d49396b0c2efc05\", \"817d8187aee15d20ef0f623615bf4da108d2429e596dce7d8e9fe6e8596c0600\"], \n \"c1\": \"9824b7bc0d7dc58a097658babc3701f19a7210f2c197eea886c5d00dfc50c108\", \n \"D\": \"570002d437713df562fa1e3d67f70450db016570e1119532ed0e3e1bdcfd3155\"\n }, {\n \"s\": [ \"792e33a1110e87d24c8eedbd1b5d731c4fdea67a7d31168432e7d7cfd6564706\", \"d6a5c45ffc307daa0cbc600a5e6e3c9c82d493388ada75deffd3c83906dba60d\", \"6de0af97e952ddad74e59b92013a6e55d63a0289e09feb33641ae2bc1b2fd60e\", \"33ebb741b522f6c0d47a331846edd4f4d82223d193e0e5aae4b88750280b5702\", \"afb63b883a67e6a442ebaf9c7c60be8037a040c0a6e1814696ca2238717ecc0e\", \"f670db23a3063db45d74557d715d643255f5cc2b02ce024a42a2c4f48d0a020c\", \"4fe82d187b3b6edb7e2e9444cb2cb3b8862523febcd7bd5e7549b5031eb8c90c\", \"27907eadeef4328d325dfa862735dc9ba8ae26df2fbf9712ac4fc813122ea10c\", \"0ffd43cf70c217fbfed3edd012d66d959d03aba43873f87634a3ec91efdc9209\", \"86321b3ae6152c4e1688b81775c069fd5a41e71e2dae304bd5c479e3247ee20a\", \"94266393a0761debae592b659f32e71c39343636ac1ab2d3732af1fa9778d701\", \"fceb0b71bad10c860e51e637430e6520d5639c6a1c430aaad3802c30a92b500f\", \"e55616cfb92ae1ce65f3eba6285da428c7849d24918f6d3bf5bcd8b37db6a403\", \"a3220064e82e94bc8a3df93e39332b133bda28c866f30e45c824039e448ef704\", \"59086810004d7613201583e9497b5c17e878238bc1280c87ed2381b8fd308606\", \"56df5be83e02f18e2308f2546a553299ca285b53c7f66ad0297c907e01dccb02\"], \n \"c1\": \"1d0e21631768d826068eeb457b877f4009977de0fda3d981dc3887515060c50b\", \n \"D\": \"6cbeb0704498abea125f623107f97b33daf3ba02d619b6edb8223167fcc1df4c\"\n }, {\n \"s\": [ \"1909cb506805d1f464b1ebeaa69a1b1a0cf02387504296c7ce63c61e0385f000\", \"36dde496366c9988b48d904c4111379548fa829d55787ed9c629ca873862a406\", \"48f6f8e24387826471f8e6047baf0babff8f21cdfb81d4930749638856f2340d\", \"306f636d277ae1792f84a293e4f66350c75023a1b5967eb40a43c440a246c603\", \"42d8e7ebaa4ee68b8551b9807d7b308574a90a7c54134112d6fdb3151af2c90a\", \"2463bad24e336dd34bffcb34713b6802e711a168e4afd30bfc3dba82550fba03\", \"6a8d9f5070875b622865ce43fa773e77ebcca52dd7fb846aca9c133eb93bfd08\", \"030bf7d2f95f541cee1f8ede2d0099177bc447d8c67a6d3b05ee3183498b1b0a\", \"9e8c271f40e36941df4924502e31d8122d28fef01063e9692152371ffbd44d0d\", \"54b527ba4a550b6191a40192e1735f9e9634df5647c3580179ed7f11d893890e\", \"968845f97aa642bccfb6b9e84a4d68189d59fc729f23059dead12b5bce438403\", \"aa7a1ef89da4ac0e4617a978acd99ce57213509702e31941dcab7530c902690a\", \"7da1e1c0ae121c31fa19cf8889ecd8b61cf086be7cfe695b172a36cdc326d504\", \"e55918e88a21efc120a54ffb6f852d5b97ccd7cba3ed1e44b6fda91be7b5de05\", \"89ee42794cc797c25249a9a6d40f873e52b491389065a950c8aed5b6675f5c06\", \"6d1dce8ae2cee39a8f7b1c4adf5fba44fbb41a81b06a5ca9b597ff6081d89a0e\"], \n \"c1\": \"df9d832bc866de4095119e7fb0eaeb8930b705d0712aa17e11754ee27da80105\", \n \"D\": \"befbd6dfc2d3e6e8a040c080a7d4508621474e11a3ba7fee3a2d6ed428324c22\"\n }, {\n \"s\": [ \"d723f61c46b0dedf57ed8a1f5b4588f9922fa5bcbfe9cb9d97d558a4030e7809\", \"c1057838edc60be81f357bf466bc5b9fe05d58ce8009fa509db7d24e0404f609\", \"b7ade4df5611ef00761069d18aec275e33941db96feb81abcf87171b7d54f001\", \"3d88f8d39dbcb57df1b0a3a3463bc75995e8193afb08815de576c1de957b700a\", \"ba945788360754f9575a3d9c3909e56b171e193da4dec2789cc41ce933c82904\", \"e5c4c01c218680c44c46ff1d501ffef20d6a5b067c523ea161e115e7b0179007\", \"835c82d226c6d3f7615d13ee729f7c926d429a33302cc61febb9ec53d24dcc00\", \"322740613d4714cdf380dec0673929b91f0c173455c1d5f605b3c2a2998e9703\", \"09c59130a49a6dfe6b9f56d4ae04e21e9b14034e54e0b8745ba2c918f39d2f0f\", \"95e48323f664da395a8e2d6e294c202a51e5cb513a664f947deb2ce6c2700006\", \"32cf0c9decc4857d801b939cb2e97498832d469e9487978a447e40b25904f80c\", \"e5d6e7d6a32ea5fdbaa470d134b318b41c8ab7e8cdffec513ad4cc2abe280406\", \"081827c389a2c247d0ee40235dd62f1ffecbf6aa3389abda43298ab752f9480f\", \"898f59005b4d8a2fdc4631bf7f2cb10a6cb789dd028e5ce71f0cb3512b3ed30a\", \"497bedd241b6bbef8bc099340e859f20d47d928fd1bd804eed2b41e266dcb00e\", \"3ba7ab8b1d442ef9200458654d613fa08ced2b8d86cbc27260e099bea23e4904\"], \n \"c1\": \"e5ce096899cee734b4a524e714c1b98e9acf571563328ef71d45e3bf6fca2d0b\", \n \"D\": \"afe7b32ad8919db80210eaef8c2c89bcbbafabcd6d5292cdfe439c1a9f1fae66\"\n }, {\n \"s\": [ \"090411640c6ccf905547102b49f7a025a3c7e095e9f7eadd457c147d96210208\", \"962f737018ec057b427e2d31ca7eb58b25e9fc4cf7cd9550129035d6b98da60b\", \"a7919d4fe9b99e4da7598fe85015c43be4eca45d1913458e9db9bb8a155c1a0e\", \"b64774939673b8914c6bfe98348182ec3a222da702175ebd2b8c970bb878d807\", \"858ebb7d35f6675bd2f4ab35af156defc38ca3fe0ec5b305f2d13481bdfaf40b\", \"09d0076aa6d6e2de0a284fc8027f7ef58df0eba11a524764b4e939371566a50a\", \"2f1c2171207626099ea70cbb62112001ce82353e7640b91370b2a277047b9903\", \"e6aac7bdc67db5d1e316b7ecd9d26d9efa52a1427aef62ffe91d19e5549f9106\", \"059a55d8085e937920beceb69b6233a950913aedb6c9913853257d80a7f2df09\", \"240a0b0e2265f861f25f3b3f020295e4c3a557d8f210a19713fbea955e2fbc00\", \"aee13bb44e9d935d547cf7b071b05d94292a4d753018e4b2058fe919e98fca02\", \"09f9e3815440b6b34990f49af2210cb6074f74dc7bbf5804a6a2986c2a1fe60c\", \"70493fb69d8a0a256d3ad34313bb2d88cd9e3dbee169bd656369c2ad0b598e03\", \"e2d43843317dcd53af92eaf7de73f89d0aae108da35e9639a951cc4e6bb6bc08\", \"478d9108aa862add6731184656515718c3bbe4c666bd3730ecbb150447fb2a0f\", \"6a1cd0c4c26f836163f5bd045d30243b49e10bedad9c3a974faed2469b128d02\"], \n \"c1\": \"025202ee0e8e262e50081c08bf278b9de30afe9fa80ed99f0b50915bb8375205\", \n \"D\": \"1ce7b914a5b6c8eb0e4a2de2f412a377ebfbe8e3f409ef8580b4878a380c11e4\"\n }, {\n \"s\": [ \"76588b8804b369130947a7b805aaf563125b08d79fc2a7a6dd59edbfcac7aa08\", \"de4ec98ad49fa4c88de7702238c883b2790c56b5752326843113fc7a3e860e05\", \"8ac662acfecfe2f803960414b6c5a3f0e16ba8bc45d2e9442b74d13f7774f30a\", \"f7f4cbb7eb1a2545914dd2562cd1cc69ca12a46b76e4b3d0fdaf520e4a4a7f0d\", \"d8ce412d8b0567f994a8c1684e5549402078b59200b8b697c776e6e58d34cd0b\", \"797938f4eba967963055852af7194e1d776f455e2e84402598e1ce415f872000\", \"0531b2935021bad2348c4b5976d3cf2d89626138a5f204102559c592a33be507\", \"dd596db1cbc03ab91f1f5a80729619bfb1bc830d88188e3f9593fb430d405009\", \"2f9064ce03ebf328aa662c7adf612240056e0418f531fe086f19509d44cd8e0c\", \"83393059bff678437c3d61acafab248c7b41119bb777354b836e9752849c8005\", \"4fcad749c5660627ac7a24adce564b62426563b0caeb16769ffd5afae4264307\", \"1d211d06f860445b0a384ea4275a1f6f2b0caf11ec2fead1b8a8abd71215fb0b\", \"a65cee1f28aeaf4fb54685c4b9c1d4583d05be800fb3b392c9c9b16d45b47405\", \"89c45a1d7adc9fbc4a8a91a6d95a256954408f1e1794b9b73c7fa92d9d1d3d0d\", \"62e035bd6a3c91f354608ac7d002f1ec1481b92fe1efb47dd717c58388432e08\", \"21b4642894dc1d95bbcb4bea812edd2be509e3723eb27dab02f0aa5174bfda01\"], \n \"c1\": \"1bac757ac05198bfae729198b21ad83ed77d7099933ca8d438223eb5172e2900\", \n \"D\": \"5f8d4bb439e9f1fd7d9319bc235a800ad8cea46cf72aa161403411fcd239c2cc\"\n }, {\n \"s\": [ \"c1caf86b559d263ec57ba80793cb6045195b2306dc2d3d1545b4f9f628733f01\", \"50a751cc8468d64e4fd87d8f6808c387112ea6cbd2747347a2702a1c53d03606\", \"1c256f19897dced055dedc2f6000444b891b2f0f103ec52169bef5620ecc8802\", \"b9c40573c76e1a18907d9aaa82cfde4f69fbe29a031ae39e9d47161b52561104\", \"b9c4272fd23a758596329701d313cf2a2a5a0783b9f39b0d9d376b1a53d9aa02\", \"008e31c110ea76d7f3dd3b6119d18436d7d87638aaa30c633eef1fcd5923ef0f\", \"702f504ae3e3157382c50c022725f50e0de515d1bef117ca08d832e098b0240e\", \"6c2be19c7e25a8f32f876f4ea74809a659e02b8f2ec908dbd119ccf0edcd7909\", \"94de8034f11691ac2cd6500c32e20287e968196dfd93de98d31a5733b19e7a07\", \"35c541ce2da123a622e10b79abd68ed242218fc198d3fd9bbe3dced2aabf0707\", \"50593ef0ddbcf3da82d63d30faf24289e054434fe9b2547de07056c2ae927f00\", \"ddf5820c7620d2b9cc0f5047fe4a74d163cc3813f5465bcad682ea0be2d0070d\", \"34b1b382dc98e343b261a1a605ce916498f476b2131ff2353e1f9a3ce3e71902\", \"f2820f6f514b72f32b2a3e2d50fad8c47129849e2643ffd978f3d6a8893a6a0c\", \"1dfb540d9405f41c1b62bba4b0893273535b223deb4bfbf64c2a8b6ac040f006\", \"10d5d9beda68f687ebd4035c39c45cd80cef2c15ab5f35f24af9c0560ae08f07\"], \n \"c1\": \"b50751c330e15f3ee3a8e07783dc00ca6b8f78d465d7dea2c8c83de74d5f000e\", \n \"D\": \"240a2046c4d4346fde8802305e568074e94ff89cf0a3d5346eb77564c35b15b6\"\n }, {\n \"s\": [ \"b5746f3412f0a78c40323c98a985c255d63656bf1115c4c826622d462b26f608\", \"130852be14f740b8570f5f4a5511b86a784c51a5e4fb8e945e31c8040628da04\", \"fcadb8cc0c23e4b24bf2222932c7893ca6b9904dca2ff8a4968d35a0d368ac01\", \"7496afccec6a815cb767f0b779ae95e3ba5260724a34f18cbc9204840414e603\", \"44dedaec24d73ccdbe4807b8f1555c074c94f53d94177fd1698997c431bd100d\", \"9a501dbae938247f5b77eb42505ef9cefefd4e7f42287903b316f036dacf8008\", \"9174621ca177d5a71c9aed771de721522b0f72c815e97b130f1cbcb83877010f\", \"56c9926851bccc1f48abeb9972a7cd765435ed59666c643e8bf9f6f0dbf91e0c\", \"32da0d9416c4c27b002fe1547ad2406532ecf44c89e6245ac041ac0ce4e4eb0d\", \"7ca53b3bab439481930d4e214b3b8460354614c9fc4a2c5035671c4fbc522d0b\", \"0ea983f506dcead626b4f7932bc9b7f497f8233a1556478c0786218bac666e0e\", \"a9e7ac6a29eba7c27edc4f25dbf6906883bbf4ec47d1ebf38a3fb70c0df46f0b\", \"0541a8b5e21a9ebfa0a08a660daf10a2dac0d7c8bba437f3b931c96455b58805\", \"b0d522d58f81354805dc073ae2dfd82bc827ae72807fd533687f78a450eba70e\", \"f15be34213fc5ca3b3762944f1d018c46e7bb0b9829132633e54528c7178b700\", \"0c3852eff49d00338308b216cf77fa36f67130ca7424138560ff9adbbec1ac0f\"], \n \"c1\": \"3c300aa9725662614ee790040e3668b7b512e4953b5a68a3df04f881978dac0e\", \n \"D\": \"77be03f07de3ea414ab5de8b4148df7b2c7548de2392fed2d26dcb61deeb329a\"\n }, {\n \"s\": [ \"434e34961ba8fc0a8533731fbf13923c60ecaece5ac0633100fd18b1a47ab40a\", \"9c3d10e14fdd5f1c448479b933db813f16c87d74e925e91a370be9d5cc5f550f\", \"3bec745edc71c836a48d90e51777f2c480f5338917fa9ab9ce258bcf6eae6708\", \"542e1d2ac11c524ee41c307b7deac63d13dcea39ca2a3fe38f4fe0ba59a6f405\", \"2485619c52efba1b68fb0f0ef5aa921cc57fd1d341f3556a1d6b73d17f6db60b\", \"496621f542826cddad8de2e517258ab046842f696b9c4f2835506522f9d2b500\", \"93ed535605c4815979bac5627c103f51e4ad001e6a600f0d0b4ca4b81f06390b\", \"14ad02ea0c3b0349824c1e0573549bd853d20cad9fa1edcca9bc566014f0af06\", \"528ffe2ecbaa066240c9bff71fd5ddeff863b27cb00a74fccfb1db5499a8bc06\", \"eb01cdeb3b01b261e61f3fdf0f3973a4e1104fb997c8b6e3ae436f1ce0353c02\", \"8c292daef3187740a653da65cf9610bc22e0fdd7dd5ae16293b057f31e8db002\", \"edb9e8b76d70aeb7d1aafa772a757c790590260f51c7671871f0df38f6cb230d\", \"84ab59ad81a924c2c8e28a7537ada86ea383b9476311c1ba4fc3ab2cdfec0205\", \"637c21cdb13444bc4b3a74e18f399b62fdabea074a3fbf866486b4ed86a6a406\", \"8b85c656cca6ed3e68b35175e1924973e82d462e4c2704e06609d552f5e81203\", \"d04fbbdf96911fd3fef07f359a8ea935d5a3657bb773783cdc060395d0081102\"], \n \"c1\": \"f50063354fae3f6e43ab851dc5c29fc36d48b1e654b09260aa5e62c743cc6b08\", \n \"D\": \"61f9bfa62752561e2a80f8275dc0e2abaf8f65572fe4fe8e8ccc2d91bdd7c646\"\n }, {\n \"s\": [ \"8783932798cf483e2b6793f0fbc08495d135ae16abd9d334a58bc97de071d10b\", \"f13f1dd379ef24f8e41f63787166417e18e5960f38163fb0b92a7127fa22ee03\", \"db3562c139a0887a8820e0f85f1714fd29a59d500474f92e1187bf9729c7210c\", \"f85a5b1f13d581dd7b8b25a709888f147420a493cc6e036ab9c6366c0e4ebb07\", \"026b0e3fd5b34eee445669452cf92bbab5fad18b9b164179b2bd489155c01709\", \"1a448388d6ca40cea2523618c4a992c37d3c20a7139b940417cdda9c7852bd08\", \"2bc30446d38f51d4d3ef9b4a8d2482dc05a1efe359d65e9fac6d673fc3a9570b\", \"123f5a609851be3b90461ccdd602d85a251164e1c757ed256c4d92b10bec9302\", \"a5e9f89069c73bbd2266ab755a4575254a43604b69965de8d131cb812e6a6e0c\", \"56a983915850fe148d329c29d15eec67464427da9d763b89c52995b32c1c500f\", \"f81d35fa348ec90c2f4680c1e073fb259df6a4dd145b8951e017948c29760507\", \"84ed0e7207997ffc619ef2ee4e53fa88af9aea63cd2f1a761151bcab58025a07\", \"e3a2b2ce40f9c25e0a88d82efd67c0fefa4cab7cfaa290865508e2e17edee808\", \"98d4eed281cd7a8d0e4090a97ff3cbc0ac62712528b367a32bea105840701e0c\", \"6588cd1a118bc1b8c4625a3e48f69b779f0f575cd2e505b362051021df8c1b04\", \"fb273a52cfe42e7fb16164d3218777622f89580bea14ed586073df23c6c9da01\"], \n \"c1\": \"574ae621f62af3728281a4c3cffcfd6f192324ea9eece0755366bde98b5daf0e\", \n \"D\": \"2b9c58587c15f7b76023125dfbd5fadb374d71481f4427e8a15eb189ee2bc4cb\"\n }, {\n \"s\": [ \"e3bb3b0fdcac5fce8f46a31e9a33a1ce34b8dcf22990f9e15218f67d8ce5470a\", \"e819fbd3781424adda93c5a443cbb6c209b9ca424c0eee28ab168f05331b6106\", \"06f7a9f33ed3ea477451373c0f3578017beacc7b15fae66c0b11abace6d3860b\", \"c48977b4d2a9ecaca3b0e3f43b4583f86d560ae1ff0e2cb77edb5419deea3f0c\", \"acba39780fc58f3ce0c29c66a3591892c176e1a7d3bb54fbcec8f79fa2bbc004\", \"229afd6fdc18c4cb89edf1459ee314763cafe1e75287615c3dbb7f5b05fbab0a\", \"9adf444c01a3227b51509dc5bcbf4e6a2eec548d68d1fc6d1bb52673c7efe303\", \"c80b9b1cacaac0cfd73dcca1618f30116410c8e6a1475fc191bd6a080b016608\", \"7aa6531e59f9d96dc30101580f2cac5efda8fcb7996e085fac2ae03f7b0a460d\", \"5cab710600c77c4d3f7f6af775a422b9af2b687d6cab4ddff8e6190e0d998503\", \"5f6529e1a277a089ad1b5894cd8f1261918587b89e66dd9bf5891943a319b607\", \"ebf127991ac2145696f4b8489d14fe076983381b0ceb983014bea17aed90d90a\", \"79d2c9c6e58453483488bf5c892cce23273e98ba4847f39250b126885643ed00\", \"540fcb83defd5588e0a342a4d5f73b815e8b18c7d0c7a9aa32b86e65783d2b07\", \"e2465870e9a8e1364e6baa88dea32a47f215f661a692d52c0ddada551bbc8d0d\", \"b7a4e1e987f9f330b0abfcab7318c30fcb4d801f008bbadad93172b8d8f0ce08\"], \n \"c1\": \"5648a5b033e7f5921900756f12d59d70334cfe24a848e1b85d83e316d303a100\", \n \"D\": \"add604567b2f5703238410cb63177a22dc934864af6763498346f6039ff36035\"\n }, {\n \"s\": [ \"2519cca87dbad1d3f8003bb46a9072b8b396114731d0e1f0df03fcdfee3a0401\", \"435fcd82fb448d96064ca438928ab3f2dbc705ef16e9b9dfd65ae8d800a49c01\", \"62c35f8ab595be1da87c6c3a8e10886b16519589f22a260d1ef3c2ab11ee9204\", \"bda92882ad266b29b1eb64f959c3cf828b27afe849a61195870c65d34ea70706\", \"c0944f8091ea483dbc06745a8658dcf5749cbb4887a079399af13cfa69a30608\", \"8e5bbc7389fd33bbedc0f8b2bcd730b8fa0fb7d5c957a4551e9dce29c4b15f04\", \"e057cd99771aab75a7887b82cb12453cea07f785b4ef560e654eac26a079e90a\", \"9d3a60c39c1a3a301b9b3a2a0bb7c7071c96ce37bf59b1b847632ceba30b5d04\", \"4c532e896c29fb2b3769aa0f7495f97a3475a718ce17479f805ede3c893c8d0c\", \"0e362207bf9a4c5a48ca988550185e3d010a52b4ad65bce64419142d7238c800\", \"20e43b99cb80ceb18bc6a2b045333f8ec169211dd26dc8dfa398691f1d76d30f\", \"1b4204a9113a1e353594033e50c908cae266ccd449bae6e030a0acb6619b360f\", \"92833140fe243c5be4bd1fda19455a96818f3cbee2232383fbb909c615d1050b\", \"687f4451524d507cc9130149899e040927a553b90dd324d57db73c08a5fd0e01\", \"3f08be740ff23243f87b2c0f4fab5d65edf4e7297ed774852e423e9412e7be0f\", \"99a677719928d879cd56bcabfd4dce3ff96a7c5ce34f23471268f42d4b739902\"], \n \"c1\": \"95b03026c77645eccf963218535168fd08ab0969ba97b812b6bf3a598e710f09\", \n \"D\": \"450dd5123579658951b21eb143e4b76ee660a32654a0723e48d2069dcb52edde\"\n }], \n \"pseudoOuts\": [ \"0690dbbb2bd0c2de2536eb40c89238127c48085de1eb89c8f5bd3208e2c22924\", \"22bf268ade80cb5331cbf74cb7338f5d8c99b106b837d0b7679b40907ce4fc4a\", \"054bf6418951192254108b6765270f70cecbc01f3e145ca05f1123ad79f8f09b\", \"bf7320af0e1802202b1fccb190fa8750d845bd8ea324bcbf558233ae7b65835a\", \"a84a144c11883a3a19c084b9c9a9e4a4fbc4a722294a0bd35a8374fa6910fe4b\", \"635c8ab7bc1a7951f0f16cfea09a52ab31e5c85848273df135cfacf630e2e8db\", \"9728ef733e2adde7dc65b29d6816743ee4b57cdbcc507ae8156edc3f5548e616\", \"439cc02daa04a7e0edfc5755b0463072cd9f1480c33e7730e098e6201dd68c35\", \"574847b37652d8dfc7ab8eb88788fcc3a54304f69287396bd6783c33dd2f9d92\", \"e6bf19bd48cd062b5a5b222e8cc9a84883593261ee7308701453e7250749a7b0\", \"f26a28be17dbcbe096e717476b12c1093da5718771a43de20d9b51b76c3a2069\", \"46f097408973c8441e5a94cd160192ef7e6aac57b8b82ca809f7e504fb6a48dc\", \"58a18f5fa5952de6df1f3af19d3041b908946d480ad2328876bc22fc174762e4\", \"75a5502d85e878e536a78f06574df2119e5c514f8e78910659bf079b189725d9\", \"ee0918829b2772828ea6691664868283974656827c44687c47b6164aca68817a\", \"888859e80063da37ac442386181fa238fb2db123b49d3f158c2b8a444c80c9c9\"]\n }\n}", + "weight": 11843 + },{ + "blob_size": 2320, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 115000000, + "id_hash": "c072513a1e96497ad7a99c2cc39182bcb4f820e42cce0f04718048424713d9b1", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261657, + "max_used_block_height": 3195160, + "max_used_block_id_hash": "2f7b8ca3dbd64cb33f428ece414b2b1cef405cfcd85fab1a70383490cc7ed603", + "receive_time": 1721261657, + "relayed": true, + "tx_blob": "0200010200108fb0ad269cfd890bb8a5980195e69e0184fc06c7f705d0d21b8df305b7c601e5da08dc0197ed03e6cd06a632cc7680bf01fb3e7cc08761a6037ca29965f27d2a145f045da5a1018ca7e6a5a5a93dbbd33d0a0003dcfa3a2800cdabeea3c3206f05408adb96d2d6fdbbf704a0372bfee0535ca036f00003992edd42ab4ef6f21ac1639c8515b5930f39788788671122993164515bedfe4c8a0003e255c00974b24fbfd3cd45fd4004d6dc76201813ac827295b1eb04e7d14d10dd9500037d42e7770c696359631ae7e566fedd1417077d2f3bdee93240f544703b299ef7e40003755335d298ae54a80e4f3fca0c1066fe744d5a2719d188c0ab10e45a94ada4d3850003e5ccb6fc793c664acfd057d7f6fcc77850032b96702ce4d7112bda338c7aa576d10003f7e699af9f0b9ff887845f0b0600816a58faf680ad32c61ee4465c643e30a340d200031de678a8a50613c5db143e7b3d5aef14be9f1f5ce2805b3a3f2fabd0af950acc380003d1af1d1d909b9d07e4cbd8d4792ce3c719280a2c4a7618d8b2be89d3e2472dab56000365159da725872bbe45cd5b5a8eb8b2082c52f161c66130ecd74aa1690e31e1cf2e2101669fe64b9088044833435038ee59827b0b3f93b042e2ea7a2de803e8889a473a06c085eb36a2d1d7927d22c62713d484cfcc32ce08d57d1588f967ef3dce69f0b186e9417afe28372e3011410c15d178d85cb7992d5489eeaa56ab735167278d301a060edacb9944a0d91f4d1890403da176509a5a8fe8b16032ba1abf24aa58fcf1f37233985203599a5196678b96873b000d7e7138e1a39ec14709f7e44ed3d5a8d9be17401d2d4d362ce2798383fc2954c8f79876471eb52ad4e7cbbd0a1647771d11aef73612f15ca15c13ac18ed477bef4492b0b229ae04b00ac4a144329a0ea2f86e4decd8ab76e8f76e604a39aa8350777c113b99f7c2808a50c8556079f6b4b1e857d75ccf4fa43429e27d1bb205c9bd8dd2ccd6151c8106eaeda4ac3f28771864870d2327e129acd4a88a16e143901bdf17e2bbba7e8934c8b05bd95a35a04a020beb1298b8ae94c2b2bf7911ad173f20a9dd9a2dfd3929a3300e8522e0db027f391f8b03ae58064b76218608c02ef5771f3058852719367c5c7cef36169357e3b9ee637eecba7f28df2ac097dad28fc995a01f42f19181d1d691f92e8e2737cac55974f4d3e0f86f674c3e84cee205f60143108ec2a3072162e9b3b82aed46df599bd192c8f923293a398281f0c67e8aae7a1a1878999827176bcfc32df00414cd6be57d8bbad907564d071e77c625005d6e497cd58d30d420d5cde92a7ef8ddcdc5e953d55eb223e95cc1e9edba4049e7728542ceacaa8bceb6dc82ce3b0294eb57476103f4669b1b446fc8b3ff970409ad02e606b08b273e39f50344351bbe3e7f1f09437982f3c9817698394df12004635da6726be57bc727d6cc2488e2d1f1a7f64cd9fffcad5281176cb43cfc36040ae73b14501d732dda57d80108176de3a885cd7ca322e7368c5d1ef63b74d38d4f94a7ea1436feead534e9bfdbe5fd186199a172462f2165026673c883f6093633257c2b940a6a5c7dc3561e7685017a7a612d59facd45ad3aedee4433d66d7f6194c0ca2b30c86c26c07ff69a8fc5c568980c3662598a19f5c63259258dacf50ee6b83395d52fba08cb0305746c9ff74c8e0431ac10b65fada1ab841921b5a70538a2a1792830106d045c51f95043ca1ee492169208ec52dd8ce307025f5297d7d40cd94668079ea06fb9b65b2ca7d8edf58062cde615fc324b921db1611f24228ff52d986087dc7c56247aff907f7434b094e2d1f6c69be514196acbf758a3ab3db25b0a80c0fc3e9076e0a934f4cf5d2bdddbd7d9af0c47fe3dfe5d8de4dc203dd4a862cde72b72f138109137de2481ea146e6758877e7bdf4d98db15e97b610acaca6ad74c33497b2ca9a3e4445dd211d89156337d0b2e6bbc5ad5f6833f36b239e79526a48fc7b86cf9c7882af011404cab69de4b9c4f2b7929e7d23763eaf5fc673c2c2755231bf1e41bf1dcd4ea549e0b610d7a449337c499817921ef2fce52795d3ac0410faea86d480fe37dfe169ec177d53a83532d27b3ccfa7663c6d727a093c62d3f7988584f579bd146fde10992155bcb47ffec0d5390e4190aaf24a8709c9954e65d370ec79a47eb973fdcee672afa283c986be21f59db0f1b914438ae83c19c43c6a0113400e6883c84aa64a788086d2e6827bf125db4f23612d7e92d8fe260b2e1e76a235ef3ab824a000713ab23f34661ccc596e2198239f29bfcc05d99a86c42fe802eed8da93490cffadfc7adc848568af250835d9e926a71c446422faa9b57cb1ee4c4b9251d8e99c43635e7ce796d0dec1918393562bb70463c67a254e58ae723dd8c2f4534ced8e8fd68c2b655a046756cd42c93b4660418b166ac1ca569cc5642462664b87d02a15cd8fcc81c335d4a131b34e880ee01241d90a51e88fbeaaeb3952f8e2c32376cb883b28445d345ccd3991632732f0b315388f9b8fc18599ac2d74bc0654e8e6f684c87a274889384dc7393b3301f04e94dc23c3c41d08ad7cebe96c4221b9c1b9f02d24ee707a28a91e7e9f9fa84073be8794c7c34beaa52086b541f9904607eb115f2111dc96b5bce5b75454214098279c030920e6d1bfbfd083421eba2365d671467b2a2a3fcba832c5a6d86a60173f228a07d33596edc8aa922a0c9b26996502b33eeec0038f74fe87485f7b6075a484e14e6c961d8417dd2cbc48029b272dd42b935f27fe7ca7afed71dceed0b5f3a4688c6ea8b0a444b442ac0396a20784f4f9f94d025e2098c482717cf4e05a1862e45b8d12b90b19773857752fe4721a49a97eedf189d25c8b0d659883d0d3e2ce49e18a2f85ecf736191b9058f16cb65d96bf12be8300642c31400e3df05a8d748a1c84d932aa94c1337ed2c6303e4e4e1e4360cb23e4879760e60501007e2d2e18200688a0d71e5fb79211614cdf88bd2cd96252cd8742a2d8ea5914f0946752e3c0ffca147d0b962337b4e8674966b3ef8ed0f7698ea50e6dfc43a8105989c046ed697d5317e81ab30add72eadaf7d5cc74e68fd4b23e1d70a3ba43d0bd64720bb8524bf732f9f5ea45fbaf38470c693834b48658c4961e4d4987e30068b15e598afbade29176f45c702a78f0af5576519e177d355d8c17e2f2a859261369cc31747f01d8cb57916b59a11ff4aba68e8ea5ead7fa431f5389f8620f759", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 80435215, 23232156, 2495160, 2601749, 114180, 97223, 452944, 96653, 25399, 142693, 220, 63127, 108262, 6438, 15180, 24448\n ], \n \"k_image\": \"fb3e7cc08761a6037ca29965f27d2a145f045da5a1018ca7e6a5a5a93dbbd33d\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"dcfa3a2800cdabeea3c3206f05408adb96d2d6fdbbf704a0372bfee0535ca036\", \n \"view_tag\": \"f0\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"992edd42ab4ef6f21ac1639c8515b5930f39788788671122993164515bedfe4c\", \n \"view_tag\": \"8a\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"e255c00974b24fbfd3cd45fd4004d6dc76201813ac827295b1eb04e7d14d10dd\", \n \"view_tag\": \"95\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"7d42e7770c696359631ae7e566fedd1417077d2f3bdee93240f544703b299ef7\", \n \"view_tag\": \"e4\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"755335d298ae54a80e4f3fca0c1066fe744d5a2719d188c0ab10e45a94ada4d3\", \n \"view_tag\": \"85\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"e5ccb6fc793c664acfd057d7f6fcc77850032b96702ce4d7112bda338c7aa576\", \n \"view_tag\": \"d1\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"f7e699af9f0b9ff887845f0b0600816a58faf680ad32c61ee4465c643e30a340\", \n \"view_tag\": \"d2\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"1de678a8a50613c5db143e7b3d5aef14be9f1f5ce2805b3a3f2fabd0af950acc\", \n \"view_tag\": \"38\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"d1af1d1d909b9d07e4cbd8d4792ce3c719280a2c4a7618d8b2be89d3e2472dab\", \n \"view_tag\": \"56\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"65159da725872bbe45cd5b5a8eb8b2082c52f161c66130ecd74aa1690e31e1cf\", \n \"view_tag\": \"2e\"\n }\n }\n }\n ], \n \"extra\": [ 1, 102, 159, 230, 75, 144, 136, 4, 72, 51, 67, 80, 56, 238, 89, 130, 123, 11, 63, 147, 176, 66, 226, 234, 122, 45, 232, 3, 232, 136, 154, 71, 58\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 115000000, \n \"ecdhInfo\": [ {\n \"amount\": \"a2d1d7927d22c627\"\n }, {\n \"amount\": \"13d484cfcc32ce08\"\n }, {\n \"amount\": \"d57d1588f967ef3d\"\n }, {\n \"amount\": \"ce69f0b186e9417a\"\n }, {\n \"amount\": \"fe28372e3011410c\"\n }, {\n \"amount\": \"15d178d85cb7992d\"\n }, {\n \"amount\": \"5489eeaa56ab7351\"\n }, {\n \"amount\": \"67278d301a060eda\"\n }, {\n \"amount\": \"cb9944a0d91f4d18\"\n }, {\n \"amount\": \"90403da176509a5a\"\n }], \n \"outPk\": [ \"8fe8b16032ba1abf24aa58fcf1f37233985203599a5196678b96873b000d7e71\", \"38e1a39ec14709f7e44ed3d5a8d9be17401d2d4d362ce2798383fc2954c8f798\", \"76471eb52ad4e7cbbd0a1647771d11aef73612f15ca15c13ac18ed477bef4492\", \"b0b229ae04b00ac4a144329a0ea2f86e4decd8ab76e8f76e604a39aa8350777c\", \"113b99f7c2808a50c8556079f6b4b1e857d75ccf4fa43429e27d1bb205c9bd8d\", \"d2ccd6151c8106eaeda4ac3f28771864870d2327e129acd4a88a16e143901bdf\", \"17e2bbba7e8934c8b05bd95a35a04a020beb1298b8ae94c2b2bf7911ad173f20\", \"a9dd9a2dfd3929a3300e8522e0db027f391f8b03ae58064b76218608c02ef577\", \"1f3058852719367c5c7cef36169357e3b9ee637eecba7f28df2ac097dad28fc9\", \"95a01f42f19181d1d691f92e8e2737cac55974f4d3e0f86f674c3e84cee205f6\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"43108ec2a3072162e9b3b82aed46df599bd192c8f923293a398281f0c67e8aae\", \n \"A1\": \"7a1a1878999827176bcfc32df00414cd6be57d8bbad907564d071e77c625005d\", \n \"B\": \"6e497cd58d30d420d5cde92a7ef8ddcdc5e953d55eb223e95cc1e9edba4049e7\", \n \"r1\": \"728542ceacaa8bceb6dc82ce3b0294eb57476103f4669b1b446fc8b3ff970409\", \n \"s1\": \"ad02e606b08b273e39f50344351bbe3e7f1f09437982f3c9817698394df12004\", \n \"d1\": \"635da6726be57bc727d6cc2488e2d1f1a7f64cd9fffcad5281176cb43cfc3604\", \n \"L\": [ \"e73b14501d732dda57d80108176de3a885cd7ca322e7368c5d1ef63b74d38d4f\", \"94a7ea1436feead534e9bfdbe5fd186199a172462f2165026673c883f6093633\", \"257c2b940a6a5c7dc3561e7685017a7a612d59facd45ad3aedee4433d66d7f61\", \"94c0ca2b30c86c26c07ff69a8fc5c568980c3662598a19f5c63259258dacf50e\", \"e6b83395d52fba08cb0305746c9ff74c8e0431ac10b65fada1ab841921b5a705\", \"38a2a1792830106d045c51f95043ca1ee492169208ec52dd8ce307025f5297d7\", \"d40cd94668079ea06fb9b65b2ca7d8edf58062cde615fc324b921db1611f2422\", \"8ff52d986087dc7c56247aff907f7434b094e2d1f6c69be514196acbf758a3ab\", \"3db25b0a80c0fc3e9076e0a934f4cf5d2bdddbd7d9af0c47fe3dfe5d8de4dc20\", \"3dd4a862cde72b72f138109137de2481ea146e6758877e7bdf4d98db15e97b61\"\n ], \n \"R\": [ \"caca6ad74c33497b2ca9a3e4445dd211d89156337d0b2e6bbc5ad5f6833f36b2\", \"39e79526a48fc7b86cf9c7882af011404cab69de4b9c4f2b7929e7d23763eaf5\", \"fc673c2c2755231bf1e41bf1dcd4ea549e0b610d7a449337c499817921ef2fce\", \"52795d3ac0410faea86d480fe37dfe169ec177d53a83532d27b3ccfa7663c6d7\", \"27a093c62d3f7988584f579bd146fde10992155bcb47ffec0d5390e4190aaf24\", \"a8709c9954e65d370ec79a47eb973fdcee672afa283c986be21f59db0f1b9144\", \"38ae83c19c43c6a0113400e6883c84aa64a788086d2e6827bf125db4f23612d7\", \"e92d8fe260b2e1e76a235ef3ab824a000713ab23f34661ccc596e2198239f29b\", \"fcc05d99a86c42fe802eed8da93490cffadfc7adc848568af250835d9e926a71\", \"c446422faa9b57cb1ee4c4b9251d8e99c43635e7ce796d0dec1918393562bb70\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"463c67a254e58ae723dd8c2f4534ced8e8fd68c2b655a046756cd42c93b46604\", \"18b166ac1ca569cc5642462664b87d02a15cd8fcc81c335d4a131b34e880ee01\", \"241d90a51e88fbeaaeb3952f8e2c32376cb883b28445d345ccd3991632732f0b\", \"315388f9b8fc18599ac2d74bc0654e8e6f684c87a274889384dc7393b3301f04\", \"e94dc23c3c41d08ad7cebe96c4221b9c1b9f02d24ee707a28a91e7e9f9fa8407\", \"3be8794c7c34beaa52086b541f9904607eb115f2111dc96b5bce5b7545421409\", \"8279c030920e6d1bfbfd083421eba2365d671467b2a2a3fcba832c5a6d86a601\", \"73f228a07d33596edc8aa922a0c9b26996502b33eeec0038f74fe87485f7b607\", \"5a484e14e6c961d8417dd2cbc48029b272dd42b935f27fe7ca7afed71dceed0b\", \"5f3a4688c6ea8b0a444b442ac0396a20784f4f9f94d025e2098c482717cf4e05\", \"a1862e45b8d12b90b19773857752fe4721a49a97eedf189d25c8b0d659883d0d\", \"3e2ce49e18a2f85ecf736191b9058f16cb65d96bf12be8300642c31400e3df05\", \"a8d748a1c84d932aa94c1337ed2c6303e4e4e1e4360cb23e4879760e60501007\", \"e2d2e18200688a0d71e5fb79211614cdf88bd2cd96252cd8742a2d8ea5914f09\", \"46752e3c0ffca147d0b962337b4e8674966b3ef8ed0f7698ea50e6dfc43a8105\", \"989c046ed697d5317e81ab30add72eadaf7d5cc74e68fd4b23e1d70a3ba43d0b\"], \n \"c1\": \"d64720bb8524bf732f9f5ea45fbaf38470c693834b48658c4961e4d4987e3006\", \n \"D\": \"8b15e598afbade29176f45c702a78f0af5576519e177d355d8c17e2f2a859261\"\n }], \n \"pseudoOuts\": [ \"369cc31747f01d8cb57916b59a11ff4aba68e8ea5ead7fa431f5389f8620f759\"]\n }\n}", + "weight": 5750 + },{ + "blob_size": 1879, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 66240000, + "id_hash": "d696e0a07d4a5315239fda1d2fec3fa94c7f87148e254a2e6ce8a648bed86bb3", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261654, + "max_used_block_height": 3195160, + "max_used_block_id_hash": "2f7b8ca3dbd64cb33f428ece414b2b1cef405cfcd85fab1a70383490cc7ed603", + "receive_time": 1721261653, + "relayed": true, + "tx_blob": "020001020010d4cbc025f888800aa9b3da02d483d801b4c3188ad105b38a0ee7c3089831b542af37b3f901e68006885282d2028701428be79097b510e49fe5b25804029ac8bfa5e2a640a8b0e3e0a8199b1d26f22f050003f4c8ce3812f84c4dae9fab0e120faf7e9583c5bb024674a4ceb36550a58da368690003edcedc3b21577e2cb73380c7a829cc6707109e672cee030aa007618ff033ed9e8c00037d3b66c65d73e2c172c52ef1336987487da2507fc7c6ca0579ff7e8d34c47f4d170003d423aefc2536b8839995f569dc5e61385f8a537067f5c00aac957cccfb5a4790b5000397fcebd0877bdd71d2a90aed5a58180a0deecaed068f9e43ce44df011aecae0e382101430ad497db87046e33184d9617daa64add82a53dc16982a056b682b2f9a2da980680fcca1f6a94c40de9d6fb8935d837180bad6fc5bb224d8ed00140afb3123c36a4ff194f108db26877967b024bbd0cb51d34a7f5cb26262573b9a66cd70635204820f80d01f04297474b9ca965cdc9302126eebe75aa86a0a837f9dcface5fa38ad81bdcaa40e1fe942f2fc2768f267feece915df41d9864291b57885c1e0f8e4c93a7a4610aef477cf53b805dad5a1006071fde67ca8c47f545c2fdf3202120ef652e0173b4257b91811f7f39df6d0c9c1d2e8f00074c619e9e4322dd006c41c10b67c4ed21aae62ce8748701865c7cb3df6f168e9564b1c7f0f7ac8429f2ecc311a10557feec8395cd8e03be8f38908c2fee85f557cdb534d9775ae9583f7a73024d9a7efa1d75b74f4e62b0a40954b6ca4c0c2091beb3021f21cce994f5a679c0f45126a83d9cb431ab6babb1d3272ed9456ea6af5c35037b4a89a23640c6edf1a4576d592bbc5dd0c4f40aa3e918712d4812f5bc8a01e38e6dc1d4b5dc39ccb5e26eeaf5c88b85d968c800e10bfca42e320f6b03c3065856b58908555a266b7308321ba94e80aab0956b0c09e2f1c8c5da991672dea0c43de4c27d232f50c35e3fc836ac8eec7d6466d8af2ce9277a92aca9f4d4d4230d1dfbb042871dc92498339a6225d6ac016e0e9dde852364c31878a19a2820bd196c1f837b398a56152ae01928f9f2fb80aae7301795da93006123b6cea54f34f1232a9f1ddafa340764a5b0b305af112c72c76a6ed5d5513739fbbc56bc027b4251b23a8c7727a2abf5b3e9a803aa3d144022d129fede2abec4f787d574df639bd48d0de0d1e981d88de680a7d40033b0f93b51ed6e60b8199c2adbc7de0b8f76802d8e74d9e46d2145a61232920571bc1d23fce134c02c268b523f5d5d31876d2259b2292597c15937c3057e5c4350391327429428ac6069bdd5db4515113d046f8058506b6e04399016157964b7c7dd41b929e9fb09997f51fa6db7903465b3cab1093ad38a783b67542e75bde42bbf5c07e415c24241017f2365a3e05b1bb8eab7b4ef681d3208a98715fd84e60be86931800276ce667812e9602afa5baacb0caa1ef9fc37cbac120918564ca04d73ce130ca42c28577d504516e5f44b843a76b2c1558d142e64ce4a55c34bf197a0251022a94e973f59a4352fe5e086e79fc90319e29834ab75bb24c4438c383f19ff77463d0f322566c1b5c64f1dd5ab8472ae4e8b4ac20ab110f5281a848ceca73330fc45d3366b7bd1accb139fec1ed99f15d11ef3ec2e7207a5e7bdafa136a85ccb0a55c3e77c86a70c2496fbcd0373c8aac6067731183cf211bbefbc7d71269d6ba62c98aa791613cbd267b4424763141075bf53d6c4472d3f7685f73f726b16caff0b90183a71c42ca6a27bf6d818abea8c67eb90ce4aca1f9efcb43215214336da99b4074faba4a99c9cfb429e70759024f3d5ce150e9e97e77c4f70501efc575c485e0aea5ed0f246930d73be9d8662f41a408323beacd53135df2eeea93722a00bfc026f5827dccdb05f8e9fedbfc7632b36324d891eb8220978da3ad6e4ad1f3e3507f4b7b8f4914326b28fda56141b2be6350c9c8499afbe66da9417d03598c40f05ec043be6b64b2aa2a0a5da6ab9825d6ebc03578ef5e7370ea63d051cb9f87e01933cfe48a5b22c49d6aca2cf869f6e98bb89c51c6fc9a5cea61a03920214aa0a014a19d0f3fe97670e3990021dae0fdbb5ed4979b1e2144ec3d1ba4b68c39205a86c3350cf2315a25c4ddb21d24ff2a3bdd5e0c53556e2c917920059781700081bdc3fd3966568487382cdb556e8fce96995dc2fb2d598edd55abb423e1ab50b74fdd8568356d62c742394e7bd5e1869a8f28a8557525664b8b28ab14c3d1108e4bae38d05447408f71bdb233aab9cd9a740e242794b9c75760064fdc90c2e0b902b407f311b34d9a172731fe7aa10c9bd189071bdf6f24be900fd8711a08407052db276f4ec535ffb2341e486de7e4ef4b0bbd10de4409c80221a653455930b82edf3a40421c3c665bba621821945a05a7c6c7ca3946f0b673a7e8fa2023d0e2b05b2fc21ffbdb68b0ccb9e0aacb2623f1a843896e68eb7157f8d115a3d7e0e02fd894c154ea56eb1b0d8f296325d3a71f581b868cbade1bbb50b73bf52fa0883cf71eb20aae786f9b27637e3d19ae75ae5206c0a6f789241741bfee9ce05259fe40f2628445d7d741b152930c35ab1c40aba265817242be986971e83665677", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 78652884, 20972664, 5675433, 3539412, 401844, 92298, 230707, 139751, 6296, 8501, 7087, 31923, 98406, 10504, 43266, 135\n ], \n \"k_image\": \"428be79097b510e49fe5b25804029ac8bfa5e2a640a8b0e3e0a8199b1d26f22f\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"f4c8ce3812f84c4dae9fab0e120faf7e9583c5bb024674a4ceb36550a58da368\", \n \"view_tag\": \"69\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"edcedc3b21577e2cb73380c7a829cc6707109e672cee030aa007618ff033ed9e\", \n \"view_tag\": \"8c\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"7d3b66c65d73e2c172c52ef1336987487da2507fc7c6ca0579ff7e8d34c47f4d\", \n \"view_tag\": \"17\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"d423aefc2536b8839995f569dc5e61385f8a537067f5c00aac957cccfb5a4790\", \n \"view_tag\": \"b5\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"97fcebd0877bdd71d2a90aed5a58180a0deecaed068f9e43ce44df011aecae0e\", \n \"view_tag\": \"38\"\n }\n }\n }\n ], \n \"extra\": [ 1, 67, 10, 212, 151, 219, 135, 4, 110, 51, 24, 77, 150, 23, 218, 166, 74, 221, 130, 165, 61, 193, 105, 130, 160, 86, 182, 130, 178, 249, 162, 218, 152\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 66240000, \n \"ecdhInfo\": [ {\n \"amount\": \"6a94c40de9d6fb89\"\n }, {\n \"amount\": \"35d837180bad6fc5\"\n }, {\n \"amount\": \"bb224d8ed00140af\"\n }, {\n \"amount\": \"b3123c36a4ff194f\"\n }, {\n \"amount\": \"108db26877967b02\"\n }], \n \"outPk\": [ \"4bbd0cb51d34a7f5cb26262573b9a66cd70635204820f80d01f04297474b9ca9\", \"65cdc9302126eebe75aa86a0a837f9dcface5fa38ad81bdcaa40e1fe942f2fc2\", \"768f267feece915df41d9864291b57885c1e0f8e4c93a7a4610aef477cf53b80\", \"5dad5a1006071fde67ca8c47f545c2fdf3202120ef652e0173b4257b91811f7f\", \"39df6d0c9c1d2e8f00074c619e9e4322dd006c41c10b67c4ed21aae62ce87487\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"865c7cb3df6f168e9564b1c7f0f7ac8429f2ecc311a10557feec8395cd8e03be\", \n \"A1\": \"8f38908c2fee85f557cdb534d9775ae9583f7a73024d9a7efa1d75b74f4e62b0\", \n \"B\": \"a40954b6ca4c0c2091beb3021f21cce994f5a679c0f45126a83d9cb431ab6bab\", \n \"r1\": \"b1d3272ed9456ea6af5c35037b4a89a23640c6edf1a4576d592bbc5dd0c4f40a\", \n \"s1\": \"a3e918712d4812f5bc8a01e38e6dc1d4b5dc39ccb5e26eeaf5c88b85d968c800\", \n \"d1\": \"e10bfca42e320f6b03c3065856b58908555a266b7308321ba94e80aab0956b0c\", \n \"L\": [ \"e2f1c8c5da991672dea0c43de4c27d232f50c35e3fc836ac8eec7d6466d8af2c\", \"e9277a92aca9f4d4d4230d1dfbb042871dc92498339a6225d6ac016e0e9dde85\", \"2364c31878a19a2820bd196c1f837b398a56152ae01928f9f2fb80aae7301795\", \"da93006123b6cea54f34f1232a9f1ddafa340764a5b0b305af112c72c76a6ed5\", \"d5513739fbbc56bc027b4251b23a8c7727a2abf5b3e9a803aa3d144022d129fe\", \"de2abec4f787d574df639bd48d0de0d1e981d88de680a7d40033b0f93b51ed6e\", \"60b8199c2adbc7de0b8f76802d8e74d9e46d2145a61232920571bc1d23fce134\", \"c02c268b523f5d5d31876d2259b2292597c15937c3057e5c4350391327429428\", \"ac6069bdd5db4515113d046f8058506b6e04399016157964b7c7dd41b929e9fb\"\n ], \n \"R\": [ \"997f51fa6db7903465b3cab1093ad38a783b67542e75bde42bbf5c07e415c242\", \"41017f2365a3e05b1bb8eab7b4ef681d3208a98715fd84e60be86931800276ce\", \"667812e9602afa5baacb0caa1ef9fc37cbac120918564ca04d73ce130ca42c28\", \"577d504516e5f44b843a76b2c1558d142e64ce4a55c34bf197a0251022a94e97\", \"3f59a4352fe5e086e79fc90319e29834ab75bb24c4438c383f19ff77463d0f32\", \"2566c1b5c64f1dd5ab8472ae4e8b4ac20ab110f5281a848ceca73330fc45d336\", \"6b7bd1accb139fec1ed99f15d11ef3ec2e7207a5e7bdafa136a85ccb0a55c3e7\", \"7c86a70c2496fbcd0373c8aac6067731183cf211bbefbc7d71269d6ba62c98aa\", \"791613cbd267b4424763141075bf53d6c4472d3f7685f73f726b16caff0b9018\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"3a71c42ca6a27bf6d818abea8c67eb90ce4aca1f9efcb43215214336da99b407\", \"4faba4a99c9cfb429e70759024f3d5ce150e9e97e77c4f70501efc575c485e0a\", \"ea5ed0f246930d73be9d8662f41a408323beacd53135df2eeea93722a00bfc02\", \"6f5827dccdb05f8e9fedbfc7632b36324d891eb8220978da3ad6e4ad1f3e3507\", \"f4b7b8f4914326b28fda56141b2be6350c9c8499afbe66da9417d03598c40f05\", \"ec043be6b64b2aa2a0a5da6ab9825d6ebc03578ef5e7370ea63d051cb9f87e01\", \"933cfe48a5b22c49d6aca2cf869f6e98bb89c51c6fc9a5cea61a03920214aa0a\", \"014a19d0f3fe97670e3990021dae0fdbb5ed4979b1e2144ec3d1ba4b68c39205\", \"a86c3350cf2315a25c4ddb21d24ff2a3bdd5e0c53556e2c91792005978170008\", \"1bdc3fd3966568487382cdb556e8fce96995dc2fb2d598edd55abb423e1ab50b\", \"74fdd8568356d62c742394e7bd5e1869a8f28a8557525664b8b28ab14c3d1108\", \"e4bae38d05447408f71bdb233aab9cd9a740e242794b9c75760064fdc90c2e0b\", \"902b407f311b34d9a172731fe7aa10c9bd189071bdf6f24be900fd8711a08407\", \"052db276f4ec535ffb2341e486de7e4ef4b0bbd10de4409c80221a653455930b\", \"82edf3a40421c3c665bba621821945a05a7c6c7ca3946f0b673a7e8fa2023d0e\", \"2b05b2fc21ffbdb68b0ccb9e0aacb2623f1a843896e68eb7157f8d115a3d7e0e\"], \n \"c1\": \"02fd894c154ea56eb1b0d8f296325d3a71f581b868cbade1bbb50b73bf52fa08\", \n \"D\": \"83cf71eb20aae786f9b27637e3d19ae75ae5206c0a6f789241741bfee9ce0525\"\n }], \n \"pseudoOuts\": [ \"9fe40f2628445d7d741b152930c35ab1c40aba265817242be986971e83665677\"]\n }\n}", + "weight": 3312 + },{ + "blob_size": 1539, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 492480000, + "id_hash": "a60834967cc6d22e61acbc298d5d2c725cbf5c8c492b999f3420da126f41b6c7", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261654, + "max_used_block_height": 3195144, + "max_used_block_id_hash": "464cb0e47663a64ee8eaf483c46d6584e9a7945a0c792b19cdbde426ec3a5034", + "receive_time": 1721261653, + "relayed": true, + "tx_blob": "020001020010dab5ae1feefaf606fea6c609aca1ec018fa0e70186de3df48307c5b72ab9a001dea302de3c9c17c2b701a923a801d302d2ed8513f48724933df6229a9fb6ededdcf5d0963280ee44fa9216ceebe7941f020003c9558a2daba528058e11b62fb334f0c1324096575db177d3676f2f3a7287f154640003cfc87315c4bbbfc8053d6f4921465da881c2675a531141d272c7b4e085829909c82c01af5114379f90bb68c84743bedf90bd63aa394ea61191a0c315c9816d8e0b9efd020901d1c905878ab04b850680cceaea016f7857e7ade8bd0ee83ef9696bc9b3823e5b43d81c8f49c1f4c6e6ccdb93f7d3d500bcc38801eba594e5377f8339c5e2baed73a25fdcb6162c8cac13ab20adf8fa0652db145499fff83ca114f5b5c39b01d0531831fce743ab9dcdd3571f8a93c433dfa17e9885741b50e34ed92e5d3b41b43bd7a99784d3e95385b94d578222b3bd7e8f0d981a318c424c98eaeffb9cab22da9b9ff6d666a72f2e56751796015b36634f455d2b1f99874b6b30707a20961c12819b752f60ec321436fe0d3bfd681b14048578b14eef8bbedad3f672fc010518a1c63af4ed08d8e7b875641eff37acf9fe46ca0a1b428ea685131eab8003a3cbe82bb87769ab6c5763eb468d04c985a650dde6db581ed3926f44f68bd9040796c94d7d0655ace603e046b12e0267718d575ac32b6fbfe0b911158198d2c5bc08c54b7f62ed326d8965c90e9bb29ad63fcb6d1d4ca5f849aba2eccb5a51ed1d28f2eceda481bd1f3a0229a9cce6b8816831ff5ac7d129f8f3088cdcfe356d73cd4b07a4453a016b3cd65bcb3ac7f85bed185044ffced69b4e111af67582c85551a1fee0e370eb1360611653b571f6dd35f1ab90b1af509dd630b88e6851322e65f1189e46bde8175c4add525e5975c8de17b718155ba857dcba3695543ef6e6a71d548c4940850707eeaf3769839ec636f3e0088d92bbdd5eb2e508529cfb4c078cfa9204c35863256579aefad74fbc7e411174375840bc3070c74939a0de834131dabafa47de92c03d38384d870f3b65f73e7e411ca4130de1f0a706399b64f63dc809524dcdcc76b3a1c0eb963384c39779125ec10c7eeccf9190200a76c4df88dedc7a6b7661ef39be599844c88f5320a1288db2f6395f73907851f966fe67c584ec77c89b15e597254b6550c460f4607c9aa7287577c6c1fc3c3b0b01f0f30818dce251455e18138c4509100cf7c1c79e9b0d332a57a0e3734b8bdaddfa0a3233560a441f2bff47b98de49b8c87e5401e7dfe844b7ad4e3b669394e44816e4cb057b6afce2a9fe3915084deae4d047e698b6753ec6da7e4b3a34babe73206a5990f57933939c242e27891786882631d5fd264e26d2853aba16336ae15070f00602edfbfd9abf9702c7cf9607b52ad0a04d08518054c7f4cada448853092090164292412d8c4196fb4ddb842669ea9de50421b6f219cb05a0befe63f618f043ececbc47f1e3547e4e7abe935c972ed8f60258e1bb7b7b97a74109bfb5b6a03308939028b18f56c3128641e4bf7c7c5884fc05ee7ebdb76b85ad30cb9a1d90680dfdfecbfee9ea2e98bc50e3021f165dcf3890713572b0847f4767a244dc908c8931d54a79dbc34f35888485b977aa7d809b3513aec10d4a522175edfcc9e044a15f68a6e1f5b2095f9c7791984e794b75d3f9940b602b8984773443e4c760bf28dca4e93a036aa4be7771073d23aca7732aae7a364c628437064dc5e8dbf0dfb1fe2333a90b6950dc764e90840ebb74fb98803afe6ccd792ad637183747402c3218537f5b9416b9559b6cadd148c5a8bb52b4f92be7cf130ef729227acac092c13e6c8a9b5046688b81ded00f5ccfbd2f70bfb5aeec37de7d7ea4097feb5005590fab38475f3e37dcc9df3fa3a176d8f45e732acfd12e3f680bc4b511aa90e8c853f0d294f351f8a16a486dbd7bdbe582d48ff956a7c17d719220293d9b3088b24cbaa956ee96d662fe1f30b3d477c96dd3fab9902fcecf8692725440a380f5ffe1c8872ada9fcba8ad026cbb5e9d8324943cb5ebde77eefdcd5bb175c750a577032337113d6d1f45d488bfedbc3a25427633bb1873eb45a9a4ea726c33c0b297af95c01880e1f3bcb0ac86f4d7105e5c6cee032282fc50aa2d34c06b4c4bb", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 65772250, 14531950, 20026238, 3870892, 3788815, 1011462, 115188, 695237, 20537, 37342, 7774, 2972, 23490, 4521, 168, 339\n ], \n \"k_image\": \"d2ed8513f48724933df6229a9fb6ededdcf5d0963280ee44fa9216ceebe7941f\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"c9558a2daba528058e11b62fb334f0c1324096575db177d3676f2f3a7287f154\", \n \"view_tag\": \"64\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"cfc87315c4bbbfc8053d6f4921465da881c2675a531141d272c7b4e085829909\", \n \"view_tag\": \"c8\"\n }\n }\n }\n ], \n \"extra\": [ 1, 175, 81, 20, 55, 159, 144, 187, 104, 200, 71, 67, 190, 223, 144, 189, 99, 170, 57, 78, 166, 17, 145, 160, 195, 21, 201, 129, 109, 142, 11, 158, 253, 2, 9, 1, 209, 201, 5, 135, 138, 176, 75, 133\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 492480000, \n \"ecdhInfo\": [ {\n \"amount\": \"6f7857e7ade8bd0e\"\n }, {\n \"amount\": \"e83ef9696bc9b382\"\n }], \n \"outPk\": [ \"3e5b43d81c8f49c1f4c6e6ccdb93f7d3d500bcc38801eba594e5377f8339c5e2\", \"baed73a25fdcb6162c8cac13ab20adf8fa0652db145499fff83ca114f5b5c39b\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"d0531831fce743ab9dcdd3571f8a93c433dfa17e9885741b50e34ed92e5d3b41\", \n \"A1\": \"b43bd7a99784d3e95385b94d578222b3bd7e8f0d981a318c424c98eaeffb9cab\", \n \"B\": \"22da9b9ff6d666a72f2e56751796015b36634f455d2b1f99874b6b30707a2096\", \n \"r1\": \"1c12819b752f60ec321436fe0d3bfd681b14048578b14eef8bbedad3f672fc01\", \n \"s1\": \"0518a1c63af4ed08d8e7b875641eff37acf9fe46ca0a1b428ea685131eab8003\", \n \"d1\": \"a3cbe82bb87769ab6c5763eb468d04c985a650dde6db581ed3926f44f68bd904\", \n \"L\": [ \"96c94d7d0655ace603e046b12e0267718d575ac32b6fbfe0b911158198d2c5bc\", \"08c54b7f62ed326d8965c90e9bb29ad63fcb6d1d4ca5f849aba2eccb5a51ed1d\", \"28f2eceda481bd1f3a0229a9cce6b8816831ff5ac7d129f8f3088cdcfe356d73\", \"cd4b07a4453a016b3cd65bcb3ac7f85bed185044ffced69b4e111af67582c855\", \"51a1fee0e370eb1360611653b571f6dd35f1ab90b1af509dd630b88e6851322e\", \"65f1189e46bde8175c4add525e5975c8de17b718155ba857dcba3695543ef6e6\", \"a71d548c4940850707eeaf3769839ec636f3e0088d92bbdd5eb2e508529cfb4c\"\n ], \n \"R\": [ \"8cfa9204c35863256579aefad74fbc7e411174375840bc3070c74939a0de8341\", \"31dabafa47de92c03d38384d870f3b65f73e7e411ca4130de1f0a706399b64f6\", \"3dc809524dcdcc76b3a1c0eb963384c39779125ec10c7eeccf9190200a76c4df\", \"88dedc7a6b7661ef39be599844c88f5320a1288db2f6395f73907851f966fe67\", \"c584ec77c89b15e597254b6550c460f4607c9aa7287577c6c1fc3c3b0b01f0f3\", \"0818dce251455e18138c4509100cf7c1c79e9b0d332a57a0e3734b8bdaddfa0a\", \"3233560a441f2bff47b98de49b8c87e5401e7dfe844b7ad4e3b669394e44816e\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"4cb057b6afce2a9fe3915084deae4d047e698b6753ec6da7e4b3a34babe73206\", \"a5990f57933939c242e27891786882631d5fd264e26d2853aba16336ae15070f\", \"00602edfbfd9abf9702c7cf9607b52ad0a04d08518054c7f4cada44885309209\", \"0164292412d8c4196fb4ddb842669ea9de50421b6f219cb05a0befe63f618f04\", \"3ececbc47f1e3547e4e7abe935c972ed8f60258e1bb7b7b97a74109bfb5b6a03\", \"308939028b18f56c3128641e4bf7c7c5884fc05ee7ebdb76b85ad30cb9a1d906\", \"80dfdfecbfee9ea2e98bc50e3021f165dcf3890713572b0847f4767a244dc908\", \"c8931d54a79dbc34f35888485b977aa7d809b3513aec10d4a522175edfcc9e04\", \"4a15f68a6e1f5b2095f9c7791984e794b75d3f9940b602b8984773443e4c760b\", \"f28dca4e93a036aa4be7771073d23aca7732aae7a364c628437064dc5e8dbf0d\", \"fb1fe2333a90b6950dc764e90840ebb74fb98803afe6ccd792ad637183747402\", \"c3218537f5b9416b9559b6cadd148c5a8bb52b4f92be7cf130ef729227acac09\", \"2c13e6c8a9b5046688b81ded00f5ccfbd2f70bfb5aeec37de7d7ea4097feb500\", \"5590fab38475f3e37dcc9df3fa3a176d8f45e732acfd12e3f680bc4b511aa90e\", \"8c853f0d294f351f8a16a486dbd7bdbe582d48ff956a7c17d719220293d9b308\", \"8b24cbaa956ee96d662fe1f30b3d477c96dd3fab9902fcecf8692725440a380f\"], \n \"c1\": \"5ffe1c8872ada9fcba8ad026cbb5e9d8324943cb5ebde77eefdcd5bb175c750a\", \n \"D\": \"577032337113d6d1f45d488bfedbc3a25427633bb1873eb45a9a4ea726c33c0b\"\n }], \n \"pseudoOuts\": [ \"297af95c01880e1f3bcb0ac86f4d7105e5c6cee032282fc50aa2d34c06b4c4bb\"]\n }\n}", + "weight": 1539 + },{ + "blob_size": 1536, + "do_not_relay": false, + "double_spend_seen": false, + "fee": 491520000, + "id_hash": "aef60754dc1b2cd788faf23dd3c62afd3a0ac14e088cd6c8d22f1597860e47cd", + "kept_by_block": false, + "last_failed_height": 0, + "last_failed_id_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "last_relayed_time": 1721261653, + "max_used_block_height": 3195160, + "max_used_block_id_hash": "2f7b8ca3dbd64cb33f428ece414b2b1cef405cfcd85fab1a70383490cc7ed603", + "receive_time": 1721261653, + "relayed": true, + "tx_blob": "02000102001081d9e02d98cd8606d18f0cbc9919ce9704e78b04bdeb03e28806af8601cec110c54dafc502a6118b1c9743c7052c479dbff819502441604a914af485db2f795b7f5bc0eab877d60a1419ee54980200031085022c023e67ccef3004d93b200be5df0cca89c5a17c51d76c3ee3c107d39190000332dd926cf0eac221cabeb5beee0cc9ef68a07a79748a69c3142377b5a8dbd6c7be2c019eb1fa0d8e9c4853c7db214bf6547ddd8b23bec77445b208e296fb26d0861a5b0209016ae500293b5f1167068080b0ea0113648ac5774104fe28d4ea6e7e28511a1c5c4189735a8c2e98bb12572283f3797ff1273d5dd44312e10cbb4cfdf34c3f7664dfc643db30f011129a53843d647e7cab9e03c2df68ef71b2040d59005f4501fb8885aaa4fc7f5fcae273f7a4612c629423864cb4d12c06044dad6629f40edfb2d6cb3c9dd78407302087c767c8bf17d507471a9cb9af54dc6528de99c8dd62ca69f07a623f4ca679eccc0fa03568e346cc061ff1624c4b73ee72a7a414c68164bc2abdf7f7798e6a0f13b2c3852371c2db35ebb00c7ba018a42b0c2c31fc06a748b257eafb46efc220a57a7767664f7744420bc3ee1c7c227aa711680dd301accf87381433838debd8e517ba86d06cb7475f3b8dfa301ee879112ab2e7a00807200f21ba8eeb86935deb899b3babb7a3ebb21b2f1dfd1bb08015ae4b8ff4f16356c7e807b6bf00f6178c752d92f9630508eeccf307f8efdc7dfae7592555a0824706000a2f329413ffe6ecfe68345053f441db1815286b951cbf83b4e7c327e27019fe81f3c534b297b9388438ffb1d5acd17514678a87719b843d64df591d1e478fdac7f3940088cfe0ea54a896fbcad933fb85c9e3f060db515cc8495f4a8939102799616733953f6bb959916ebdb8cfb8c958f62e787aa7b4a3bd45c311cd83be8617ee592413d8c76bf05922147cb08447e7b899344e9c7069cf710fe22a07744704b4b118e85e16e5dc3abc02e9e6fd45e95c73ec2412355907bc4a5d3d17849043695696eb46dbeb1389bc544c5ec11d5e35a9d9be1e5aa2884377c2f4410773a62507f87e7fe992e335377585bf63a7554e352066574e4e35fdf5d1c90a7ffa6b791e317f2c7c76a85532a7ae18a74c293e3ce9c853080fe23abc42d97fb8a8320d3586343706a56e8f293322aba82e9c159a41a3ceff344177dddc3df947cf9159c194d6b9c392dd76a5ddca3f1110ca3043f7b2e82ef1b55fbc87e8b3063bd71e497c90ae647f0c21719149341298ab1402e9bbee4b674bc9509070df1e5de5f3c400ef5230773a8c5b6fb1dc556bb25a8d0110b4c6f411785927930bcbc5b5a9057c4c6b55397a8000f4d8b6cc6159a4e642aa449512e56b405beb0ee74de267b38c7135eaba1f4171e7fbe7fa79807eba65b20a78e896c94e47ae0519d587d550fbf22015c99d604c7f55019901ac5fcbd674d0d2fd6d752087ca056f89bd649fc33ddb75888ecc0327e66d0642683825f262c5cec006599bdf1f07b0f9af7553379e798af1e9644d988e703c96ae415e22d053d4c42fbb19c44d062eec4bf6ce7888a7438c2f1d5b94a415ee8ff3bd95045429378b8c2b9fb7c600656b5a6f94b1149793517bce19d0ddf2f5b30fe3a16bbebed91ee0a03c47a003778322b5d214b960acc4b9b2cdd1e55c4aed04199aa9a0b5132b2d3553c3fe087b9ffbbd7409e47528c075ee1edc51e6cedd5fe05998fddab5a3f25edb0ff2083fcf87e8e261df81a9cf178f5facd906821a543560d3f5d0bb07cf0402d69302463bc90bf04cce521b1b115ea4c57c6512695ade926f3be142001ee73e7f130bc9e506619c93bdae690697a0a642fb15921a1426de845c949799218995a96207ebd6b2904f2a881b82a065eac32ade92b8cf9c73939b858cad212976836a8108f15230cfc5131e6a379c236531ea611b1e8d5277add4f4d211cb6f9f6364cf0e8c56ab32609f43aae59b20ada5e35e217d64e44d122665073837ed50ef23c90d8132b7a8e3a732f424f3166075ca4082ba690961a894a7ed2e6f42d2e772010133381b48d999bbd4cb548c381550626e9e5b8e91c7ea1e78d01e390b5a9de6ad42feb5d5d5f1408202abac88b8c70056af4bde7b2b1a412d8feb4b0b85b88c61", + "tx_json": "{\n \"version\": 2, \n \"unlock_time\": 0, \n \"vin\": [ {\n \"key\": {\n \"amount\": 0, \n \"key_offsets\": [ 95956097, 12691096, 198609, 412860, 68558, 67047, 62909, 99426, 17199, 270542, 9925, 41647, 2214, 3595, 8599, 711\n ], \n \"k_image\": \"2c479dbff819502441604a914af485db2f795b7f5bc0eab877d60a1419ee5498\"\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"1085022c023e67ccef3004d93b200be5df0cca89c5a17c51d76c3ee3c107d391\", \n \"view_tag\": \"90\"\n }\n }\n }, {\n \"amount\": 0, \n \"target\": {\n \"tagged_key\": {\n \"key\": \"32dd926cf0eac221cabeb5beee0cc9ef68a07a79748a69c3142377b5a8dbd6c7\", \n \"view_tag\": \"be\"\n }\n }\n }\n ], \n \"extra\": [ 1, 158, 177, 250, 13, 142, 156, 72, 83, 199, 219, 33, 75, 246, 84, 125, 221, 139, 35, 190, 199, 116, 69, 178, 8, 226, 150, 251, 38, 208, 134, 26, 91, 2, 9, 1, 106, 229, 0, 41, 59, 95, 17, 103\n ], \n \"rct_signatures\": {\n \"type\": 6, \n \"txnFee\": 491520000, \n \"ecdhInfo\": [ {\n \"amount\": \"13648ac5774104fe\"\n }, {\n \"amount\": \"28d4ea6e7e28511a\"\n }], \n \"outPk\": [ \"1c5c4189735a8c2e98bb12572283f3797ff1273d5dd44312e10cbb4cfdf34c3f\", \"7664dfc643db30f011129a53843d647e7cab9e03c2df68ef71b2040d59005f45\"]\n }, \n \"rctsig_prunable\": {\n \"nbp\": 1, \n \"bpp\": [ {\n \"A\": \"fb8885aaa4fc7f5fcae273f7a4612c629423864cb4d12c06044dad6629f40edf\", \n \"A1\": \"b2d6cb3c9dd78407302087c767c8bf17d507471a9cb9af54dc6528de99c8dd62\", \n \"B\": \"ca69f07a623f4ca679eccc0fa03568e346cc061ff1624c4b73ee72a7a414c681\", \n \"r1\": \"64bc2abdf7f7798e6a0f13b2c3852371c2db35ebb00c7ba018a42b0c2c31fc06\", \n \"s1\": \"a748b257eafb46efc220a57a7767664f7744420bc3ee1c7c227aa711680dd301\", \n \"d1\": \"accf87381433838debd8e517ba86d06cb7475f3b8dfa301ee879112ab2e7a008\", \n \"L\": [ \"200f21ba8eeb86935deb899b3babb7a3ebb21b2f1dfd1bb08015ae4b8ff4f163\", \"56c7e807b6bf00f6178c752d92f9630508eeccf307f8efdc7dfae7592555a082\", \"4706000a2f329413ffe6ecfe68345053f441db1815286b951cbf83b4e7c327e2\", \"7019fe81f3c534b297b9388438ffb1d5acd17514678a87719b843d64df591d1e\", \"478fdac7f3940088cfe0ea54a896fbcad933fb85c9e3f060db515cc8495f4a89\", \"39102799616733953f6bb959916ebdb8cfb8c958f62e787aa7b4a3bd45c311cd\", \"83be8617ee592413d8c76bf05922147cb08447e7b899344e9c7069cf710fe22a\"\n ], \n \"R\": [ \"744704b4b118e85e16e5dc3abc02e9e6fd45e95c73ec2412355907bc4a5d3d17\", \"849043695696eb46dbeb1389bc544c5ec11d5e35a9d9be1e5aa2884377c2f441\", \"0773a62507f87e7fe992e335377585bf63a7554e352066574e4e35fdf5d1c90a\", \"7ffa6b791e317f2c7c76a85532a7ae18a74c293e3ce9c853080fe23abc42d97f\", \"b8a8320d3586343706a56e8f293322aba82e9c159a41a3ceff344177dddc3df9\", \"47cf9159c194d6b9c392dd76a5ddca3f1110ca3043f7b2e82ef1b55fbc87e8b3\", \"063bd71e497c90ae647f0c21719149341298ab1402e9bbee4b674bc9509070df\"\n ]\n }\n ], \n \"CLSAGs\": [ {\n \"s\": [ \"1e5de5f3c400ef5230773a8c5b6fb1dc556bb25a8d0110b4c6f411785927930b\", \"cbc5b5a9057c4c6b55397a8000f4d8b6cc6159a4e642aa449512e56b405beb0e\", \"e74de267b38c7135eaba1f4171e7fbe7fa79807eba65b20a78e896c94e47ae05\", \"19d587d550fbf22015c99d604c7f55019901ac5fcbd674d0d2fd6d752087ca05\", \"6f89bd649fc33ddb75888ecc0327e66d0642683825f262c5cec006599bdf1f07\", \"b0f9af7553379e798af1e9644d988e703c96ae415e22d053d4c42fbb19c44d06\", \"2eec4bf6ce7888a7438c2f1d5b94a415ee8ff3bd95045429378b8c2b9fb7c600\", \"656b5a6f94b1149793517bce19d0ddf2f5b30fe3a16bbebed91ee0a03c47a003\", \"778322b5d214b960acc4b9b2cdd1e55c4aed04199aa9a0b5132b2d3553c3fe08\", \"7b9ffbbd7409e47528c075ee1edc51e6cedd5fe05998fddab5a3f25edb0ff208\", \"3fcf87e8e261df81a9cf178f5facd906821a543560d3f5d0bb07cf0402d69302\", \"463bc90bf04cce521b1b115ea4c57c6512695ade926f3be142001ee73e7f130b\", \"c9e506619c93bdae690697a0a642fb15921a1426de845c949799218995a96207\", \"ebd6b2904f2a881b82a065eac32ade92b8cf9c73939b858cad212976836a8108\", \"f15230cfc5131e6a379c236531ea611b1e8d5277add4f4d211cb6f9f6364cf0e\", \"8c56ab32609f43aae59b20ada5e35e217d64e44d122665073837ed50ef23c90d\"], \n \"c1\": \"8132b7a8e3a732f424f3166075ca4082ba690961a894a7ed2e6f42d2e7720101\", \n \"D\": \"33381b48d999bbd4cb548c381550626e9e5b8e91c7ea1e78d01e390b5a9de6ad\"\n }], \n \"pseudoOuts\": [ \"42feb5d5d5f1408202abac88b8c70056af4bde7b2b1a412d8feb4b0b85b88c61\"]\n }\n}", + "weight": 1536 + }], + "untrusted": false +}"#; +} + +define_request_and_response! { + get_transaction_pool_stats (other), + GET_TRANSACTION_POOL_STATS: &str, + Request = +r#"{}"#; + Response = +r#"{ + "credits": 0, + "pool_stats": { + "bytes_max": 11843, + "bytes_med": 2219, + "bytes_min": 1528, + "bytes_total": 144192, + "fee_total": 7018100000, + "histo": [{ + "bytes": 11219, + "txs": 4 + },{ + "bytes": 9737, + "txs": 5 + },{ + "bytes": 8757, + "txs": 4 + },{ + "bytes": 14763, + "txs": 4 + },{ + "bytes": 15007, + "txs": 6 + },{ + "bytes": 15924, + "txs": 6 + },{ + "bytes": 17869, + "txs": 8 + },{ + "bytes": 10894, + "txs": 5 + },{ + "bytes": 38485, + "txs": 10 + },{ + "bytes": 1537, + "txs": 1 + }], + "histo_98pc": 186, + "num_10m": 0, + "num_double_spends": 0, + "num_failing": 0, + "num_not_relayed": 0, + "oldest": 1721261651, + "txs_total": 53 + }, + "status": "OK", + "top_hash": "", + "untrusted": false +}"#; +} + +define_request_and_response! { + stop_daemon (other), + STOP_DAEMON: &str, + Request = +r#"{}"#; + Response = +r#"{ + "status": "OK" +}"#; +} + +define_request_and_response! { + get_limit (other), + GET_LIMIT: &str, + Request = +r#"{}"#; + Response = +r#"{ + "limit_down": 1280000, + "limit_up": 1280000, + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + set_limit (other), + SET_LIMIT: &str, + Request = +r#"{ + "limit_down": 1024 +}"#; + Response = +r#"{ + "limit_down": 1024, + "limit_up": 128, + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + out_peers (other), + OUT_PEERS: &str, + Request = +r#"{ + "out_peers": 3232235535 +}"#; + Response = +r#"{ + "out_peers": 3232235535, + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + get_net_stats (other), + GET_NET_STATS: &str, + Request = +r#"{}"#; + Response = +r#"{ + "start_time": 1721251858, + "status": "OK", + "total_bytes_in": 16283817214, + "total_bytes_out": 34225244079, + "total_packets_in": 5981922, + "total_packets_out": 3627107, + "untrusted": false +}"#; +} + +define_request_and_response! { + get_outs (other), + GET_OUTS: &str, + Request = +r#"{ + "outputs": [{ + "amount": 1, + "index": 0 + },{ + "amount": 1, + "index": 1 + }], + "get_txid": true +}"#; + Response = +r#"{ + "credits": 0, + "outs": [{ + "height": 51941, + "key": "08980d939ec297dd597119f498ad69fed9ca55e3a68f29f2782aae887ef0cf8e", + "mask": "1738eb7a677c6149228a2beaa21bea9e3370802d72a3eec790119580e02bd522", + "txid": "9d651903b80fb70b9935b72081cd967f543662149aed3839222511acd9100601", + "unlocked": true + },{ + "height": 51945, + "key": "454fe46c405be77625fa7e3389a04d3be392346983f27603561ac3a3a74f4a75", + "mask": "1738eb7a677c6149228a2beaa21bea9e3370802d72a3eec790119580e02bd522", + "txid": "230bff732dc5f225df14fff82aadd1bf11b3fb7ad3a03413c396a617e843f7d0", + "unlocked": true + }], + "status": "OK", + "top_hash": "", + "untrusted": false +}"#; +} + +define_request_and_response! { + update (other), + UPDATE: &str, + Request = +r#"{ + "command": "check" +}"#; + Response = +r#"{ + "auto_uri": "", + "hash": "", + "path": "", + "status": "OK", + "untrusted": false, + "update": false, + "user_uri": "", + "version": "" +}"#; +} + +define_request_and_response! { + pop_blocks (other), + POP_BLOCKS: &str, + Request = +r#"{ + "nblocks": 6 +}"#; + Response = +r#"{ + "height": 76482, + "status": "OK", + "untrusted": false +}"#; +} + +define_request_and_response! { + UNDOCUMENTED_ENDPOINT (other), + GET_TRANSACTION_POOL_HASHES: &str, + Request = +r#"{}"#; + Response = +r#"{ + "credits": 0, + "status": "OK", + "top_hash": "", + "tx_hashes": [ + "aa928aed888acd6152c60194d50a4df29b0b851be6169acf11b6a8e304dd6c03", + "794345f321a98f3135151f3056c0fdf8188646a8dab27de971428acf3551dd11", + "1e9d2ae11f2168a228942077483e70940d34e8658c972bbc3e7f7693b90edf17", + "7375c928f261d00f07197775eb0bfa756e5f23319819152faa0b3c670fe54c1b", + "2e4d5f8c5a45498f37fb8b6ca4ebc1efa0c371c38c901c77e66b08c072287329", + "eee6d596cf855adfb10e1597d2018e3a61897ac467ef1d4a5406b8d20bfbd52f", + "59c574d7ba9bb4558470f74503c7518946a85ea22c60fccfbdec108ce7d8f236", + "0d57bec1e1075a9e1ac45cf3b3ced1ad95ccdf2a50ce360190111282a0178655", + "60d627b2369714a40009c07d6185ebe7fa4af324fdfa8d95a37a936eb878d062", + "661d7e728a901a8cb4cf851447d9cd5752462687ed0b776b605ba706f06bdc7d", + "b80e1f09442b00b3fffe6db5d263be6267c7586620afff8112d5a8775a6fc58e", + "974063906d1ddfa914baf85176b0f689d616d23f3d71ed4798458c8b4f9b9d8f", + "d2575ae152a180be4981a9d2fc009afcd073adaa5c6d8b022c540a62d6c905bb", + "3d78aa80ee50f506683bab9f02855eb10257a08adceda7cbfbdfc26b10f6b1bb", + "8b5bc125bdb73b708500f734501d55088c5ac381a0879e1141634eaa72b6a4da", + "11c06f4d2f00c912ca07313ed2ea5366f3cae914a762bed258731d3d9e3706df", + "b3644dc7c9a3a53465fe80ad3769e516edaaeb7835e16fdd493aac110d472ae1", + "ed2478ad793b923dbf652c8612c40799d764e5468897021234a14a37346bc6ee" + ], + "untrusted": false +}"#; +} + +define_request_and_response! { + UNDOCUMENTED_ENDPOINT (other), + GET_PUBLIC_NODES: &str, + Request = +r#"{}"#; + Response = +r#"{ + "status": "OK", + "untrusted": false, + "white": [{ + "host": "70.52.75.3", + "last_seen": 1721246387, + "rpc_credits_per_hash": 0, + "rpc_port": 18081 + },{ + "host": "zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion:18083", + "last_seen": 1720186288, + "rpc_credits_per_hash": 0, + "rpc_port": 18089 + }] +}"#; +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/test-utils/src/rpc/mod.rs b/test-utils/src/rpc/mod.rs index 14c963ac..0da6b48f 100644 --- a/test-utils/src/rpc/mod.rs +++ b/test-utils/src/rpc/mod.rs @@ -1,25 +1,6 @@ -//! Monero RPC client. +//! Monero RPC data & client. //! -//! This module is a client for Monero RPC that maps the types -//! into the native types used by Cuprate found in `cuprate_types`. -//! -//! # Usage -//! ```rust,ignore -//! #[tokio::main] -//! async fn main() { -//! // Create RPC client. -//! let rpc = HttpRpcClient::new(None).await; -//! -//! // Collect 20 blocks. -//! let mut vec: Vec = vec![]; -//! for height in (3130269 - 20)..3130269 { -//! vec.push(rpc.get_verified_block_information(height).await); -//! } -//! } -//! ``` +//! This module has a `monerod` RPC [`client`] and some real request/response [`data`]. -mod client; -pub use client::HttpRpcClient; - -mod constants; -pub use constants::LOCALHOST_RPC_URL; +pub mod client; +pub mod data; diff --git a/types/Cargo.toml b/types/Cargo.toml index 9764f015..a5af3b26 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -9,12 +9,19 @@ repository = "https://github.com/Cuprate/cuprate/tree/main/types" keywords = ["cuprate", "types"] [features] -default = ["blockchain"] +default = ["blockchain", "epee", "serde"] blockchain = [] +epee = ["dep:cuprate-epee-encoding"] +serde = ["dep:serde"] [dependencies] +cuprate-epee-encoding = { path = "../net/epee-encoding", optional = true } +cuprate-fixed-bytes = { path = "../net/fixed-bytes" } + +bytes = { workspace = true } curve25519-dalek = { workspace = true } monero-serai = { workspace = true } +serde = { workspace = true, features = ["derive"], optional = true } serde = { workspace = true, optional = true } borsh = { workspace = true, optional = true } diff --git a/types/README.md b/types/README.md index 6a2015af..876931fd 100644 --- a/types/README.md +++ b/types/README.md @@ -1,20 +1,11 @@ # `cuprate-types` -Various data types shared by Cuprate. +Shared data types within Cuprate. -- [1. File Structure](#1-file-structure) - - [1.1 `src/`](#11-src) +This crate is a kitchen-sink for data types that are shared across Cuprate. ---- - -## 1. File Structure -A quick reference of the structure of the folders & files in `cuprate-types`. - -Note that `lib.rs/mod.rs` files are purely for re-exporting/visibility/lints, and contain no code. Each sub-directory has a corresponding `mod.rs`. - -### 1.1 `src/` -The top-level `src/` files. - -| File | Purpose | -|---------------------|---------| -| `service.rs` | Types used in database requests; `enum {Request,Response}` -| `types.rs` | Various general types used by Cuprate \ No newline at end of file +# Features flags +| Feature flag | Does what | +|--------------|-----------| +| `blockchain` | Enables the `blockchain` module, containing the blockchain database request/response types +| `serde` | Enables `serde` on types where applicable +| `epee` | Enables `cuprate-epee-encoding` on types where applicable diff --git a/types/src/block_complete_entry.rs b/types/src/block_complete_entry.rs new file mode 100644 index 00000000..ba5fc2b0 --- /dev/null +++ b/types/src/block_complete_entry.rs @@ -0,0 +1,154 @@ +//! Contains [`BlockCompleteEntry`] and the related types. + +//---------------------------------------------------------------------------------------------------- Import +#[cfg(feature = "epee")] +use bytes::Bytes; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use cuprate_fixed_bytes::ByteArray; + +#[cfg(feature = "epee")] +use cuprate_epee_encoding::{ + epee_object, + macros::bytes::{Buf, BufMut}, + EpeeValue, InnerMarker, +}; + +//---------------------------------------------------------------------------------------------------- BlockCompleteEntry +/// A block that can contain transactions. +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct BlockCompleteEntry { + /// `true` if transaction data is pruned. + pub pruned: bool, + /// The block. + pub block: Bytes, + /// The block weight/size. + pub block_weight: u64, + /// The block's transactions. + pub txs: TransactionBlobs, +} + +#[cfg(feature = "epee")] +epee_object!( + BlockCompleteEntry, + pruned: bool = false, + block: Bytes, + block_weight: u64 = 0_u64, + txs: TransactionBlobs = TransactionBlobs::None => + TransactionBlobs::tx_blob_read, + TransactionBlobs::tx_blob_write, + TransactionBlobs::should_write_tx_blobs, +); + +//---------------------------------------------------------------------------------------------------- TransactionBlobs +/// Transaction blobs within [`BlockCompleteEntry`]. +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum TransactionBlobs { + /// Pruned transaction blobs. + Pruned(Vec), + /// Normal transaction blobs. + Normal(Vec), + #[default] + /// No transactions. + None, +} + +impl TransactionBlobs { + /// Returns [`Some`] if `self` is [`Self::Pruned`]. + pub fn take_pruned(self) -> Option> { + match self { + Self::Normal(_) => None, + Self::Pruned(txs) => Some(txs), + Self::None => Some(vec![]), + } + } + + /// Returns [`Some`] if `self` is [`Self::Normal`]. + pub fn take_normal(self) -> Option> { + match self { + Self::Normal(txs) => Some(txs), + Self::Pruned(_) => None, + Self::None => Some(vec![]), + } + } + + /// Returns the byte length of the blob. + pub fn len(&self) -> usize { + match self { + Self::Normal(txs) => txs.len(), + Self::Pruned(txs) => txs.len(), + Self::None => 0, + } + } + + /// Returns `true` if the byte length of the blob is `0`. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Epee read function. + #[cfg(feature = "epee")] + fn tx_blob_read(b: &mut B) -> cuprate_epee_encoding::Result { + let marker = cuprate_epee_encoding::read_marker(b)?; + match marker.inner_marker { + InnerMarker::Object => Ok(Self::Pruned(Vec::read(b, &marker)?)), + InnerMarker::String => Ok(Self::Normal(Vec::read(b, &marker)?)), + _ => Err(cuprate_epee_encoding::Error::Value( + "Invalid marker for tx blobs".to_string(), + )), + } + } + + /// Epee write function. + #[cfg(feature = "epee")] + fn tx_blob_write( + self, + field_name: &str, + w: &mut B, + ) -> cuprate_epee_encoding::Result<()> { + if self.should_write_tx_blobs() { + match self { + Self::Normal(bytes) => { + cuprate_epee_encoding::write_field(bytes, field_name, w)?; + } + Self::Pruned(obj) => { + cuprate_epee_encoding::write_field(obj, field_name, w)?; + } + Self::None => (), + } + } + Ok(()) + } + + /// Epee should write function. + #[cfg(feature = "epee")] + fn should_write_tx_blobs(&self) -> bool { + !self.is_empty() + } +} + +//---------------------------------------------------------------------------------------------------- PrunedTxBlobEntry +/// A pruned transaction with the hash of the missing prunable data +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PrunedTxBlobEntry { + /// The transaction. + pub tx: Bytes, + /// The prunable transaction hash. + pub prunable_hash: ByteArray<32>, +} + +#[cfg(feature = "epee")] +epee_object!( + PrunedTxBlobEntry, + tx: Bytes, + prunable_hash: ByteArray<32>, +); + +//---------------------------------------------------------------------------------------------------- Import +#[cfg(test)] +mod tests {} diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index 31cc46ef..1ff06c29 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -9,12 +9,7 @@ use std::{ ops::Range, }; -#[cfg(feature = "borsh")] -use borsh::{BorshDeserialize, BorshSerialize}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::types::{ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; +use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; //---------------------------------------------------------------------------------------------------- ReadRequest /// A read request to the blockchain database. @@ -30,12 +25,17 @@ pub enum BCReadRequest { /// Request a block's extended header. /// /// The input is the block's height. - BlockExtendedHeader(usize), + BlockExtendedHeader(u64), /// Request a block's hash. /// - /// The input is the block's height. - BlockHash(usize), + /// The input is the block's height and the chain it is on. + BlockHash(u64, Chain), + + /// Request to check if we have a block and which [`Chain`] it is on. + /// + /// The input is the block's hash. + FindBlock([u8; 32]), /// Removes the block hashes that are not in the _main_ chain. /// @@ -45,15 +45,15 @@ pub enum BCReadRequest { /// Request a range of block extended headers. /// /// The input is a range of block heights. - BlockExtendedHeaderInRange(Range), + BlockExtendedHeaderInRange(Range, Chain), /// Request the current chain height. /// /// Note that this is not the top-block height. ChainHeight, - /// Request the total amount of generated coins (atomic units) so far. - GeneratedCoins, + /// Request the total amount of generated coins (atomic units) at this height. + GeneratedCoins(u64), /// Request data for multiple outputs. /// @@ -83,10 +83,21 @@ pub enum BCReadRequest { /// The input is a list of output amounts. NumberOutputsWithAmount(Vec), - /// Check that all key images within a set arer not spent. + /// Check that all key images within a set are not spent. /// /// Input is a set of key images. KeyImagesSpent(HashSet<[u8; 32]>), + + /// A request for the compact chain history. + CompactChainHistory, + + /// A request to find the first unknown block ID in a list of block IDs. + //// + /// # Invariant + /// The [`Vec`] containing the block IDs must be sorted in chronological block + /// order, or else the returned response is unspecified and meaningless, + /// as this request performs a binary search. + FindFirstUnknown(Vec<[u8; 32]>), } //---------------------------------------------------------------------------------------------------- WriteRequest @@ -123,6 +134,11 @@ pub enum BCResponse { /// Inner value is the hash of the requested block. BlockHash([u8; 32]), + /// Response to [`BCReadRequest::FindBlock`]. + /// + /// Inner value is the chain and height of the block if found. + FindBlock(Option<(Chain, u64)>), + /// Response to [`BCReadRequest::FilterUnknownHashes`]. /// /// Inner value is the list of hashes that were in the main chain. @@ -136,11 +152,11 @@ pub enum BCResponse { /// Response to [`BCReadRequest::ChainHeight`]. /// /// Inner value is the chain height, and the top block's hash. - ChainHeight(usize, [u8; 32]), + ChainHeight(u64, [u8; 32]), /// Response to [`BCReadRequest::GeneratedCoins`]. /// - /// Inner value is the total amount of generated coins so far, in atomic units. + /// Inner value is the total amount of generated coins up to and including the chosen height, in atomic units. GeneratedCoins(u64), /// Response to [`BCReadRequest::Outputs`]. @@ -164,6 +180,23 @@ pub enum BCResponse { /// The inner value is `false` if _none_ of the key images were spent. KeyImagesSpent(bool), + /// Response to [`BCReadRequest::CompactChainHistory`]. + CompactChainHistory { + /// A list of blocks IDs in our chain, starting with the most recent block, all the way to the genesis block. + /// + /// These blocks should be in reverse chronological order, not every block is needed. + block_ids: Vec<[u8; 32]>, + /// The current cumulative difficulty of the chain. + cumulative_difficulty: u128, + }, + + /// The response for [`BCReadRequest::FindFirstUnknown`]. + /// + /// Contains the index of the first unknown block and its expected height. + /// + /// This will be [`None`] if all blocks were known. + FindFirstUnknown(Option<(usize, u64)>), + //------------------------------------------------------ Writes /// Response to [`BCWriteRequest::WriteBlock`]. /// diff --git a/types/src/lib.rs b/types/src/lib.rs index 2d161f7e..bcf6a45d 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -1,11 +1,4 @@ -//! Cuprate shared data types. -//! -//! This crate is a kitchen-sink for data types that are shared across `Cuprate`. -//! -//! # Features flags -//! The [`blockchain`] module, containing the blockchain database request/response -//! types, must be enabled with the `blockchain` feature (on by default). - +#![doc = include_str!("../README.md")] //---------------------------------------------------------------------------------------------------- Lints // Forbid lints. // Our code, and code generated (e.g macros) cannot overrule these. @@ -86,9 +79,13 @@ // // Documentation for each module is located in the respective file. +mod block_complete_entry; mod types; + +pub use block_complete_entry::{BlockCompleteEntry, PrunedTxBlobEntry, TransactionBlobs}; pub use types::{ - ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation, VerifiedTransactionInformation, + AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, OutputOnChain, + VerifiedBlockInformation, VerifiedTransactionInformation, }; //---------------------------------------------------------------------------------------------------- Feature-gated diff --git a/types/src/types.rs b/types/src/types.rs index e99b624d..92215951 100644 --- a/types/src/types.rs +++ b/types/src/types.rs @@ -38,7 +38,8 @@ pub struct ExtendedBlockHeader { //---------------------------------------------------------------------------------------------------- VerifiedTransactionInformation /// Verified information of a transaction. /// -/// This represents a transaction in a valid block. +/// - If this is in a [`VerifiedBlockInformation`] this represents a valid transaction +/// - If this is in an [`AltBlockInformation`] this represents a potentially valid transaction #[derive(Clone, Debug, PartialEq, Eq)] pub struct VerifiedTransactionInformation { /// The transaction itself. @@ -91,6 +92,53 @@ pub struct VerifiedBlockInformation { pub cumulative_difficulty: u128, } +//---------------------------------------------------------------------------------------------------- ChainID +/// A unique ID for an alt chain. +/// +/// The inner value is meaningless. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct ChainId(pub u64); + +//---------------------------------------------------------------------------------------------------- Chain +/// An identifier for a chain. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum Chain { + /// The main chain. + Main, + /// An alt chain. + Alt(ChainId), +} + +//---------------------------------------------------------------------------------------------------- AltBlockInformation +/// A block on an alternative chain. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AltBlockInformation { + /// The block itself. + pub block: Block, + /// The serialized byte form of [`Self::block`]. + /// + /// [`Block::serialize`]. + pub block_blob: Vec, + /// All the transactions in the block, excluding the [`Block::miner_tx`]. + pub txs: Vec, + /// The block's hash. + /// + /// [`Block::hash`]. + pub block_hash: [u8; 32], + /// The block's proof-of-work hash. + pub pow_hash: [u8; 32], + /// The block's height. + pub height: u64, + /// The adjusted block size, in bytes. + pub weight: usize, + /// The long term block weight, which is the weight factored in with previous block weights. + pub long_term_weight: usize, + /// The cumulative difficulty of all blocks up until and including this block. + pub cumulative_difficulty: u128, + /// The [`ChainId`] of the chain this alt block is on. + pub chain_id: ChainId, +} + //---------------------------------------------------------------------------------------------------- OutputOnChain /// An already existing transaction output. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/typos.toml b/typos.toml index abab1903..fbd66d09 100644 --- a/typos.toml +++ b/typos.toml @@ -17,4 +17,6 @@ extend-ignore-identifiers-re = [ extend-exclude = [ "/misc/gpg_keys/", "cryptonight/", + "/test-utils/src/rpc/data/json.rs", + "rpc/types/src/json.rs", ]