mirror of
https://github.com/MAGICGrants/cuprate-for-explorer.git
synced 2026-01-08 03:13:50 -05:00
Merge branch 'Cuprate:main' into main
This commit is contained in:
130
.github/workflows/ci.yml
vendored
130
.github/workflows/ci.yml
vendored
@@ -18,6 +18,17 @@ env:
|
||||
RUSTDOCFLAGS: '-D warnings'
|
||||
# Enable debug information generation for build dependencies.
|
||||
CARGO_PROFILE_DEV_BUILD_OVERRIDE_DEBUG: true
|
||||
# Build commands.
|
||||
CMD_DOC: cargo doc --all-features --no-deps
|
||||
CMD_CLIPPY: cargo clippy --all-features --all-targets -- -D warnings
|
||||
CMD_TEST: |
|
||||
# HACK: how to test both DB backends that are feature-gated?
|
||||
cargo test --all-features
|
||||
cargo test --package cuprate-blockchain --no-default-features --features redb
|
||||
CMD_BUILD: cargo build --all-features --all-targets
|
||||
CMD_HACK: |
|
||||
cargo install cargo-hack --locked
|
||||
cargo hack check --feature-powerset --no-dev-deps
|
||||
|
||||
jobs:
|
||||
# Run format separately.
|
||||
@@ -78,7 +89,7 @@ jobs:
|
||||
- name: Build WASM 32-bit
|
||||
run: cargo build --target wasm32-unknown-unknown -p ${{ matrix.crate }}
|
||||
|
||||
# All other CI.
|
||||
# CI, runs on GitHub provided OSs.
|
||||
ci:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -93,46 +104,95 @@ jobs:
|
||||
]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target
|
||||
key: ${{ matrix.os }}
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target
|
||||
key: ${{ matrix.os }}
|
||||
|
||||
- name: Download monerod
|
||||
uses: ./.github/actions/monerod-download
|
||||
- name: Download monerod
|
||||
uses: ./.github/actions/monerod-download
|
||||
|
||||
- name: Install dependencies (Windows)
|
||||
if: matrix.os == 'windows-2022'
|
||||
uses: lukka/get-cmake@v3.31.6 # Needed for `randomx-rs`
|
||||
- name: Install dependencies (Windows)
|
||||
if: matrix.os == 'windows-2022'
|
||||
uses: lukka/get-cmake@v3.31.6 # Needed for `randomx-rs`
|
||||
|
||||
- name: Documentation
|
||||
run: cargo doc --all-features --no-deps
|
||||
- name: Documentation
|
||||
run: ${{ env.CMD_DOC }}
|
||||
|
||||
- name: Clippy (fail on warnings)
|
||||
run: cargo clippy --all-features --all-targets -- -D warnings
|
||||
- name: Clippy (fail on warnings)
|
||||
run: ${{ env.CMD_CLIPPY }}
|
||||
|
||||
# HACK: how to test both DB backends that are feature-gated?
|
||||
- name: Test
|
||||
run: |
|
||||
cargo test --all-features
|
||||
cargo test --package cuprate-blockchain --no-default-features --features redb
|
||||
- name: Test
|
||||
run: ${{ env.CMD_TEST }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all-features --all-targets
|
||||
- name: Build
|
||||
run: ${{ env.CMD_BUILD }}
|
||||
|
||||
- name: Hack Check
|
||||
run: |
|
||||
cargo install cargo-hack --locked
|
||||
cargo hack check --feature-powerset --no-dev-deps
|
||||
- name: Hack Check
|
||||
run: ${{ env.CMD_HACK }}
|
||||
|
||||
# CI, runs in a Docker image.
|
||||
ci-docker:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ matrix.os.image }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
# TODO: support musl <https://github.com/Cuprate/cuprate/issues/336>
|
||||
# - { image: alpine:latest, commands: "apk update && apk add alpine-sdk cmake curl bash" }
|
||||
- { image: archlinux:latest, commands: "pacman -Syyu --noconfirm base-devel git cmake openssl" }
|
||||
- { image: debian:stable, commands: "apt update && apt -y install build-essential curl cmake git pkg-config libssl-dev" }
|
||||
- { image: fedora:latest, commands: "dnf install --assumeyes @development-tools gcc gcc-c++ cmake git openssl-devel perl-core" }
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target
|
||||
key: ${{ matrix.os.image }}
|
||||
|
||||
- name: Dependencies
|
||||
run: ${{ matrix.os.commands }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
|
||||
- name: Download monerod
|
||||
uses: ./.github/actions/monerod-download
|
||||
|
||||
- name: Documentation
|
||||
run: ${{ env.CMD_DOC }}
|
||||
|
||||
- name: Clippy (fail on warnings)
|
||||
run: ${{ env.CMD_CLIPPY }}
|
||||
|
||||
- name: Test
|
||||
run: ${{ env.CMD_TEST }}
|
||||
|
||||
- name: Build
|
||||
run: ${{ env.CMD_BUILD }}
|
||||
|
||||
- name: Hack Check
|
||||
run: ${{ env.CMD_HACK }}
|
||||
4146
Cargo.lock
generated
4146
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -163,10 +163,11 @@ cuprate-rpc-interface = { path = "rpc/interface", default-featur
|
||||
cuprate-zmq-types = { path = "zmq/types", default-features = false }
|
||||
|
||||
# External dependencies
|
||||
axum = { version = "0.7", default-features = false }
|
||||
axum = { version = "0.8", default-features = false }
|
||||
anyhow = { version = "1", default-features = false }
|
||||
arc-swap = { version = "1", default-features = false }
|
||||
arrayvec = { version = "0.7", default-features = false }
|
||||
arti-client = { version = "0.33", default-features = false }
|
||||
async-trait = { version = "0.1", default-features = false }
|
||||
bitflags = { version = "2", default-features = false }
|
||||
blake3 = { version = "1", default-features = false }
|
||||
@@ -176,15 +177,15 @@ bytes = { version = "1", default-features = false }
|
||||
cfg-if = { version = "1", default-features = false }
|
||||
clap = { version = "4", default-features = false }
|
||||
chrono = { version = "0.4", default-features = false }
|
||||
crypto-bigint = { version = "0.5", default-features = false }
|
||||
crypto-bigint = { version = "0.6", default-features = false }
|
||||
crossbeam = { version = "0.8", default-features = false }
|
||||
const_format = { version = "0.2", default-features = false }
|
||||
curve25519-dalek = { version = "4", default-features = false }
|
||||
dashmap = { version = "6", default-features = false }
|
||||
dirs = { version = "5", default-features = false }
|
||||
dirs = { version = "6", default-features = false }
|
||||
futures = { version = "0.3", default-features = false }
|
||||
hex = { version = "0.4", default-features = false }
|
||||
hex-literal = { version = "0.4", default-features = false }
|
||||
hex-literal = { version = "1", default-features = false }
|
||||
indexmap = { version = "2", default-features = false }
|
||||
monero-address = { git = "https://github.com/Cuprate/serai.git", rev = "e6ae8c2", default-features = false }
|
||||
monero-serai = { git = "https://github.com/Cuprate/serai.git", rev = "e6ae8c2", default-features = false }
|
||||
@@ -195,19 +196,27 @@ randomx-rs = { git = "https://github.com/Cuprate/randomx-rs.git", rev
|
||||
rand = { version = "0.8", default-features = false }
|
||||
rand_distr = { version = "0.4", default-features = false }
|
||||
rayon = { version = "1", default-features = false }
|
||||
safelog = { version = "0.4", default-features = false }
|
||||
serde_bytes = { version = "0.11", default-features = false }
|
||||
serde_json = { version = "1", default-features = false }
|
||||
serde = { version = "1", default-features = false }
|
||||
strum = { version = "0.26", default-features = false }
|
||||
thiserror = { version = "1", default-features = false }
|
||||
strum = { version = "0.27", default-features = false }
|
||||
thiserror = { version = "2", default-features = false }
|
||||
thread_local = { version = "1", default-features = false }
|
||||
tokio-util = { version = "0.7", default-features = false }
|
||||
tokio-stream = { version = "0.1", default-features = false }
|
||||
tokio-socks = { git = "https://github.com/Cuprate/tokio-socks.git", rev = "8737caf", default-features = false }
|
||||
tokio = { version = "1", default-features = false }
|
||||
tower = { git = "https://github.com/Cuprate/tower.git", rev = "6c7faf0", default-features = false } # <https://github.com/tower-rs/tower/pull/796>
|
||||
tower = { version = "0.5", default-features = false }
|
||||
tower-http = { version = "0.6", default-features = false }
|
||||
toml = { version = "0.8", default-features = false }
|
||||
toml_edit = { version = "0.22", default-features = false }
|
||||
toml = { version = "0.9", default-features = false }
|
||||
toml_edit = { version = "0.23", default-features = false }
|
||||
tor-cell = { version = "0.33", default-features = false }
|
||||
tor-config-path = { version = "0.33", default-features = false }
|
||||
tor-hsservice = { version = "0.33", default-features = false }
|
||||
tor-persist = { version = "0.33", default-features = false }
|
||||
tor-proto = { version = "0.33", default-features = false }
|
||||
tor-rtcompat = { version = "0.33", default-features = false }
|
||||
tracing-appender = { version = "0.2", default-features = false }
|
||||
tracing-subscriber = { version = "0.3", default-features = false }
|
||||
tracing = { version = "0.1", default-features = false }
|
||||
@@ -218,7 +227,7 @@ monero-simple-request-rpc = { git = "https://github.com/Cuprate/serai.git", rev
|
||||
tempfile = { version = "3" }
|
||||
pretty_assertions = { version = "1" }
|
||||
proptest = { version = "1" }
|
||||
proptest-derive = { version = "0.5" }
|
||||
proptest-derive = { version = "0.6" }
|
||||
tokio-test = { version = "0.4" }
|
||||
arbitrary = { version = "1" }
|
||||
|
||||
@@ -424,7 +433,7 @@ allow_attributes = "deny"
|
||||
undocumented_unsafe_blocks = "deny"
|
||||
# multiple_unsafe_ops_per_block = "deny"
|
||||
# single_char_lifetime_names = "deny"
|
||||
# wildcard_enum_match_arm = "deny"
|
||||
wildcard_enum_match_arm = "deny"
|
||||
|
||||
[workspace.lints.rust]
|
||||
# Cold
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cuprated"
|
||||
version = "0.0.4"
|
||||
version = "0.0.5"
|
||||
edition = "2021"
|
||||
description = "The Cuprate Rust Monero node."
|
||||
license = "AGPL-3.0-only"
|
||||
@@ -28,6 +28,7 @@ cuprate-hex = { workspace = true }
|
||||
cuprate-json-rpc = { workspace = true }
|
||||
cuprate-levin = { workspace = true }
|
||||
cuprate-p2p-core = { workspace = true }
|
||||
cuprate-p2p-transport = { workspace = true }
|
||||
cuprate-p2p = { workspace = true }
|
||||
cuprate-pruning = { workspace = true }
|
||||
cuprate-rpc-interface = { workspace = true, features = ["dummy"] }
|
||||
@@ -41,6 +42,7 @@ cuprate-wire = { workspace = true }
|
||||
# TODO: after v1.0.0, remove unneeded dependencies.
|
||||
axum = { workspace = true, features = ["tokio", "http1", "http2"] }
|
||||
anyhow = { workspace = true }
|
||||
arti-client = { workspace = true, features = ["tokio", "native-tls", "onion-service-client", "onion-service-service", "static"] }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
borsh = { workspace = true }
|
||||
@@ -68,6 +70,7 @@ randomx-rs = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_distr = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
safelog = { workspace = true }
|
||||
serde_bytes = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
@@ -77,8 +80,11 @@ thread_local = { workspace = true }
|
||||
tokio-util = { workspace = true, features = ["rt"] }
|
||||
tokio-stream = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
toml = { workspace = true, features = ["parse", "display"]}
|
||||
toml_edit = { workspace = true }
|
||||
toml = { workspace = true, features = ["parse", "display", "serde"]}
|
||||
toml_edit = { workspace = true, features = ["parse", "display"] }
|
||||
tor-hsservice = { workspace = true }
|
||||
tor-persist = { workspace = true }
|
||||
tor-rtcompat = { workspace = true }
|
||||
tower = { workspace = true, features = ["limit"] }
|
||||
tower-http = { workspace = true, features = ["limit"] }
|
||||
tracing-appender = { workspace = true }
|
||||
@@ -94,4 +100,4 @@ cuprate-hex = { workspace = true }
|
||||
serde_json = { workspace = true, features = ["std"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
workspace = true
|
||||
@@ -26,6 +26,10 @@ impl<N: NetworkZone> Service<ChainSvcRequest<N>> for ChainService {
|
||||
}
|
||||
|
||||
fn call(&mut self, req: ChainSvcRequest<N>) -> Self::Future {
|
||||
#[expect(
|
||||
clippy::wildcard_enum_match_arm,
|
||||
reason = "other requests should be unreachable"
|
||||
)]
|
||||
let map_res = |res: BlockchainResponse| match res {
|
||||
BlockchainResponse::CompactChainHistory {
|
||||
block_ids,
|
||||
|
||||
@@ -6689,5 +6689,64 @@
|
||||
"e7f6a57e22e00fe4b1920050d307913d92be3e3643705a2b4274dbc5ab2a29c9",
|
||||
"4d972ee9bb86b7a459fb22dde36505ae2df03d1e09ec68347a072419be93bf5e",
|
||||
"b305e716b0a7ec2198717bbf0c4d4639f250cfb5887115615b6bdfa27d7f53df",
|
||||
"6eb51cb5f4657e2b77698da1967a55aeaadc75486742e72a81b39b2b67dae552"
|
||||
"6eb51cb5f4657e2b77698da1967a55aeaadc75486742e72a81b39b2b67dae552",
|
||||
"124d452cc82e080a0b5ac9c2889b494f085d98d8df452cc3e1737f43c7a840aa",
|
||||
"b61ec3a754342be8e36cdc1e7d5f2e9747ae3fa118ad6f97a27f6494438f958b",
|
||||
"7e28afbe2f06767c4a765ffba1a7c664f3c59ffc724a0a73916c5d6430b65460",
|
||||
"427e5a2e625f42973dadf5352c0ab44dade0b19168b7501b8af8839d676cfa27",
|
||||
"19ca9884dc2e0780109fcd53a46c5101837f6699aa5db40ef924bf2d50c1f6b7",
|
||||
"eb358be36e0b6d5665e74ac2af929886c4ecc193251087a0d43179984310ed01",
|
||||
"dcce24f62cc52181a861b042d00d69e9b80a4e1c316b788e90ea62760b82bca0",
|
||||
"3c6a2fb63c170537e2f3f752c1700efc5df808a97fc81f786c2adae1eab608a3",
|
||||
"f006df6b2514dde690faf30235dbc0c0570b01b713d7227cfbe4052aa98ad8c9",
|
||||
"d3dbe69aa850a747b9676b0a0e07c6fa4e704cc9bfe020ae10b896af4bf67ef5",
|
||||
"43adaf14c8f134500e63468edfc2adbab6ea986b85854c1816e3ec92e51d1f3f",
|
||||
"ac0df0d0a2827c0b348ca80d8cdfde5fa774c087a95e3769cd2b1704e725144a",
|
||||
"7c0ce77be4884dade682588587b76f72506bb1e31158bdf5724e43f0fca8c03d",
|
||||
"56099ac0215bb888e7cbc046785a89836880a5a941ea6e38fcbefb5c1b5f431a",
|
||||
"0b48e7e6858f491e2607bfa0305e0ae5adc8b488e35016d9a3da55be2e79449c",
|
||||
"9dd6f882396b2084c0d19eade9c62d426c09a5550d29ea3d8002a66cbd7b3f65",
|
||||
"1427d42e3aba9ff988fa8f01d4a7d4df0ee48fdfaffd835012504143e4d42cce",
|
||||
"cb3f2ff7dbc77358fa9bf86f0ba128291ff3a4e6f6995c358c4a403f6d786502",
|
||||
"6d02a0c40e8382ac340d8a6a3d90cdc6cf3cb24b7fba676ec19cafa8b2c29e17",
|
||||
"c590e527df810910b1c6fc5e3279023a9ce1474c8a9bd4423caf44bf15a0246d",
|
||||
"022207cd9a8f71d531110c66ead0bf9b3fc0656474cea730ab23a5467ce37b3a",
|
||||
"e9f2b885bec01e4e154dc1d0cff803619da4189644860ddce0ca75c972e3d5df",
|
||||
"a81a5b67d5201bf61cb7d028c7805b981ff5241a6c3824847126377f5fbb02c2",
|
||||
"4509cd16ca5e31745058593335ae54956e5f5b4cf6787249bfc5c13f67e8bf70",
|
||||
"89a6c5660bee4607785d73a6303bbe31518b42a21cde1630dfaeb4bcde402055",
|
||||
"a1fcdbfd416ae42f2244274e65ef553bd0f429181a413dc6fab69bd1113d64e5",
|
||||
"e2c0aac6f3bbcbb6abf958e19c535cb46a01a2deb6636f0e02c38d82951dfaa0",
|
||||
"1e2a1f5ac5dc0f019e9f14bab80e5ad0dec01a64a9892af63a332ecb09a1f4ae",
|
||||
"45b2fb06ad208ccee3bf79cd9bb85cfbe9e7ac1c0f091e79fc987a455606cc33",
|
||||
"9fe65e1214a5a36d7450f35a9c600573d97a7aacc3f9c8f147542a14f825f75e",
|
||||
"ad621dac9ad7ca8e21c7e2da7715f0c391c32c459438ce9c6bd342dbe423aaf6",
|
||||
"f5dc9abbe5d720f38ee72167253b5f98761c099f0bbf1853d1a1c05d1303f1f4",
|
||||
"90e9506eff3d8c2389dbffe36378cedf8d0e53b435a75c43dcdb585b0ef568b6",
|
||||
"800582f0ed0c5a41c5002dbf3616ec8025dc7eff24ca30bb1a66aa01bfd2c2c2",
|
||||
"83335160c0c598e406ece04906714652cdaba8434ca580fc693b7b892ec96d64",
|
||||
"7e0350c97901bf2fc8231fda1b03fac8d42ad3e9fcd3fd8f2cd94c9ba937f2e7",
|
||||
"b40ce4ef841a0857cc735d6e866ef3625daec563071263cfef571a476dff2089",
|
||||
"43f0b05bff502bc49764c208f21d00b03d0f54fb7a9f437332aab3f17bbc7a65",
|
||||
"a3fcc27172a0053ac5aae5ddc0aea36caeb8c6d50f9235186f1b1a0757213b64",
|
||||
"6b1caa809e9466e4b5c37b9abd89681cf1420609f86cdb068054d4a198d93720",
|
||||
"76fee78c21d6183cdb4ce87bb5732202cbb12716aa72799d0a2060e4dad44826",
|
||||
"e8c59cf1f095f834207b03ad811f4c007149b8d725409a7fa201389c3f137a05",
|
||||
"2272970d06a35ea1d4b5de9957e22c2a86563a73a9add6fd0e1b2bc5f99b4b3b",
|
||||
"e1800bb4454c12708acf377459251bbdc269b94d3ab977302bb5bcb591aee717",
|
||||
"bf134399845ca74a35653a5d0bb269e2998ab72bf34b0cdc7faf80a6a1fa8c9c",
|
||||
"1e98b32d7bd643cf82a1b918f84c3d21688b63d902772511dbd28b2a4a9d2500",
|
||||
"7fc074ac36a98268499a272be0ecb38cb798d90da96b8e0340dee7db966d05e6",
|
||||
"0352892ab4c8f5364072ad65c9d96d9cff71ef3b72d9f23b8c8f462b3493d362",
|
||||
"0d290d55b21802cfc16493897965236ee887b891344e09d592c3a4d97c69cdbc",
|
||||
"be7080d09096d1c253be2c055066b91319fa2c51444fa314c972b256197be870",
|
||||
"fc18353b059239f4f92b6254b2584482d8f2c0146852a76050f5aab877af81e2",
|
||||
"a88575d9c08dd4e5204c65071add4e6d6b6abb9906a34ac190bc404a43dd86cc",
|
||||
"5bb304ce2e25541151a9892164d9e35f78b6f7350c603178287d7fdd4a99412f",
|
||||
"f4080d1fcb6080aeaceb04c777e2f8a5960582fa881cfc88396bd5ba2d5f364c",
|
||||
"eafe0e592ec7836f820891f6b0801a33901282a840162298a8113b4f0e656372",
|
||||
"6e23cbfd7df18fb4a282c0726f8cc788e3e14a14704164cd497700e274993a8b",
|
||||
"d286fbd5debde93670c1feaa34e01eab9f356114c4231037638dd62a9f46ad06",
|
||||
"68ba3c15eafe978a0d8d1191b61a9cb6740975996fe21ddb41da300853ca19c9",
|
||||
"1b1217033a52bba8ff455d85558296ea683efde60e9e389477b01bc5f59ba258"
|
||||
]
|
||||
@@ -28,6 +28,7 @@ use crate::{
|
||||
types::ConsensusBlockchainReadHandle,
|
||||
},
|
||||
constants::PANIC_CRITICAL_SERVICE_ERROR,
|
||||
txpool::TxpoolManagerHandle,
|
||||
};
|
||||
|
||||
mod commands;
|
||||
@@ -46,7 +47,7 @@ pub async fn init_blockchain_manager(
|
||||
clearnet_interface: NetworkInterface<ClearNet>,
|
||||
blockchain_write_handle: BlockchainWriteHandle,
|
||||
blockchain_read_handle: BlockchainReadHandle,
|
||||
txpool_write_handle: TxpoolWriteHandle,
|
||||
txpool_manager_handle: TxpoolManagerHandle,
|
||||
mut blockchain_context_service: BlockchainContextService,
|
||||
block_downloader_config: BlockDownloaderConfig,
|
||||
) {
|
||||
@@ -72,7 +73,7 @@ pub async fn init_blockchain_manager(
|
||||
blockchain_read_handle,
|
||||
BoxError::from,
|
||||
),
|
||||
txpool_write_handle,
|
||||
txpool_manager_handle,
|
||||
blockchain_context_service,
|
||||
stop_current_block_downloader,
|
||||
broadcast_svc: clearnet_interface.broadcast_svc(),
|
||||
@@ -93,8 +94,8 @@ pub struct BlockchainManager {
|
||||
blockchain_write_handle: BlockchainWriteHandle,
|
||||
/// A [`BlockchainReadHandle`].
|
||||
blockchain_read_handle: ConsensusBlockchainReadHandle,
|
||||
/// A [`TxpoolWriteHandle`].
|
||||
txpool_write_handle: TxpoolWriteHandle,
|
||||
|
||||
txpool_manager_handle: TxpoolManagerHandle,
|
||||
/// The blockchain context cache, this caches the current state of the blockchain to quickly calculate/retrieve
|
||||
/// values without needing to go to a [`BlockchainReadHandle`].
|
||||
blockchain_context_service: BlockchainContextService,
|
||||
|
||||
@@ -619,11 +619,8 @@ impl super::BlockchainManager {
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR);
|
||||
|
||||
self.txpool_write_handle
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(TxpoolWriteRequest::NewBlock { spent_key_images })
|
||||
self.txpool_manager_handle
|
||||
.new_block(spent_key_images)
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,12 @@ use cuprate_p2p::{block_downloader::BlockBatch, BroadcastSvc};
|
||||
use cuprate_p2p_core::handles::HandleBuilder;
|
||||
use cuprate_types::{CachedVerificationState, TransactionVerificationData, TxVersion};
|
||||
|
||||
use crate::blockchain::{
|
||||
check_add_genesis, manager::BlockchainManager, manager::BlockchainManagerCommand,
|
||||
ConsensusBlockchainReadHandle,
|
||||
use crate::{
|
||||
blockchain::{
|
||||
check_add_genesis, manager::BlockchainManager, manager::BlockchainManagerCommand,
|
||||
ConsensusBlockchainReadHandle,
|
||||
},
|
||||
txpool::TxpoolManagerHandle,
|
||||
};
|
||||
|
||||
async fn mock_manager(data_dir: PathBuf) -> BlockchainManager {
|
||||
@@ -30,7 +33,7 @@ async fn mock_manager(data_dir: PathBuf) -> BlockchainManager {
|
||||
let (mut blockchain_read_handle, mut blockchain_write_handle, _) =
|
||||
cuprate_blockchain::service::init(blockchain_config).unwrap();
|
||||
let (txpool_read_handle, txpool_write_handle, _) =
|
||||
cuprate_txpool::service::init(txpool_config).unwrap();
|
||||
cuprate_txpool::service::init(&txpool_config).unwrap();
|
||||
|
||||
check_add_genesis(
|
||||
&mut blockchain_read_handle,
|
||||
@@ -56,7 +59,7 @@ async fn mock_manager(data_dir: PathBuf) -> BlockchainManager {
|
||||
BlockchainManager {
|
||||
blockchain_write_handle,
|
||||
blockchain_read_handle,
|
||||
txpool_write_handle,
|
||||
txpool_manager_handle: TxpoolManagerHandle::mock(),
|
||||
blockchain_context_service,
|
||||
stop_current_block_downloader: Arc::new(Default::default()),
|
||||
broadcast_svc: BroadcastSvc::mock(),
|
||||
|
||||
@@ -8,7 +8,9 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use arti_client::KeystoreSelector;
|
||||
use clap::Parser;
|
||||
use safelog::DisplayRedacted;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use cuprate_consensus::ContextConfig;
|
||||
@@ -17,11 +19,13 @@ use cuprate_helper::{
|
||||
network::Network,
|
||||
};
|
||||
use cuprate_p2p::block_downloader::BlockDownloaderConfig;
|
||||
use cuprate_p2p_core::ClearNet;
|
||||
use cuprate_p2p_core::{ClearNet, Tor};
|
||||
use cuprate_wire::OnionAddr;
|
||||
|
||||
use crate::{
|
||||
constants::{DEFAULT_CONFIG_STARTUP_DELAY, DEFAULT_CONFIG_WARNING},
|
||||
logging::eprintln_red,
|
||||
tor::{TorContext, TorMode},
|
||||
};
|
||||
|
||||
mod args;
|
||||
@@ -31,6 +35,7 @@ mod rayon;
|
||||
mod rpc;
|
||||
mod storage;
|
||||
mod tokio;
|
||||
mod tor;
|
||||
mod tracing_config;
|
||||
|
||||
#[macro_use]
|
||||
@@ -40,8 +45,9 @@ use fs::FileSystemConfig;
|
||||
use p2p::P2PConfig;
|
||||
use rayon::RayonConfig;
|
||||
pub use rpc::RpcConfig;
|
||||
use storage::StorageConfig;
|
||||
pub use storage::{StorageConfig, TxpoolConfig};
|
||||
use tokio::TokioConfig;
|
||||
use tor::TorConfig;
|
||||
use tracing_config::TracingConfig;
|
||||
|
||||
/// Header to put at the start of the generated config file.
|
||||
@@ -144,6 +150,10 @@ config_struct! {
|
||||
/// Configuration for cuprated's P2P system.
|
||||
pub p2p: P2PConfig,
|
||||
|
||||
#[child = true]
|
||||
/// Configuration for cuprated's Tor component
|
||||
pub tor: TorConfig,
|
||||
|
||||
#[child = true]
|
||||
/// Configuration for cuprated's RPC system.
|
||||
pub rpc: RpcConfig,
|
||||
@@ -165,6 +175,7 @@ impl Default for Config {
|
||||
fast_sync: true,
|
||||
tracing: Default::default(),
|
||||
tokio: Default::default(),
|
||||
tor: Default::default(),
|
||||
rayon: Default::default(),
|
||||
p2p: Default::default(),
|
||||
rpc: Default::default(),
|
||||
@@ -219,11 +230,51 @@ impl Config {
|
||||
gray_peers_percent: self.p2p.clear_net.gray_peers_percent,
|
||||
p2p_port: self.p2p.clear_net.p2p_port,
|
||||
rpc_port: self.rpc.restricted.port_for_p2p(),
|
||||
address_book_config: self
|
||||
.p2p
|
||||
.clear_net
|
||||
.address_book_config
|
||||
.address_book_config(&self.fs.cache_directory, self.network),
|
||||
address_book_config: self.p2p.clear_net.address_book_config.address_book_config(
|
||||
&self.fs.cache_directory,
|
||||
self.network,
|
||||
None,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`Tor`], [`cuprate_p2p::P2PConfig`].
|
||||
pub fn tor_p2p_config(&self, ctx: &TorContext) -> cuprate_p2p::P2PConfig<Tor> {
|
||||
let inbound_enabled = self.p2p.tor_net.inbound_onion;
|
||||
let our_onion_address = match ctx.mode {
|
||||
TorMode::Off => None,
|
||||
TorMode::Daemon => inbound_enabled.then(||
|
||||
OnionAddr::new(
|
||||
&self.tor.daemon.anonymous_inbound,
|
||||
self.p2p.tor_net.p2p_port
|
||||
).expect("Unable to parse supplied `anonymous_inbound` onion address. Please make sure the address is correct.")),
|
||||
TorMode::Arti => inbound_enabled.then(|| {
|
||||
let addr = ctx.arti_onion_service
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.generate_identity_key(KeystoreSelector::Primary)
|
||||
.unwrap()
|
||||
.display_unredacted()
|
||||
.to_string();
|
||||
|
||||
OnionAddr::new(&addr, self.p2p.tor_net.p2p_port).unwrap()
|
||||
})
|
||||
};
|
||||
|
||||
cuprate_p2p::P2PConfig {
|
||||
network: self.network,
|
||||
seeds: p2p::tor_net_seed_nodes(self.network),
|
||||
outbound_connections: self.p2p.tor_net.outbound_connections,
|
||||
extra_outbound_connections: self.p2p.tor_net.extra_outbound_connections,
|
||||
max_inbound_connections: self.p2p.tor_net.max_inbound_connections,
|
||||
gray_peers_percent: self.p2p.tor_net.gray_peers_percent,
|
||||
p2p_port: self.p2p.tor_net.p2p_port,
|
||||
rpc_port: 0,
|
||||
address_book_config: self.p2p.tor_net.address_book_config.address_book_config(
|
||||
&self.fs.cache_directory,
|
||||
self.network,
|
||||
our_onion_address,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ macro_rules! config_struct {
|
||||
|
||||
$($tt: tt)*
|
||||
) => {
|
||||
#[allow(clippy::doc_markdown, clippy::allow_attributes)]
|
||||
$(#[$meta])*
|
||||
pub struct $name {
|
||||
$(
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
path::Path,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use arti_client::{
|
||||
config::onion_service::{OnionServiceConfig, OnionServiceConfigBuilder},
|
||||
TorClient, TorClientBuilder, TorClientConfig,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
|
||||
use cuprate_helper::{fs::address_book_path, network::Network};
|
||||
use cuprate_p2p::config::TransportConfig;
|
||||
use cuprate_p2p_core::{
|
||||
transports::{Tcp, TcpServerConfig},
|
||||
ClearNet, Transport,
|
||||
ClearNet, NetworkZone, Tor, Transport,
|
||||
};
|
||||
use cuprate_p2p_transport::{Arti, ArtiClientConfig, ArtiServerConfig};
|
||||
use cuprate_wire::OnionAddr;
|
||||
|
||||
use crate::{p2p::ProxySettings, tor::TorMode};
|
||||
|
||||
use super::macros::config_struct;
|
||||
|
||||
@@ -24,6 +34,10 @@ config_struct! {
|
||||
/// The clear-net P2P config.
|
||||
pub clear_net: ClearNetConfig,
|
||||
|
||||
#[child = true]
|
||||
/// The tor-net P2P config.
|
||||
pub tor_net: TorNetConfig,
|
||||
|
||||
#[child = true]
|
||||
/// Block downloader config.
|
||||
///
|
||||
@@ -145,9 +159,10 @@ config_struct! {
|
||||
/// Examples | 0.0, 0.5, 0.123, 0.999, 1.0
|
||||
pub gray_peers_percent: f64,
|
||||
|
||||
/// The port to use to accept incoming IPv4 P2P connections.
|
||||
/// The port bind to this network zone.
|
||||
///
|
||||
/// Setting this to 0 will disable incoming P2P connections.
|
||||
/// This port will be bind to if the incoming P2P
|
||||
/// server for this zone has been enabled.
|
||||
///
|
||||
/// Type | Number
|
||||
/// Valid values | 0..65534
|
||||
@@ -163,6 +178,17 @@ config_struct! {
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct ClearNetConfig {
|
||||
|
||||
/// Enable IPv4 inbound server.
|
||||
///
|
||||
/// The inbound server will listen on port `p2p.clear_net.p2p_port`.
|
||||
/// Setting this to `false` will disable incoming IPv4 P2P connections.
|
||||
///
|
||||
/// Type | boolean
|
||||
/// Valid values | false, true
|
||||
/// Examples | false
|
||||
pub enable_inbound: bool,
|
||||
|
||||
/// The IPv4 address to bind and listen for connections on.
|
||||
///
|
||||
/// Type | IPv4 address
|
||||
@@ -171,6 +197,7 @@ config_struct! {
|
||||
|
||||
/// Enable IPv6 inbound server.
|
||||
///
|
||||
/// The inbound server will listen on port `p2p.clear_net.p2p_port`.
|
||||
/// Setting this to `false` will disable incoming IPv6 P2P connections.
|
||||
///
|
||||
/// Type | boolean
|
||||
@@ -183,20 +210,83 @@ config_struct! {
|
||||
/// Type | IPv6 address
|
||||
/// Examples | "::", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
|
||||
pub listen_on_v6: Ipv6Addr,
|
||||
|
||||
#[comment_out = true]
|
||||
/// The proxy to use for outgoing P2P connections
|
||||
///
|
||||
/// This setting can only take "Tor" at the moment.
|
||||
/// This will anonymise clearnet connections through Tor.
|
||||
///
|
||||
/// Setting this to "" (an empty string) will disable the proxy.
|
||||
///
|
||||
/// Enabling this setting will disable inbound connections.
|
||||
///
|
||||
/// Type | String
|
||||
/// Valid values | "Tor"
|
||||
/// Examples | "Tor"
|
||||
pub proxy: ProxySettings,
|
||||
}
|
||||
|
||||
/// The config values for P2P tor.
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct TorNetConfig {
|
||||
|
||||
#[comment_out = true]
|
||||
/// Enable the Tor P2P network.
|
||||
///
|
||||
/// Type | boolean
|
||||
/// Valid values | false, true
|
||||
/// Examples | false
|
||||
pub enabled: bool,
|
||||
|
||||
#[comment_out = true]
|
||||
/// Enable Tor inbound onion server.
|
||||
///
|
||||
/// In Arti mode, setting this to `true` will enable Arti's onion service for accepting inbound
|
||||
/// Tor P2P connections. The keypair and therefore onion address is generated randomly on first run.
|
||||
///
|
||||
/// In Daemon mode, setting this to `true` will enable a TCP server listening for inbound connections
|
||||
/// from your Tor daemon. Refer to the `tor.anonymous_inbound` and `tor.listening_addr` field for onion address
|
||||
/// and listening configuration.
|
||||
///
|
||||
/// The server will listen on port `p2p.tor_net.p2p_port`
|
||||
///
|
||||
/// Type | boolean
|
||||
/// Valid values | false, true
|
||||
/// Examples | false
|
||||
pub inbound_onion: bool,
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ClearNetConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
p2p_port: 18080,
|
||||
enable_inbound: true,
|
||||
listen_on: Ipv4Addr::UNSPECIFIED,
|
||||
enable_inbound_v6: false,
|
||||
listen_on_v6: Ipv6Addr::UNSPECIFIED,
|
||||
proxy: ProxySettings::Socks(String::new()),
|
||||
outbound_connections: 32,
|
||||
extra_outbound_connections: 8,
|
||||
max_inbound_connections: 128,
|
||||
gray_peers_percent: 0.7,
|
||||
address_book_config: AddressBookConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TorNetConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
inbound_onion: false,
|
||||
p2p_port: 18080,
|
||||
outbound_connections: 12,
|
||||
extra_outbound_connections: 2,
|
||||
max_inbound_connections: 128,
|
||||
gray_peers_percent: 0.7,
|
||||
address_book_config: AddressBookConfig::default(),
|
||||
}
|
||||
}
|
||||
@@ -266,16 +356,18 @@ impl Default for AddressBookConfig {
|
||||
|
||||
impl AddressBookConfig {
|
||||
/// Returns the [`cuprate_address_book::AddressBookConfig`].
|
||||
pub fn address_book_config(
|
||||
pub fn address_book_config<Z: NetworkZone>(
|
||||
&self,
|
||||
cache_dir: &Path,
|
||||
network: Network,
|
||||
) -> cuprate_address_book::AddressBookConfig {
|
||||
our_own_address: Option<Z::Addr>,
|
||||
) -> cuprate_address_book::AddressBookConfig<Z> {
|
||||
cuprate_address_book::AddressBookConfig {
|
||||
max_white_list_length: self.max_white_list_length,
|
||||
max_gray_list_length: self.max_gray_list_length,
|
||||
peer_store_directory: address_book_path(cache_dir, network),
|
||||
peer_save_period: self.peer_save_period,
|
||||
our_own_address,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,3 +409,25 @@ pub fn clear_net_seed_nodes(network: Network) -> Vec<SocketAddr> {
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Seed nodes for `Tor`.
|
||||
pub fn tor_net_seed_nodes(network: Network) -> Vec<OnionAddr> {
|
||||
let seeds = match network {
|
||||
Network::Mainnet => [
|
||||
"zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion:18083",
|
||||
"qz43zul2x56jexzoqgkx2trzwcfnr6l3hbtfcfx54g4r3eahy3bssjyd.onion:18083",
|
||||
"plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion:18083",
|
||||
"plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion:18083",
|
||||
"plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion:18083",
|
||||
"aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion:18083",
|
||||
]
|
||||
.as_slice(),
|
||||
Network::Stagenet | Network::Testnet => [].as_slice(),
|
||||
};
|
||||
|
||||
seeds
|
||||
.iter()
|
||||
.map(|s| s.parse())
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ config_struct! {
|
||||
pub struct BlockchainConfig { }
|
||||
|
||||
/// The tx-pool config.
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct TxpoolConfig {
|
||||
/// The maximum size of the tx-pool.
|
||||
@@ -71,6 +71,14 @@ config_struct! {
|
||||
/// Valid values | >= 0
|
||||
/// Examples | 100_000_000, 50_000_000
|
||||
pub max_txpool_byte_size: usize,
|
||||
|
||||
/// The maximum age of transactions in the pool in seconds.
|
||||
/// Transactions will be dropped after this time is reached.
|
||||
///
|
||||
/// Type | Number
|
||||
/// Valid values | >= 0
|
||||
/// Examples | 100_000_000, 50_000_000
|
||||
pub maximum_age_secs: u64,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +87,7 @@ impl Default for TxpoolConfig {
|
||||
Self {
|
||||
sync_mode: SyncMode::default(),
|
||||
max_txpool_byte_size: 100_000_000,
|
||||
maximum_age_secs: 60 * 60 * 24,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
133
binaries/cuprated/src/config/tor.rs
Normal file
133
binaries/cuprated/src/config/tor.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use cuprate_helper::fs::CUPRATE_DATA_DIR;
|
||||
|
||||
use crate::{config::macros::config_struct, tor::TorMode};
|
||||
|
||||
config_struct! {
|
||||
/// Arti config
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
pub struct ArtiConfig {
|
||||
/// Path to the arti state directory.
|
||||
///
|
||||
/// The default directories for each OS:
|
||||
///
|
||||
/// | OS | Path |
|
||||
/// |---------|-----------------------------------------------------|
|
||||
/// | Windows | "C:\Users\Alice\AppData\Roaming\Cuprate\" |
|
||||
/// | macOS | "/Users/Alice/Library/Application Support/Cuprate/" |
|
||||
/// | Linux | "/home/alice/.local/share/cuprate/" |
|
||||
pub directory_path: PathBuf,
|
||||
|
||||
/// Enable isolated circuits for Arti.
|
||||
///
|
||||
/// If set, Arti will use different tor circuits for each connections. This can
|
||||
/// cause stability issues if the connection count is important.
|
||||
///
|
||||
/// Type | boolean
|
||||
/// Valid values | false, true
|
||||
/// Examples | false
|
||||
pub isolated_circuit: bool,
|
||||
|
||||
/// Enable PoW security for Arti.
|
||||
///
|
||||
/// If set, Arti will enforce an EquiX PoW to be resolved for
|
||||
/// other nodes to complete a rendez-vous request when under
|
||||
/// heavy load.
|
||||
///
|
||||
/// Type | boolean
|
||||
/// Valid values | false, true
|
||||
/// Examples | false
|
||||
pub onion_service_pow: bool,
|
||||
}
|
||||
|
||||
/// Tor config
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
pub struct TorDaemonConfig {
|
||||
/// The IP address and port of the external Tor daemon to use for outgoing connections.
|
||||
///
|
||||
/// Type | Socket address
|
||||
/// Examples | "[::1]:9050", "127.0.0.1:9050"
|
||||
pub address: SocketAddr,
|
||||
|
||||
#[comment_out = true]
|
||||
/// Enable inbound connections for Daemon mode
|
||||
///
|
||||
/// This string specify the onion address that should be advertized to the Tor network
|
||||
/// and that your daemon should be expecting connections from.
|
||||
///
|
||||
/// When this is set, `p2p.tor_net.p2p_port` is not used for host listening, but as the source
|
||||
/// port of your hidden service in your torrc configuration file. For setting Cuprate's
|
||||
/// listening port see `tor.listening_addr` field
|
||||
///
|
||||
/// Type | String
|
||||
/// Valid values | "<56 character domain>.onion"
|
||||
/// Examples | "monerotoruzizulg5ttgat2emf4d6fbmiea25detrmmy7erypseyteyd.onion"
|
||||
pub anonymous_inbound: String,
|
||||
|
||||
/// The IP address and port to bind and listen on for anonymous inbound connections from Tor Daemon.
|
||||
///
|
||||
/// Type | Socket address
|
||||
/// Examples | "0.0.0.0:18083", "192.168.1.50:2000", "[::]:5000", "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:18082"
|
||||
pub listening_addr: SocketAddr,
|
||||
}
|
||||
|
||||
/// Tor config
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
pub struct TorConfig {
|
||||
|
||||
#[comment_out = true]
|
||||
/// Enable Tor network by specifying how to connect to it.
|
||||
///
|
||||
/// When "Daemon" is set, the Tor daemon address to use can be
|
||||
/// specified in `tor.daemon_addr`.
|
||||
///
|
||||
/// Type | String
|
||||
/// Valid values | "Arti", "Daemon", "Off"
|
||||
/// Examples | "Arti"
|
||||
pub mode: TorMode,
|
||||
|
||||
#[child = true]
|
||||
/// Arti config
|
||||
///
|
||||
/// Only relevant if `tor.mode` is set to "Arti"
|
||||
pub arti: ArtiConfig,
|
||||
|
||||
#[child = true]
|
||||
/// Tor Daemon config
|
||||
///
|
||||
/// Only relevant if `tor.mode` is set to "Daemon"
|
||||
pub daemon: TorDaemonConfig,
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TorDaemonConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
address: "127.0.0.1:9050".parse().unwrap(),
|
||||
anonymous_inbound: String::new(),
|
||||
listening_addr: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 18083).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ArtiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
directory_path: CUPRATE_DATA_DIR.join("arti"),
|
||||
isolated_circuit: false,
|
||||
onion_service_pow: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,15 +43,15 @@ mod test {
|
||||
fn version() {
|
||||
let semantic_version = format!("{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}");
|
||||
assert_eq!(VERSION, VERSION);
|
||||
assert_eq!(VERSION, "0.0.4");
|
||||
assert_eq!(VERSION, "0.0.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_build() {
|
||||
if cfg!(debug_assertions) {
|
||||
assert_eq!(VERSION_BUILD, "0.0.4-debug");
|
||||
assert_eq!(VERSION_BUILD, "0.0.5-debug");
|
||||
} else {
|
||||
assert_eq!(VERSION_BUILD, "0.0.4-release");
|
||||
assert_eq!(VERSION_BUILD, "0.0.5-release");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ const _: () = {
|
||||
|
||||
/// The killswitch activates if the current timestamp is ahead of this timestamp.
|
||||
///
|
||||
/// Wed Jul 23 12:00:00 AM UTC 2025
|
||||
pub const KILLSWITCH_ACTIVATION_TIMESTAMP: u64 = 1753228800;
|
||||
/// Wed Sep 3 12:00:00 AM UTC 2025
|
||||
pub const KILLSWITCH_ACTIVATION_TIMESTAMP: u64 = 1756857600;
|
||||
|
||||
/// Check if the system clock is past a certain timestamp,
|
||||
/// if so, exit the entire program.
|
||||
@@ -44,8 +44,8 @@ fn killswitch() {
|
||||
/// sanity checking the system's clock to make
|
||||
/// sure it is not overly behind.
|
||||
///
|
||||
/// Wed Jun 4 12:36:45 PM UTC 2025
|
||||
const SYSTEM_CLOCK_SANITY_TIMESTAMP: u64 = 1749040605;
|
||||
/// Tue Jul 15 05:20:00 PM UTC 2025
|
||||
const SYSTEM_CLOCK_SANITY_TIMESTAMP: u64 = 1752600000;
|
||||
|
||||
let current_ts = current_unix_timestamp();
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ use cuprate_types::blockchain::BlockchainWriteRequest;
|
||||
use txpool::IncomingTxHandler;
|
||||
|
||||
use crate::{
|
||||
config::Config, constants::PANIC_CRITICAL_SERVICE_ERROR, logging::CupratedTracingFilter,
|
||||
config::Config,
|
||||
constants::PANIC_CRITICAL_SERVICE_ERROR,
|
||||
logging::CupratedTracingFilter,
|
||||
tor::{initialize_tor_if_enabled, TorMode},
|
||||
};
|
||||
|
||||
mod blockchain;
|
||||
@@ -47,6 +50,7 @@ mod p2p;
|
||||
mod rpc;
|
||||
mod signals;
|
||||
mod statics;
|
||||
mod tor;
|
||||
mod txpool;
|
||||
mod version;
|
||||
|
||||
@@ -88,7 +92,7 @@ fn main() {
|
||||
.expect(DATABASE_CORRUPT_MSG);
|
||||
|
||||
let (txpool_read_handle, txpool_write_handle, _) =
|
||||
cuprate_txpool::service::init_with_pool(config.txpool_config(), db_thread_pool)
|
||||
cuprate_txpool::service::init_with_pool(&config.txpool_config(), db_thread_pool)
|
||||
.inspect_err(|e| error!("Txpool database error: {e}"))
|
||||
.expect(DATABASE_CORRUPT_MSG);
|
||||
|
||||
@@ -118,25 +122,30 @@ fn main() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Bootstrap or configure Tor if enabled.
|
||||
let tor_context = initialize_tor_if_enabled(&config).await;
|
||||
|
||||
// Start p2p network zones
|
||||
let (network_interfaces, tx_handler_subscribers) = p2p::initialize_zones_p2p(
|
||||
&config,
|
||||
context_svc.clone(),
|
||||
blockchain_write_handle.clone(),
|
||||
blockchain_read_handle.clone(),
|
||||
txpool_write_handle.clone(),
|
||||
txpool_read_handle.clone(),
|
||||
tor_context,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create the incoming tx handler service.
|
||||
let tx_handler = IncomingTxHandler::init(
|
||||
config.storage.txpool.clone(),
|
||||
network_interfaces.clearnet_network_interface.clone(),
|
||||
network_interfaces.tor_network_interface,
|
||||
txpool_write_handle.clone(),
|
||||
txpool_read_handle.clone(),
|
||||
context_svc.clone(),
|
||||
blockchain_read_handle.clone(),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
// Send tx handler sender to all network zones
|
||||
for zone in tx_handler_subscribers {
|
||||
@@ -150,7 +159,7 @@ fn main() {
|
||||
network_interfaces.clearnet_network_interface,
|
||||
blockchain_write_handle,
|
||||
blockchain_read_handle.clone(),
|
||||
txpool_write_handle.clone(),
|
||||
tx_handler.txpool_manager.clone(),
|
||||
context_svc.clone(),
|
||||
config.block_downloader_config(),
|
||||
)
|
||||
@@ -162,6 +171,7 @@ fn main() {
|
||||
blockchain_read_handle,
|
||||
context_svc.clone(),
|
||||
txpool_read_handle,
|
||||
tx_handler,
|
||||
);
|
||||
|
||||
// Start the command listener.
|
||||
|
||||
@@ -4,14 +4,20 @@
|
||||
|
||||
use std::convert::From;
|
||||
|
||||
use arti_client::TorClient;
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::oneshot::{self, Sender};
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
use tower::{Service, ServiceExt};
|
||||
|
||||
use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle};
|
||||
use cuprate_consensus::BlockchainContextService;
|
||||
use cuprate_p2p::{config::TransportConfig, NetworkInterface, P2PConfig};
|
||||
use cuprate_p2p_core::{client::InternalPeerID, transports::Tcp, ClearNet, NetworkZone, Transport};
|
||||
use cuprate_p2p_core::{
|
||||
client::InternalPeerID, transports::Tcp, ClearNet, NetworkZone, Tor, Transport,
|
||||
};
|
||||
use cuprate_p2p_transport::{Arti, ArtiClientConfig, Daemon};
|
||||
use cuprate_txpool::service::{TxpoolReadHandle, TxpoolWriteHandle};
|
||||
use cuprate_types::blockchain::BlockchainWriteRequest;
|
||||
|
||||
@@ -19,6 +25,10 @@ use crate::{
|
||||
blockchain,
|
||||
config::Config,
|
||||
constants::PANIC_CRITICAL_SERVICE_ERROR,
|
||||
tor::{
|
||||
transport_arti_config, transport_clearnet_arti_config, transport_daemon_config, TorContext,
|
||||
TorMode,
|
||||
},
|
||||
txpool::{self, IncomingTxHandler},
|
||||
};
|
||||
|
||||
@@ -28,10 +38,20 @@ pub mod request_handler;
|
||||
|
||||
pub use network_address::CrossNetworkInternalPeerId;
|
||||
|
||||
/// A simple parsing enum for the `p2p.clear_net.proxy` field
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub enum ProxySettings {
|
||||
Tor,
|
||||
#[serde(untagged)]
|
||||
Socks(String),
|
||||
}
|
||||
|
||||
/// This struct collect all supported and optional network zone interfaces.
|
||||
pub struct NetworkInterfaces {
|
||||
/// Mandatory clearnet network interface
|
||||
pub clearnet_network_interface: NetworkInterface<ClearNet>,
|
||||
/// Optional tor network interface
|
||||
pub tor_network_interface: Option<NetworkInterface<Tor>>,
|
||||
// ...one can dream for more!
|
||||
}
|
||||
|
||||
@@ -39,6 +59,7 @@ impl NetworkInterfaces {
|
||||
pub const fn new(clearnet_network_interface: NetworkInterface<ClearNet>) -> Self {
|
||||
Self {
|
||||
clearnet_network_interface,
|
||||
tor_network_interface: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,25 +69,93 @@ impl NetworkInterfaces {
|
||||
pub async fn initialize_zones_p2p(
|
||||
config: &Config,
|
||||
context_svc: BlockchainContextService,
|
||||
mut blockchain_write_handle: BlockchainWriteHandle,
|
||||
mut blockchain_read_handle: BlockchainReadHandle,
|
||||
txpool_write_handle: TxpoolWriteHandle,
|
||||
txpool_read_handle: TxpoolReadHandle,
|
||||
tor_ctx: TorContext,
|
||||
) -> (NetworkInterfaces, Vec<Sender<IncomingTxHandler>>) {
|
||||
// Start TCP clearnet P2P.
|
||||
let (clearnet, incoming_tx_handler_tx) = start_zone_p2p::<ClearNet, Tcp>(
|
||||
blockchain_read_handle.clone(),
|
||||
context_svc.clone(),
|
||||
txpool_read_handle.clone(),
|
||||
config.clearnet_p2p_config(),
|
||||
(&config.p2p.clear_net).into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Start clearnet P2P.
|
||||
let (clearnet, incoming_tx_handler_tx) = {
|
||||
// If proxy is set
|
||||
match config.p2p.clear_net.proxy {
|
||||
ProxySettings::Tor => match tor_ctx.mode {
|
||||
TorMode::Arti => {
|
||||
tracing::info!("Anonymizing clearnet connections through Arti.");
|
||||
start_zone_p2p::<ClearNet, Arti>(
|
||||
blockchain_read_handle.clone(),
|
||||
context_svc.clone(),
|
||||
txpool_read_handle.clone(),
|
||||
config.clearnet_p2p_config(),
|
||||
transport_clearnet_arti_config(&tor_ctx),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
TorMode::Daemon => {
|
||||
tracing::error!("Anonymizing clearnet connections through the Tor daemon is not yet supported.");
|
||||
std::process::exit(0);
|
||||
}
|
||||
TorMode::Off => {
|
||||
tracing::error!("Clearnet proxy set to \"tor\" but Tor is actually off. Please be sure to set a mode in the configuration or command line");
|
||||
std::process::exit(0);
|
||||
}
|
||||
},
|
||||
ProxySettings::Socks(ref s) => {
|
||||
if !s.is_empty() {
|
||||
tracing::error!("Socks proxy is not yet supported.");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
start_zone_p2p::<ClearNet, Tcp>(
|
||||
blockchain_read_handle.clone(),
|
||||
context_svc.clone(),
|
||||
txpool_read_handle.clone(),
|
||||
config.clearnet_p2p_config(),
|
||||
(&config.p2p.clear_net).into(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create network interface collection
|
||||
let network_interfaces = NetworkInterfaces::new(clearnet);
|
||||
let tx_handler_subscribers = vec![incoming_tx_handler_tx];
|
||||
let mut network_interfaces = NetworkInterfaces::new(clearnet);
|
||||
let mut tx_handler_subscribers = vec![incoming_tx_handler_tx];
|
||||
|
||||
// Start Tor P2P (if enabled)
|
||||
let tor = if config.p2p.tor_net.enabled {
|
||||
match tor_ctx.mode {
|
||||
TorMode::Off => None,
|
||||
TorMode::Daemon => Some(
|
||||
start_zone_p2p::<Tor, Daemon>(
|
||||
blockchain_read_handle.clone(),
|
||||
context_svc.clone(),
|
||||
txpool_read_handle.clone(),
|
||||
config.tor_p2p_config(&tor_ctx),
|
||||
transport_daemon_config(config),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
),
|
||||
TorMode::Arti => Some(
|
||||
start_zone_p2p::<Tor, Arti>(
|
||||
blockchain_read_handle.clone(),
|
||||
context_svc.clone(),
|
||||
txpool_read_handle.clone(),
|
||||
config.tor_p2p_config(&tor_ctx),
|
||||
transport_arti_config(config, tor_ctx),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((tor, incoming_tx_handler_tx)) = tor {
|
||||
network_interfaces.tor_network_interface = Some(tor);
|
||||
tx_handler_subscribers.push(incoming_tx_handler_tx);
|
||||
}
|
||||
|
||||
(network_interfaces, tx_handler_subscribers)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use cuprate_p2p_core::{client::InternalPeerID, ClearNet, NetworkZone};
|
||||
use cuprate_p2p_core::{client::InternalPeerID, ClearNet, NetworkZone, Tor};
|
||||
use cuprate_wire::OnionAddr;
|
||||
|
||||
/// An identifier for a P2P peer on any network.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum CrossNetworkInternalPeerId {
|
||||
/// A clear-net peer.
|
||||
ClearNet(InternalPeerID<<ClearNet as NetworkZone>::Addr>),
|
||||
/// A Tor onion peer.
|
||||
Tor(InternalPeerID<<Tor as NetworkZone>::Addr>),
|
||||
}
|
||||
|
||||
impl From<InternalPeerID<<ClearNet as NetworkZone>::Addr>> for CrossNetworkInternalPeerId {
|
||||
fn from(addr: InternalPeerID<<ClearNet as NetworkZone>::Addr>) -> Self {
|
||||
impl From<InternalPeerID<SocketAddr>> for CrossNetworkInternalPeerId {
|
||||
fn from(addr: InternalPeerID<SocketAddr>) -> Self {
|
||||
Self::ClearNet(addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InternalPeerID<OnionAddr>> for CrossNetworkInternalPeerId {
|
||||
fn from(addr: InternalPeerID<OnionAddr>) -> Self {
|
||||
Self::Tor(addr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use monero_serai::{block::Block, transaction::Transaction};
|
||||
use tokio::sync::{broadcast, oneshot, watch};
|
||||
use tokio_stream::wrappers::WatchStream;
|
||||
use tower::{Service, ServiceExt};
|
||||
use tracing::instrument;
|
||||
|
||||
use cuprate_blockchain::service::BlockchainReadHandle;
|
||||
use cuprate_consensus::{
|
||||
@@ -22,10 +23,9 @@ use cuprate_consensus::{
|
||||
};
|
||||
use cuprate_dandelion_tower::TxState;
|
||||
use cuprate_fixed_bytes::ByteArrayVec;
|
||||
use cuprate_helper::cast::u64_to_usize;
|
||||
use cuprate_helper::{
|
||||
asynch::rayon_spawn_async,
|
||||
cast::usize_to_u64,
|
||||
cast::{u64_to_usize, usize_to_u64},
|
||||
map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits},
|
||||
};
|
||||
use cuprate_p2p::constants::{
|
||||
@@ -363,6 +363,7 @@ async fn new_fluffy_block<A: NetZoneAddress>(
|
||||
}
|
||||
|
||||
/// [`ProtocolRequest::NewTransactions`]
|
||||
#[instrument(level = "debug", skip_all, fields(txs = request.txs.len(), stem = !request.dandelionpp_fluff))]
|
||||
async fn new_transactions<A>(
|
||||
peer_information: PeerInformation<A>,
|
||||
request: NewTransactions,
|
||||
@@ -373,25 +374,33 @@ where
|
||||
A: NetZoneAddress,
|
||||
InternalPeerID<A>: Into<CrossNetworkInternalPeerId>,
|
||||
{
|
||||
tracing::debug!("handling new transactions");
|
||||
|
||||
let context = blockchain_context_service.blockchain_context();
|
||||
|
||||
// If we are more than 2 blocks behind the peer then ignore the txs - we are probably still syncing.
|
||||
if usize_to_u64(context.chain_height + 2)
|
||||
< peer_information
|
||||
.core_sync_data
|
||||
.lock()
|
||||
.unwrap()
|
||||
.current_height
|
||||
{
|
||||
let peer_height = peer_information
|
||||
.core_sync_data
|
||||
.lock()
|
||||
.unwrap()
|
||||
.current_height;
|
||||
if usize_to_u64(context.chain_height + 2) < peer_height {
|
||||
tracing::debug!(
|
||||
our_height = context.chain_height,
|
||||
peer_height,
|
||||
"we are too far behind peer, ignoring txs."
|
||||
);
|
||||
return Ok(ProtocolResponse::NA);
|
||||
}
|
||||
|
||||
let state = if request.dandelionpp_fluff {
|
||||
let addr = peer_information.id.into();
|
||||
|
||||
let anon_zone = matches!(addr, CrossNetworkInternalPeerId::Tor(_));
|
||||
|
||||
let state = if request.dandelionpp_fluff && !anon_zone {
|
||||
TxState::Fluff
|
||||
} else {
|
||||
TxState::Stem {
|
||||
from: peer_information.id.into(),
|
||||
}
|
||||
TxState::Stem { from: addr }
|
||||
};
|
||||
|
||||
// Drop all the data except the stuff we still need.
|
||||
@@ -401,7 +410,12 @@ where
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(IncomingTxs { txs, state })
|
||||
.call(IncomingTxs {
|
||||
txs,
|
||||
state,
|
||||
drop_relay_rule_errors: true,
|
||||
do_not_relay: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! <https://github.com/Cuprate/cuprate/pull/355>
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
num::NonZero,
|
||||
time::{Duration, Instant},
|
||||
@@ -22,6 +23,7 @@ use cuprate_helper::{
|
||||
cast::{u32_to_usize, u64_to_usize, usize_to_u64},
|
||||
fmt::HexPrefix,
|
||||
map::split_u128_into_low_high_bits,
|
||||
time::current_unix_timestamp,
|
||||
};
|
||||
use cuprate_hex::{Hex, HexVec};
|
||||
use cuprate_p2p_core::{client::handshaker::builder::DummyAddressBook, ClearNet, Network};
|
||||
@@ -58,6 +60,7 @@ use cuprate_types::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
blockchain::interface as blockchain_interface,
|
||||
constants::VERSION_BUILD,
|
||||
rpc::{
|
||||
constants::{FIELD_NOT_SUPPORTED, UNSUPPORTED_RPC_CALL},
|
||||
@@ -80,7 +83,7 @@ pub async fn map_request(
|
||||
Req::GetBlockTemplate(r) => Resp::GetBlockTemplate(not_available()?),
|
||||
Req::GetBlockCount(r) => Resp::GetBlockCount(get_block_count(state, r).await?),
|
||||
Req::OnGetBlockHash(r) => Resp::OnGetBlockHash(on_get_block_hash(state, r).await?),
|
||||
Req::SubmitBlock(r) => Resp::SubmitBlock(not_available()?),
|
||||
Req::SubmitBlock(r) => Resp::SubmitBlock(submit_block(state, r).await?),
|
||||
Req::GenerateBlocks(r) => Resp::GenerateBlocks(not_available()?),
|
||||
Req::GetLastBlockHeader(r) => {
|
||||
Resp::GetLastBlockHeader(get_last_block_header(state, r).await?)
|
||||
@@ -234,7 +237,13 @@ async fn submit_block(
|
||||
let block_id = Hex(block.hash());
|
||||
|
||||
// Attempt to relay the block.
|
||||
blockchain_manager::relay_block(todo!(), Box::new(block)).await?;
|
||||
blockchain_interface::handle_incoming_block(
|
||||
block,
|
||||
HashMap::new(), // this function reads the txpool
|
||||
&mut state.blockchain_read,
|
||||
&mut state.txpool_read,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(SubmitBlockResponse {
|
||||
base: helper::response_base(false),
|
||||
@@ -915,13 +924,15 @@ async fn get_transaction_pool_backlog(
|
||||
mut state: CupratedRpcHandler,
|
||||
_: GetTransactionPoolBacklogRequest,
|
||||
) -> Result<GetTransactionPoolBacklogResponse, Error> {
|
||||
let now = current_unix_timestamp();
|
||||
|
||||
let backlog = txpool::backlog(&mut state.txpool_read)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|entry| TxBacklogEntry {
|
||||
weight: entry.weight,
|
||||
weight: usize_to_u64(entry.weight),
|
||||
fee: entry.fee,
|
||||
time_in_pool: entry.time_in_pool.as_secs(),
|
||||
time_in_pool: now - entry.received_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -960,7 +971,7 @@ async fn get_miner_data(
|
||||
.into_iter()
|
||||
.map(|entry| GetMinerDataTxBacklogEntry {
|
||||
id: Hex(entry.id),
|
||||
weight: entry.weight,
|
||||
weight: usize_to_u64(entry.weight),
|
||||
fee: entry.fee,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -16,6 +16,7 @@ use cuprate_constants::rpc::{
|
||||
MAX_RESTRICTED_GLOBAL_FAKE_OUTS_COUNT, RESTRICTED_SPENT_KEY_IMAGES_COUNT,
|
||||
RESTRICTED_TRANSACTIONS_COUNT,
|
||||
};
|
||||
use cuprate_dandelion_tower::TxState;
|
||||
use cuprate_helper::cast::usize_to_u64;
|
||||
use cuprate_hex::{Hex, HexVec};
|
||||
use cuprate_p2p_core::{client::handshaker::builder::DummyAddressBook, ClearNet};
|
||||
@@ -49,11 +50,17 @@ use cuprate_types::{
|
||||
use crate::{
|
||||
rpc::{
|
||||
constants::UNSUPPORTED_RPC_CALL,
|
||||
handlers::{helper, shared, shared::not_available},
|
||||
service::{address_book, blockchain, blockchain_context, blockchain_manager, txpool},
|
||||
handlers::{
|
||||
helper,
|
||||
shared::{self, not_available},
|
||||
},
|
||||
service::{
|
||||
address_book, blockchain, blockchain_context, blockchain_manager, tx_handler, txpool,
|
||||
},
|
||||
CupratedRpcHandler,
|
||||
},
|
||||
statics::START_INSTANT_UNIX,
|
||||
txpool::IncomingTxs,
|
||||
};
|
||||
|
||||
/// Map a [`OtherRequest`] to the function that will lead to a [`OtherResponse`].
|
||||
@@ -69,7 +76,9 @@ pub async fn map_request(
|
||||
Req::GetTransactions(r) => Resp::GetTransactions(not_available()?),
|
||||
Req::GetAltBlocksHashes(r) => Resp::GetAltBlocksHashes(not_available()?),
|
||||
Req::IsKeyImageSpent(r) => Resp::IsKeyImageSpent(not_available()?),
|
||||
Req::SendRawTransaction(r) => Resp::SendRawTransaction(not_available()?),
|
||||
Req::SendRawTransaction(r) => {
|
||||
Resp::SendRawTransaction(send_raw_transaction(state, r).await?)
|
||||
}
|
||||
Req::SaveBc(r) => Resp::SaveBc(not_available()?),
|
||||
Req::GetPeerList(r) => Resp::GetPeerList(not_available()?),
|
||||
Req::SetLogLevel(r) => Resp::SetLogLevel(not_available()?),
|
||||
@@ -442,14 +451,32 @@ async fn send_raw_transaction(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle to txpool service.
|
||||
let tx_relay_checks =
|
||||
txpool::check_maybe_relay_local(todo!(), tx, !request.do_not_relay).await?;
|
||||
if state.is_restricted() && request.do_not_relay {
|
||||
// FIXME: implement something like `/check_tx` in `cuprated/monerod`.
|
||||
// boog900:
|
||||
// > making nodes hold txs in their pool that don't get passed
|
||||
// > around the network can cause issues, like targeted tx pool double spends
|
||||
// > there is also no reason to have this for public RPC
|
||||
return Err(anyhow!("do_not_relay is not supported on restricted RPC"));
|
||||
}
|
||||
|
||||
let txs = vec![tx.serialize().into()];
|
||||
|
||||
let mut txs = IncomingTxs {
|
||||
txs,
|
||||
state: TxState::Local,
|
||||
drop_relay_rule_errors: false,
|
||||
do_not_relay: request.do_not_relay,
|
||||
};
|
||||
|
||||
let tx_relay_checks = tx_handler::handle_incoming_txs(&mut state.tx_handler, txs).await?;
|
||||
|
||||
if tx_relay_checks.is_empty() {
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
resp.not_relayed = true;
|
||||
|
||||
// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server.cpp#L124>
|
||||
fn add_reason(reasons: &mut String, reason: &'static str) {
|
||||
if !reasons.is_empty() {
|
||||
|
||||
@@ -19,7 +19,7 @@ use cuprate_rpc_types::{
|
||||
use cuprate_txpool::service::TxpoolReadHandle;
|
||||
use cuprate_types::BlockTemplate;
|
||||
|
||||
use crate::rpc::handlers;
|
||||
use crate::{rpc::handlers, txpool::IncomingTxHandler};
|
||||
|
||||
/// TODO: use real type when public.
|
||||
#[derive(Clone)]
|
||||
@@ -169,7 +169,8 @@ pub struct CupratedRpcHandler {
|
||||
|
||||
/// Read handle to the transaction pool database.
|
||||
pub txpool_read: TxpoolReadHandle,
|
||||
// TODO: handle to txpool service.
|
||||
|
||||
pub tx_handler: IncomingTxHandler,
|
||||
}
|
||||
|
||||
impl CupratedRpcHandler {
|
||||
@@ -179,12 +180,14 @@ impl CupratedRpcHandler {
|
||||
blockchain_read: BlockchainReadHandle,
|
||||
blockchain_context: BlockchainContextService,
|
||||
txpool_read: TxpoolReadHandle,
|
||||
tx_handler: IncomingTxHandler,
|
||||
) -> Self {
|
||||
Self {
|
||||
restricted,
|
||||
blockchain_read,
|
||||
blockchain_context,
|
||||
txpool_read,
|
||||
tx_handler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use cuprate_txpool::service::TxpoolReadHandle;
|
||||
use crate::{
|
||||
config::RpcConfig,
|
||||
rpc::{rpc_handler::BlockchainManagerHandle, CupratedRpcHandler},
|
||||
txpool::IncomingTxHandler,
|
||||
};
|
||||
|
||||
/// Initialize the RPC server(s).
|
||||
@@ -33,6 +34,7 @@ pub fn init_rpc_servers(
|
||||
blockchain_read: BlockchainReadHandle,
|
||||
blockchain_context: BlockchainContextService,
|
||||
txpool_read: TxpoolReadHandle,
|
||||
tx_handler: IncomingTxHandler,
|
||||
) {
|
||||
for ((enable, addr, request_byte_limit), restricted) in [
|
||||
(
|
||||
@@ -76,6 +78,7 @@ pub fn init_rpc_servers(
|
||||
blockchain_read.clone(),
|
||||
blockchain_context.clone(),
|
||||
txpool_read.clone(),
|
||||
tx_handler.clone(),
|
||||
);
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
@@ -107,6 +110,8 @@ async fn run_rpc_server(
|
||||
let router = RouterBuilder::new()
|
||||
.json_rpc()
|
||||
.other_get_height()
|
||||
.other_send_raw_transaction()
|
||||
.other_sendrawtransaction()
|
||||
.fallback()
|
||||
.build()
|
||||
.with_state(rpc_handler);
|
||||
|
||||
@@ -16,4 +16,5 @@ pub(super) mod address_book;
|
||||
pub(super) mod blockchain;
|
||||
pub(super) mod blockchain_context;
|
||||
pub(super) mod blockchain_manager;
|
||||
pub(super) mod tx_handler;
|
||||
pub(super) mod txpool;
|
||||
|
||||
69
binaries/cuprated/src/rpc/service/tx_handler.rs
Normal file
69
binaries/cuprated/src/rpc/service/tx_handler.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use anyhow::{anyhow, Error};
|
||||
use cuprate_consensus::ExtendedConsensusError;
|
||||
use cuprate_consensus_rules::{transactions::TransactionError, ConsensusError};
|
||||
use tower::{Service, ServiceExt};
|
||||
|
||||
use cuprate_types::TxRelayChecks;
|
||||
|
||||
use crate::txpool::{IncomingTxError, IncomingTxHandler, IncomingTxs, RelayRuleError};
|
||||
|
||||
pub async fn handle_incoming_txs(
|
||||
tx_handler: &mut IncomingTxHandler,
|
||||
incoming_txs: IncomingTxs,
|
||||
) -> Result<TxRelayChecks, Error> {
|
||||
let resp = tx_handler
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| anyhow!(e))?
|
||||
.call(incoming_txs)
|
||||
.await;
|
||||
|
||||
Ok(match resp {
|
||||
Ok(()) => TxRelayChecks::empty(),
|
||||
Err(e) => match e {
|
||||
IncomingTxError::Consensus(ExtendedConsensusError::ConErr(
|
||||
ConsensusError::Transaction(e),
|
||||
)) => match e {
|
||||
TransactionError::TooBig => TxRelayChecks::TOO_BIG,
|
||||
TransactionError::KeyImageSpent => TxRelayChecks::DOUBLE_SPEND,
|
||||
|
||||
TransactionError::OutputNotValidPoint
|
||||
| TransactionError::OutputTypeInvalid
|
||||
| TransactionError::ZeroOutputForV1
|
||||
| TransactionError::NonZeroOutputForV2
|
||||
| TransactionError::OutputsOverflow
|
||||
| TransactionError::OutputsTooHigh => TxRelayChecks::INVALID_OUTPUT,
|
||||
|
||||
TransactionError::MoreThanOneMixableInputWithUnmixable
|
||||
| TransactionError::InvalidNumberOfOutputs
|
||||
| TransactionError::InputDoesNotHaveExpectedNumbDecoys
|
||||
| TransactionError::IncorrectInputType
|
||||
| TransactionError::InputsAreNotOrdered
|
||||
| TransactionError::InputsOverflow
|
||||
| TransactionError::NoInputs => TxRelayChecks::INVALID_INPUT,
|
||||
|
||||
TransactionError::KeyImageIsNotInPrimeSubGroup
|
||||
| TransactionError::AmountNotDecomposed
|
||||
| TransactionError::DuplicateRingMember
|
||||
| TransactionError::OneOrMoreRingMembersLocked
|
||||
| TransactionError::RingMemberNotFoundOrInvalid
|
||||
| TransactionError::RingSignatureIncorrect
|
||||
| TransactionError::TransactionVersionInvalid
|
||||
| TransactionError::RingCTError(_) => return Err(anyhow!("unreachable")),
|
||||
},
|
||||
IncomingTxError::Parse(_) | IncomingTxError::Consensus(_) => {
|
||||
return Err(anyhow!("unreachable"))
|
||||
}
|
||||
IncomingTxError::RelayRule(RelayRuleError::NonZeroTimelock) => {
|
||||
TxRelayChecks::NONZERO_UNLOCK_TIME
|
||||
}
|
||||
IncomingTxError::RelayRule(RelayRuleError::ExtraFieldTooLarge) => {
|
||||
TxRelayChecks::TX_EXTRA_TOO_BIG
|
||||
}
|
||||
IncomingTxError::RelayRule(RelayRuleError::FeeBelowMinimum) => {
|
||||
TxRelayChecks::FEE_TOO_LOW
|
||||
}
|
||||
IncomingTxError::DuplicateTransaction => TxRelayChecks::DOUBLE_SPEND,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
//! Functions to send [`TxpoolReadRequest`]s.
|
||||
|
||||
use std::{collections::HashSet, convert::Infallible, num::NonZero};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
convert::Infallible,
|
||||
num::NonZero,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use monero_serai::transaction::Transaction;
|
||||
@@ -17,7 +21,7 @@ use cuprate_txpool::{
|
||||
};
|
||||
use cuprate_types::{
|
||||
rpc::{PoolInfo, PoolInfoFull, PoolInfoIncremental, PoolTxInfo, TxpoolStats},
|
||||
TxInPool, TxRelayChecks,
|
||||
TransactionVerificationData, TxInPool, TxRelayChecks,
|
||||
};
|
||||
|
||||
// FIXME: use `anyhow::Error` over `tower::BoxError` in txpool.
|
||||
@@ -222,6 +226,25 @@ pub async fn all_hashes(
|
||||
Ok(hashes)
|
||||
}
|
||||
|
||||
/// [`TxpoolReadRequest::TxsForBlock`]
|
||||
pub async fn txs_for_block(
|
||||
txpool_read: &mut TxpoolReadHandle,
|
||||
tx_hashes: Vec<[u8; 32]>,
|
||||
) -> Result<(HashMap<[u8; 32], TransactionVerificationData>, Vec<usize>), Error> {
|
||||
let TxpoolReadResponse::TxsForBlock { txs, missing } = txpool_read
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| anyhow!(e))?
|
||||
.call(TxpoolReadRequest::TxsForBlock(tx_hashes))
|
||||
.await
|
||||
.map_err(|e| anyhow!(e))?
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
Ok((txs, missing))
|
||||
}
|
||||
|
||||
/// TODO: impl txpool manager.
|
||||
pub async fn flush(txpool_manager: &mut Infallible, tx_hashes: Vec<[u8; 32]>) -> Result<(), Error> {
|
||||
todo!();
|
||||
@@ -233,12 +256,3 @@ pub async fn relay(txpool_manager: &mut Infallible, tx_hashes: Vec<[u8; 32]>) ->
|
||||
todo!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// TODO: impl txpool manager.
|
||||
pub async fn check_maybe_relay_local(
|
||||
txpool_manager: &mut Infallible,
|
||||
tx: Transaction,
|
||||
relay: bool,
|
||||
) -> Result<TxRelayChecks, Error> {
|
||||
Ok(todo!())
|
||||
}
|
||||
|
||||
197
binaries/cuprated/src/tor.rs
Normal file
197
binaries/cuprated/src/tor.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
//! Tor initialization
|
||||
//!
|
||||
//! Extract configuration and initialize Arti.
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Imports
|
||||
|
||||
use std::{default, sync::Arc};
|
||||
|
||||
use arti_client::{
|
||||
config::{onion_service::OnionServiceConfigBuilder, CfgPath, TorClientConfigBuilder},
|
||||
KeystoreSelector, StreamPrefs, TorClient, TorClientBuilder, TorClientConfig,
|
||||
};
|
||||
use futures::Stream;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tor_hsservice::{OnionService, RendRequest, RunningOnionService};
|
||||
use tor_persist::hsnickname::HsNickname;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
use tracing::info;
|
||||
|
||||
use cuprate_helper::fs::CUPRATE_DATA_DIR;
|
||||
use cuprate_p2p::TransportConfig;
|
||||
use cuprate_p2p_core::{ClearNet, Tor};
|
||||
use cuprate_p2p_transport::{
|
||||
Arti, ArtiClientConfig, ArtiServerConfig, Daemon, DaemonClientConfig, DaemonServerConfig,
|
||||
};
|
||||
use cuprate_wire::OnionAddr;
|
||||
|
||||
use crate::{config::Config, p2p::ProxySettings};
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Initialization
|
||||
|
||||
#[derive(Clone, Default, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
/// Describe if Tor is enabled and how
|
||||
pub enum TorMode {
|
||||
/// Use of the [`arti_client`] library.
|
||||
Arti,
|
||||
/// Use of external tor daemon
|
||||
Daemon,
|
||||
|
||||
#[default]
|
||||
/// Tor is disabled
|
||||
Off,
|
||||
}
|
||||
|
||||
/// Contains the necessary Tor configuration or structures
|
||||
/// for initializing P2P.
|
||||
pub struct TorContext {
|
||||
/// Which mode are we using.
|
||||
pub mode: TorMode,
|
||||
|
||||
// -------- Only in Arti mode
|
||||
/// Arti bootstrapped [`TorClient`].
|
||||
pub bootstrapped_client: Option<TorClient<PreferredRuntime>>,
|
||||
/// Arti bootstrapped client config
|
||||
pub arti_client_config: Option<TorClientConfig>,
|
||||
/// Arti onion service address.
|
||||
pub arti_onion_service: Option<OnionService>,
|
||||
}
|
||||
|
||||
/// Initialize the Tor network if enabled in configuration
|
||||
///
|
||||
/// This function will bootstrap Arti if needed by Tor network zone or
|
||||
/// clearnet as a proxy.
|
||||
pub async fn initialize_tor_if_enabled(config: &Config) -> TorContext {
|
||||
let mode = config.tor.mode;
|
||||
let anonymize_clearnet = matches!(config.p2p.clear_net.proxy, ProxySettings::Tor);
|
||||
|
||||
// Start Arti client
|
||||
let (bootstrapped_client, arti_client_config) =
|
||||
if mode == TorMode::Arti && (config.p2p.tor_net.enabled || anonymize_clearnet) {
|
||||
Some(initialize_arti_client(config).await)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.unzip();
|
||||
|
||||
// Start Arti onion service
|
||||
let arti_onion_service = arti_client_config
|
||||
.as_ref()
|
||||
.map(|client_config| initialize_arti_onion_service(client_config, config));
|
||||
|
||||
TorContext {
|
||||
mode,
|
||||
bootstrapped_client,
|
||||
arti_client_config,
|
||||
arti_onion_service,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize Arti Tor client.
|
||||
async fn initialize_arti_client(config: &Config) -> (TorClient<PreferredRuntime>, TorClientConfig) {
|
||||
// Configuration
|
||||
let mut tor_config = TorClientConfig::builder();
|
||||
|
||||
// Storage
|
||||
tor_config
|
||||
.storage()
|
||||
.state_dir(CfgPath::new_literal(config.tor.arti.directory_path.clone()));
|
||||
|
||||
let tor_config = tor_config
|
||||
.build()
|
||||
.expect("Failed to build Tor client configuration.");
|
||||
|
||||
// Bootstrapping
|
||||
info!("Bootstrapping Arti's TorClient...");
|
||||
let mut tor_client = TorClient::builder()
|
||||
.config(tor_config.clone())
|
||||
.create_bootstrapped()
|
||||
.await
|
||||
.inspect_err(|err| tracing::error!("Unable to bootstrap arti: {err}"))
|
||||
.unwrap();
|
||||
|
||||
// Isolation
|
||||
if config.tor.arti.isolated_circuit {
|
||||
let mut stream_prefs = StreamPrefs::new();
|
||||
stream_prefs.isolate_every_stream();
|
||||
tor_client.set_stream_prefs(stream_prefs);
|
||||
}
|
||||
|
||||
(tor_client, tor_config)
|
||||
}
|
||||
|
||||
fn initialize_arti_onion_service(client_config: &TorClientConfig, config: &Config) -> OnionService {
|
||||
let onion_svc_config = OnionServiceConfigBuilder::default()
|
||||
.enable_pow(config.tor.arti.onion_service_pow)
|
||||
.nickname(HsNickname::new("cuprate".into()).unwrap())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
TorClient::<PreferredRuntime>::create_onion_service(client_config, onion_svc_config)
|
||||
.expect("Unable to start Arti onion service.")
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Transport configuration
|
||||
|
||||
pub fn transport_arti_config(config: &Config, ctx: TorContext) -> TransportConfig<Tor, Arti> {
|
||||
// Extracting
|
||||
let (Some(bootstrapped_client), Some(client_config)) =
|
||||
(ctx.bootstrapped_client, ctx.arti_client_config)
|
||||
else {
|
||||
panic!("Arti client should be initialized");
|
||||
};
|
||||
|
||||
let server_config = config.p2p.tor_net.inbound_onion.then(|| {
|
||||
let Some(onion_svc) = ctx.arti_onion_service else {
|
||||
panic!("inbound onion enabled, but no onion service initialized!");
|
||||
};
|
||||
|
||||
ArtiServerConfig::new(
|
||||
onion_svc,
|
||||
config.p2p.tor_net.p2p_port,
|
||||
&bootstrapped_client,
|
||||
&client_config,
|
||||
)
|
||||
});
|
||||
|
||||
TransportConfig::<Tor, Arti> {
|
||||
client_config: ArtiClientConfig {
|
||||
client: bootstrapped_client,
|
||||
},
|
||||
server_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transport_clearnet_arti_config(ctx: &TorContext) -> TransportConfig<ClearNet, Arti> {
|
||||
let Some(bootstrapped_client) = &ctx.bootstrapped_client else {
|
||||
panic!("Arti enabled but no TorClient initialized!");
|
||||
};
|
||||
|
||||
TransportConfig::<ClearNet, Arti> {
|
||||
client_config: ArtiClientConfig {
|
||||
client: bootstrapped_client.clone(),
|
||||
},
|
||||
server_config: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transport_daemon_config(config: &Config) -> TransportConfig<Tor, Daemon> {
|
||||
let mut invalid_onion = false;
|
||||
|
||||
if config.p2p.tor_net.inbound_onion && config.tor.daemon.anonymous_inbound.is_empty() {
|
||||
invalid_onion = true;
|
||||
tracing::warn!("Onion inbound is enabled yet no onion host has been defined in configuration. Inbound server disabled.");
|
||||
}
|
||||
|
||||
TransportConfig::<Tor, Daemon> {
|
||||
client_config: DaemonClientConfig {
|
||||
tor_daemon: config.tor.daemon.address,
|
||||
},
|
||||
server_config: (config.p2p.tor_net.inbound_onion && !invalid_onion).then_some(
|
||||
DaemonServerConfig {
|
||||
ip: config.tor.daemon.listening_addr.ip(),
|
||||
port: config.tor.daemon.listening_addr.port(),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,10 @@ use cuprate_txpool::service::{TxpoolReadHandle, TxpoolWriteHandle};
|
||||
|
||||
mod dandelion;
|
||||
mod incoming_tx;
|
||||
mod manager;
|
||||
mod relay_rules;
|
||||
mod txs_being_handled;
|
||||
|
||||
pub use incoming_tx::{IncomingTxError, IncomingTxHandler, IncomingTxs};
|
||||
pub use manager::TxpoolManagerHandle;
|
||||
pub use relay_rules::RelayRuleError;
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
task::{ready, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures::{future::BoxFuture, FutureExt, TryFutureExt};
|
||||
use tower::{Service, ServiceExt};
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::PollSender;
|
||||
|
||||
use cuprate_dandelion_tower::{
|
||||
pool::DandelionPoolService, DandelionConfig, DandelionRouter, Graph,
|
||||
pool::DandelionPoolService, traits::StemRequest, DandelionConfig, DandelionRouteReq,
|
||||
DandelionRouter, DandelionRouterError, Graph, State, TxState,
|
||||
};
|
||||
use cuprate_p2p::NetworkInterface;
|
||||
use cuprate_p2p_core::ClearNet;
|
||||
use cuprate_p2p_core::{client::InternalPeerID, ClearNet, NetworkZone, Tor};
|
||||
use cuprate_txpool::service::{TxpoolReadHandle, TxpoolWriteHandle};
|
||||
|
||||
use crate::{
|
||||
@@ -12,10 +22,14 @@ use crate::{
|
||||
txpool::incoming_tx::{DandelionTx, TxId},
|
||||
};
|
||||
|
||||
mod anon_net_service;
|
||||
mod diffuse_service;
|
||||
mod stem_service;
|
||||
mod tx_store;
|
||||
|
||||
pub use anon_net_service::AnonTxService;
|
||||
pub use diffuse_service::DiffuseService;
|
||||
|
||||
/// The configuration used for [`cuprate_dandelion_tower`].
|
||||
///
|
||||
/// TODO: should we expose this to users of cuprated? probably not.
|
||||
@@ -27,19 +41,76 @@ const DANDELION_CONFIG: DandelionConfig = DandelionConfig {
|
||||
};
|
||||
|
||||
/// A [`DandelionRouter`] with all generic types defined.
|
||||
type ConcreteDandelionRouter = DandelionRouter<
|
||||
stem_service::OutboundPeerStream,
|
||||
diffuse_service::DiffuseService,
|
||||
pub(super) type ConcreteDandelionRouter<Z> = DandelionRouter<
|
||||
stem_service::OutboundPeerStream<Z>,
|
||||
DiffuseService<Z>,
|
||||
CrossNetworkInternalPeerId,
|
||||
stem_service::StemPeerService<ClearNet>,
|
||||
stem_service::StemPeerService<Z>,
|
||||
DandelionTx,
|
||||
>;
|
||||
|
||||
/// The dandelion router used to send transactions to the network.
|
||||
pub(super) struct MainDandelionRouter {
|
||||
clearnet_router: ConcreteDandelionRouter<ClearNet>,
|
||||
tor_router: Option<AnonTxService<Tor>>,
|
||||
}
|
||||
|
||||
impl MainDandelionRouter {
|
||||
pub const fn new(
|
||||
clearnet_router: ConcreteDandelionRouter<ClearNet>,
|
||||
tor_router: Option<AnonTxService<Tor>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
clearnet_router,
|
||||
tor_router,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Service<DandelionRouteReq<DandelionTx, CrossNetworkInternalPeerId>> for MainDandelionRouter {
|
||||
type Response = State;
|
||||
type Error = DandelionRouterError;
|
||||
type Future = BoxFuture<'static, Result<State, DandelionRouterError>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
if let Some(tor_router) = self.tor_router.as_mut() {
|
||||
ready!(tor_router.poll_ready(cx))?;
|
||||
}
|
||||
|
||||
self.clearnet_router.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(
|
||||
&mut self,
|
||||
req: DandelionRouteReq<DandelionTx, CrossNetworkInternalPeerId>,
|
||||
) -> Self::Future {
|
||||
// TODO: is this the best way to use anonymity networks?
|
||||
if req.state == TxState::Local {
|
||||
if let Some(tor_router) = self.tor_router.as_mut() {
|
||||
if let Some(mut peer) = tor_router.peer.take() {
|
||||
tracing::debug!("routing tx over Tor");
|
||||
return peer
|
||||
.call(StemRequest(req.tx))
|
||||
.map_ok(|_| State::Stem)
|
||||
.map_err(DandelionRouterError::PeerError)
|
||||
.boxed();
|
||||
}
|
||||
|
||||
tracing::warn!(
|
||||
"failed to route tx over Tor, no connections, falling back to Clearnet"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.clearnet_router.call(req)
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the dandelion pool manager task and returns a handle to send txs to broadcast.
|
||||
pub fn start_dandelion_pool_manager(
|
||||
router: ConcreteDandelionRouter,
|
||||
router: MainDandelionRouter,
|
||||
txpool_read_handle: TxpoolReadHandle,
|
||||
txpool_write_handle: TxpoolWriteHandle,
|
||||
promote_tx: mpsc::Sender<[u8; 32]>,
|
||||
) -> DandelionPoolService<DandelionTx, TxId, CrossNetworkInternalPeerId> {
|
||||
cuprate_dandelion_tower::pool::start_dandelion_pool_manager(
|
||||
// TODO: make this constant configurable?
|
||||
@@ -47,19 +118,24 @@ pub fn start_dandelion_pool_manager(
|
||||
router,
|
||||
tx_store::TxStoreService {
|
||||
txpool_read_handle,
|
||||
txpool_write_handle,
|
||||
promote_tx: PollSender::new(promote_tx),
|
||||
},
|
||||
DANDELION_CONFIG,
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a [`DandelionRouter`] from a [`NetworkInterface`].
|
||||
pub fn dandelion_router(clear_net: NetworkInterface<ClearNet>) -> ConcreteDandelionRouter {
|
||||
pub fn dandelion_router<Z: NetworkZone>(
|
||||
network_interface: NetworkInterface<Z>,
|
||||
) -> ConcreteDandelionRouter<Z>
|
||||
where
|
||||
InternalPeerID<Z::Addr>: Into<CrossNetworkInternalPeerId>,
|
||||
{
|
||||
DandelionRouter::new(
|
||||
diffuse_service::DiffuseService {
|
||||
clear_net_broadcast_service: clear_net.broadcast_svc(),
|
||||
DiffuseService {
|
||||
clear_net_broadcast_service: network_interface.broadcast_svc(),
|
||||
},
|
||||
stem_service::OutboundPeerStream::new(clear_net),
|
||||
stem_service::OutboundPeerStream::<Z>::new(network_interface),
|
||||
DANDELION_CONFIG,
|
||||
)
|
||||
}
|
||||
|
||||
68
binaries/cuprated/src/txpool/dandelion/anon_net_service.rs
Normal file
68
binaries/cuprated/src/txpool/dandelion/anon_net_service.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{ready, Context, Poll},
|
||||
};
|
||||
|
||||
use futures::{Stream, StreamExt, TryStream};
|
||||
use tower::Service;
|
||||
|
||||
use cuprate_dandelion_tower::{DandelionRouterError, OutboundPeer};
|
||||
use cuprate_p2p::NetworkInterface;
|
||||
use cuprate_p2p_core::{client::InternalPeerID, NetworkZone};
|
||||
|
||||
use crate::{
|
||||
p2p::CrossNetworkInternalPeerId,
|
||||
txpool::dandelion::stem_service::{OutboundPeerStream, StemPeerService},
|
||||
};
|
||||
|
||||
/// The service to prepare peers on anonymous network zones for sending transactions.
|
||||
pub struct AnonTxService<Z: NetworkZone> {
|
||||
outbound_peer_discover: Pin<Box<OutboundPeerStream<Z>>>,
|
||||
pub peer: Option<StemPeerService<Z>>,
|
||||
}
|
||||
|
||||
impl<Z: NetworkZone> AnonTxService<Z>
|
||||
where
|
||||
InternalPeerID<Z::Addr>: Into<CrossNetworkInternalPeerId>,
|
||||
{
|
||||
pub fn new(network_interface: NetworkInterface<Z>) -> Self {
|
||||
Self {
|
||||
outbound_peer_discover: Box::pin(OutboundPeerStream::new(network_interface)),
|
||||
peer: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), DandelionRouterError>> {
|
||||
loop {
|
||||
if let Some(peer) = &mut self.peer {
|
||||
if ready!(peer.poll_ready(cx)).is_err() {
|
||||
self.peer = None;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
let ret = ready!(self
|
||||
.outbound_peer_discover
|
||||
.as_mut()
|
||||
.try_poll_next(cx)
|
||||
.map_err(DandelionRouterError::OutboundPeerStreamError))
|
||||
.ok_or(DandelionRouterError::OutboundPeerDiscoverExited)??;
|
||||
|
||||
match ret {
|
||||
OutboundPeer::Peer(_, mut svc) => {
|
||||
let poll = svc.poll_ready(cx);
|
||||
self.peer = Some(svc);
|
||||
if ready!(poll).is_err() {
|
||||
self.peer = None;
|
||||
}
|
||||
}
|
||||
OutboundPeer::Exhausted => return Poll::Ready(Ok(())),
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,16 @@ use tower::Service;
|
||||
|
||||
use cuprate_dandelion_tower::traits::DiffuseRequest;
|
||||
use cuprate_p2p::{BroadcastRequest, BroadcastSvc};
|
||||
use cuprate_p2p_core::ClearNet;
|
||||
use cuprate_p2p_core::{ClearNet, NetworkZone};
|
||||
|
||||
use crate::txpool::dandelion::DandelionTx;
|
||||
|
||||
/// The dandelion diffusion service.
|
||||
pub struct DiffuseService {
|
||||
pub clear_net_broadcast_service: BroadcastSvc<ClearNet>,
|
||||
pub struct DiffuseService<N: NetworkZone> {
|
||||
pub clear_net_broadcast_service: BroadcastSvc<N>,
|
||||
}
|
||||
|
||||
impl Service<DiffuseRequest<DandelionTx>> for DiffuseService {
|
||||
impl<N: NetworkZone> Service<DiffuseRequest<DandelionTx>> for DiffuseService<N> {
|
||||
type Response = ();
|
||||
type Error = tower::BoxError;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
@@ -12,38 +12,39 @@ use cuprate_dandelion_tower::{traits::StemRequest, OutboundPeer};
|
||||
use cuprate_p2p::{ClientDropGuard, NetworkInterface, PeerSetRequest, PeerSetResponse};
|
||||
use cuprate_p2p_core::{
|
||||
client::{Client, InternalPeerID},
|
||||
BroadcastMessage, ClearNet, NetworkZone, PeerRequest, ProtocolRequest,
|
||||
BroadcastMessage, ClearNet, NetworkZone, PeerRequest, ProtocolRequest, Tor,
|
||||
};
|
||||
use cuprate_wire::protocol::NewTransactions;
|
||||
|
||||
use crate::{p2p::CrossNetworkInternalPeerId, txpool::dandelion::DandelionTx};
|
||||
|
||||
/// The dandelion outbound peer stream.
|
||||
pub struct OutboundPeerStream {
|
||||
clear_net: NetworkInterface<ClearNet>,
|
||||
state: OutboundPeerStreamState,
|
||||
pub struct OutboundPeerStream<Z: NetworkZone> {
|
||||
network_interface: NetworkInterface<Z>,
|
||||
state: OutboundPeerStreamState<Z>,
|
||||
}
|
||||
|
||||
impl OutboundPeerStream {
|
||||
pub const fn new(clear_net: NetworkInterface<ClearNet>) -> Self {
|
||||
impl<Z: NetworkZone> OutboundPeerStream<Z> {
|
||||
pub const fn new(network_interface: NetworkInterface<Z>) -> Self {
|
||||
Self {
|
||||
clear_net,
|
||||
network_interface,
|
||||
state: OutboundPeerStreamState::Standby,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for OutboundPeerStream {
|
||||
type Item = Result<
|
||||
OutboundPeer<CrossNetworkInternalPeerId, StemPeerService<ClearNet>>,
|
||||
tower::BoxError,
|
||||
>;
|
||||
impl<Z: NetworkZone> Stream for OutboundPeerStream<Z>
|
||||
where
|
||||
InternalPeerID<Z::Addr>: Into<CrossNetworkInternalPeerId>,
|
||||
{
|
||||
type Item =
|
||||
Result<OutboundPeer<CrossNetworkInternalPeerId, StemPeerService<Z>>, tower::BoxError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
match &mut self.state {
|
||||
OutboundPeerStreamState::Standby => {
|
||||
let peer_set = self.clear_net.peer_set();
|
||||
let peer_set = self.network_interface.peer_set();
|
||||
let res = ready!(peer_set.poll_ready(cx));
|
||||
|
||||
self.state = OutboundPeerStreamState::AwaitingPeer(
|
||||
@@ -61,10 +62,9 @@ impl Stream for OutboundPeerStream {
|
||||
};
|
||||
|
||||
match stem_peer {
|
||||
Some(peer) => OutboundPeer::Peer(
|
||||
CrossNetworkInternalPeerId::ClearNet(peer.info.id),
|
||||
StemPeerService(peer),
|
||||
),
|
||||
Some(peer) => {
|
||||
OutboundPeer::Peer(peer.info.id.into(), StemPeerService(peer))
|
||||
}
|
||||
None => OutboundPeer::Exhausted,
|
||||
}
|
||||
})));
|
||||
@@ -75,11 +75,11 @@ impl Stream for OutboundPeerStream {
|
||||
}
|
||||
|
||||
/// The state of the [`OutboundPeerStream`].
|
||||
enum OutboundPeerStreamState {
|
||||
enum OutboundPeerStreamState<Z: NetworkZone> {
|
||||
/// Standby state.
|
||||
Standby,
|
||||
/// Awaiting a response from the peer-set.
|
||||
AwaitingPeer(BoxFuture<'static, Result<PeerSetResponse<ClearNet>, tower::BoxError>>),
|
||||
AwaitingPeer(BoxFuture<'static, Result<PeerSetResponse<Z>, tower::BoxError>>),
|
||||
}
|
||||
|
||||
/// The stem service, used to send stem txs.
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
future::ready,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::PollSender;
|
||||
use tower::{Service, ServiceExt};
|
||||
|
||||
use cuprate_dandelion_tower::{
|
||||
@@ -21,7 +26,7 @@ use super::{DandelionTx, TxId};
|
||||
/// This is just mapping the interface [`cuprate_dandelion_tower`] wants to what [`cuprate_txpool`] provides.
|
||||
pub struct TxStoreService {
|
||||
pub txpool_read_handle: TxpoolReadHandle,
|
||||
pub txpool_write_handle: TxpoolWriteHandle,
|
||||
pub promote_tx: PollSender<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl Service<TxStoreRequest<TxId>> for TxStoreService {
|
||||
@@ -29,8 +34,8 @@ impl Service<TxStoreRequest<TxId>> for TxStoreService {
|
||||
type Error = tower::BoxError;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.promote_tx.poll_reserve(cx).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: TxStoreRequest<TxId>) -> Self::Future {
|
||||
@@ -60,15 +65,13 @@ impl Service<TxStoreRequest<TxId>> for TxStoreService {
|
||||
Ok(_) => unreachable!(),
|
||||
})
|
||||
.boxed(),
|
||||
TxStoreRequest::Promote(tx_id) => self
|
||||
.txpool_write_handle
|
||||
.clone()
|
||||
.oneshot(TxpoolWriteRequest::Promote(tx_id))
|
||||
.map(|res| match res {
|
||||
Ok(_) | Err(RuntimeError::KeyNotFound) => Ok(TxStoreResponse::Ok),
|
||||
Err(e) => Err(e.into()),
|
||||
})
|
||||
.boxed(),
|
||||
TxStoreRequest::Promote(tx_id) => ready(
|
||||
self.promote_tx
|
||||
.send_item(tx_id)
|
||||
.map_err(Into::into)
|
||||
.map(|()| TxStoreResponse::Ok),
|
||||
)
|
||||
.boxed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,15 @@ use std::{
|
||||
use bytes::Bytes;
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use monero_serai::transaction::Transaction;
|
||||
use tokio::sync::mpsc;
|
||||
use tower::{BoxError, Service, ServiceExt};
|
||||
use tracing::instrument;
|
||||
|
||||
use cuprate_blockchain::service::BlockchainReadHandle;
|
||||
use cuprate_consensus::transactions::{start_tx_verification, PrepTransactions};
|
||||
use cuprate_consensus::{
|
||||
transactions::new_tx_verification_data, BlockChainContextRequest, BlockChainContextResponse,
|
||||
BlockchainContextService, ExtendedConsensusError,
|
||||
transactions::{new_tx_verification_data, start_tx_verification, PrepTransactions},
|
||||
BlockChainContextRequest, BlockChainContextResponse, BlockchainContextService,
|
||||
ExtendedConsensusError,
|
||||
};
|
||||
use cuprate_dandelion_tower::{
|
||||
pool::{DandelionPoolService, IncomingTxBuilder},
|
||||
@@ -21,7 +23,7 @@ use cuprate_dandelion_tower::{
|
||||
};
|
||||
use cuprate_helper::asynch::rayon_spawn_async;
|
||||
use cuprate_p2p::NetworkInterface;
|
||||
use cuprate_p2p_core::ClearNet;
|
||||
use cuprate_p2p_core::{ClearNet, Tor};
|
||||
use cuprate_txpool::{
|
||||
service::{
|
||||
interface::{
|
||||
@@ -35,12 +37,16 @@ use cuprate_types::TransactionVerificationData;
|
||||
|
||||
use crate::{
|
||||
blockchain::ConsensusBlockchainReadHandle,
|
||||
config::TxpoolConfig,
|
||||
constants::PANIC_CRITICAL_SERVICE_ERROR,
|
||||
p2p::CrossNetworkInternalPeerId,
|
||||
signals::REORG_LOCK,
|
||||
txpool::{
|
||||
dandelion,
|
||||
relay_rules::check_tx_relay_rules,
|
||||
dandelion::{
|
||||
self, AnonTxService, ConcreteDandelionRouter, DiffuseService, MainDandelionRouter,
|
||||
},
|
||||
manager::{start_txpool_manager, TxpoolManagerHandle},
|
||||
relay_rules::{check_tx_relay_rules, RelayRuleError},
|
||||
txs_being_handled::{TxsBeingHandled, TxsBeingHandledLocally},
|
||||
},
|
||||
};
|
||||
@@ -54,6 +60,8 @@ pub enum IncomingTxError {
|
||||
Consensus(ExtendedConsensusError),
|
||||
#[error("Duplicate tx in message")]
|
||||
DuplicateTransaction,
|
||||
#[error("Relay rule was broken: {0}")]
|
||||
RelayRule(RelayRuleError),
|
||||
}
|
||||
|
||||
/// Incoming transactions.
|
||||
@@ -62,6 +70,13 @@ pub struct IncomingTxs {
|
||||
pub txs: Vec<Bytes>,
|
||||
/// The routing state of the transactions.
|
||||
pub state: TxState<CrossNetworkInternalPeerId>,
|
||||
/// If [`true`], transactions breaking relay
|
||||
/// rules will be ignored and processing will continue,
|
||||
/// otherwise the service will return an early error.
|
||||
pub drop_relay_rule_errors: bool,
|
||||
/// If [`true`], only checks will be done,
|
||||
/// the transaction will not be relayed.
|
||||
pub do_not_relay: bool,
|
||||
}
|
||||
|
||||
/// The transaction type used for dandelion++.
|
||||
@@ -83,8 +98,7 @@ pub struct IncomingTxHandler {
|
||||
/// The dandelion txpool manager.
|
||||
pub(super) dandelion_pool_manager:
|
||||
DandelionPoolService<DandelionTx, TxId, CrossNetworkInternalPeerId>,
|
||||
/// The txpool write handle.
|
||||
pub(super) txpool_write_handle: TxpoolWriteHandle,
|
||||
pub txpool_manager: TxpoolManagerHandle,
|
||||
/// The txpool read handle.
|
||||
pub(super) txpool_read_handle: TxpoolReadHandle,
|
||||
/// The blockchain read handle.
|
||||
@@ -94,26 +108,47 @@ pub struct IncomingTxHandler {
|
||||
impl IncomingTxHandler {
|
||||
/// Initialize the [`IncomingTxHandler`].
|
||||
#[expect(clippy::significant_drop_tightening)]
|
||||
pub fn init(
|
||||
#[instrument(level = "info", skip_all, name = "start_txpool")]
|
||||
pub async fn init(
|
||||
txpool_config: TxpoolConfig,
|
||||
clear_net: NetworkInterface<ClearNet>,
|
||||
tor_net: Option<NetworkInterface<Tor>>,
|
||||
txpool_write_handle: TxpoolWriteHandle,
|
||||
txpool_read_handle: TxpoolReadHandle,
|
||||
blockchain_context_cache: BlockchainContextService,
|
||||
blockchain_read_handle: BlockchainReadHandle,
|
||||
) -> Self {
|
||||
let dandelion_router = dandelion::dandelion_router(clear_net);
|
||||
let diffuse_service = DiffuseService {
|
||||
clear_net_broadcast_service: clear_net.broadcast_svc(),
|
||||
};
|
||||
let clearnet_router = dandelion::dandelion_router(clear_net);
|
||||
let tor_router = tor_net.map(AnonTxService::new);
|
||||
|
||||
let dandelion_router = MainDandelionRouter::new(clearnet_router, tor_router);
|
||||
|
||||
let (promote_tx, promote_rx) = mpsc::channel(25);
|
||||
|
||||
let dandelion_pool_manager = dandelion::start_dandelion_pool_manager(
|
||||
dandelion_router,
|
||||
txpool_read_handle.clone(),
|
||||
txpool_write_handle.clone(),
|
||||
promote_tx,
|
||||
);
|
||||
|
||||
let txpool_manager = start_txpool_manager(
|
||||
txpool_write_handle,
|
||||
txpool_read_handle.clone(),
|
||||
promote_rx,
|
||||
diffuse_service,
|
||||
dandelion_pool_manager.clone(),
|
||||
txpool_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
Self {
|
||||
txs_being_handled: TxsBeingHandled::new(),
|
||||
blockchain_context_cache,
|
||||
dandelion_pool_manager,
|
||||
txpool_write_handle,
|
||||
txpool_manager,
|
||||
txpool_read_handle,
|
||||
blockchain_read_handle: ConsensusBlockchainReadHandle::new(
|
||||
blockchain_read_handle,
|
||||
@@ -138,8 +173,8 @@ impl Service<IncomingTxs> for IncomingTxHandler {
|
||||
self.txs_being_handled.clone(),
|
||||
self.blockchain_context_cache.clone(),
|
||||
self.blockchain_read_handle.clone(),
|
||||
self.txpool_write_handle.clone(),
|
||||
self.txpool_read_handle.clone(),
|
||||
self.txpool_manager.clone(),
|
||||
self.dandelion_pool_manager.clone(),
|
||||
)
|
||||
.boxed()
|
||||
@@ -148,12 +183,17 @@ impl Service<IncomingTxs> for IncomingTxHandler {
|
||||
|
||||
/// Handles the incoming txs.
|
||||
async fn handle_incoming_txs(
|
||||
IncomingTxs { txs, state }: IncomingTxs,
|
||||
IncomingTxs {
|
||||
txs,
|
||||
state,
|
||||
drop_relay_rule_errors,
|
||||
do_not_relay,
|
||||
}: IncomingTxs,
|
||||
txs_being_handled: TxsBeingHandled,
|
||||
mut blockchain_context_cache: BlockchainContextService,
|
||||
blockchain_read_handle: ConsensusBlockchainReadHandle,
|
||||
mut txpool_write_handle: TxpoolWriteHandle,
|
||||
mut txpool_read_handle: TxpoolReadHandle,
|
||||
mut txpool_manager_handle: TxpoolManagerHandle,
|
||||
mut dandelion_pool_manager: DandelionPoolService<DandelionTx, TxId, CrossNetworkInternalPeerId>,
|
||||
) -> Result<(), IncomingTxError> {
|
||||
let _reorg_guard = REORG_LOCK.read().await;
|
||||
@@ -183,18 +223,30 @@ async fn handle_incoming_txs(
|
||||
// TODO: this could be a DoS, if someone spams us with txs that violate these rules?
|
||||
// Maybe we should remember these invalid txs for some time to prevent them getting repeatedly sent.
|
||||
if let Err(e) = check_tx_relay_rules(&tx, context) {
|
||||
tracing::debug!(err = %e, tx = hex::encode(tx.tx_hash), "Tx failed relay check, skipping.");
|
||||
if drop_relay_rule_errors {
|
||||
tracing::debug!(err = %e, tx = hex::encode(tx.tx_hash), "Tx failed relay check, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
return Err(IncomingTxError::RelayRule(e));
|
||||
}
|
||||
|
||||
handle_valid_tx(
|
||||
tx,
|
||||
state.clone(),
|
||||
&mut txpool_write_handle,
|
||||
&mut dandelion_pool_manager,
|
||||
)
|
||||
.await;
|
||||
tracing::debug!(
|
||||
tx = hex::encode(tx.tx_hash),
|
||||
"passing tx to tx-pool manager"
|
||||
);
|
||||
|
||||
// TODO: take into account `do_not_relay` in the tx-pool manager.
|
||||
|
||||
if txpool_manager_handle
|
||||
.tx_tx
|
||||
.send((tx, state.clone()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!("The txpool manager has been stopped, dropping incoming txs");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Re-relay any txs we got in the block that were already in our stem pool.
|
||||
@@ -242,6 +294,7 @@ async fn prepare_incoming_txs(
|
||||
|
||||
// If a duplicate is in here the incoming tx batch contained the same tx twice.
|
||||
if !tx_blob_hashes.insert(tx_blob_hash) {
|
||||
tracing::debug!("peer sent duplicate tx in batch, ignoring batch.");
|
||||
return Some(Err(IncomingTxError::DuplicateTransaction));
|
||||
}
|
||||
|
||||
@@ -296,58 +349,6 @@ async fn prepare_incoming_txs(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Handle a verified tx.
|
||||
///
|
||||
/// This will add the tx to the txpool and route it to the network.
|
||||
async fn handle_valid_tx(
|
||||
tx: TransactionVerificationData,
|
||||
state: TxState<CrossNetworkInternalPeerId>,
|
||||
txpool_write_handle: &mut TxpoolWriteHandle,
|
||||
dandelion_pool_manager: &mut DandelionPoolService<
|
||||
DandelionTx,
|
||||
TxId,
|
||||
CrossNetworkInternalPeerId,
|
||||
>,
|
||||
) {
|
||||
let incoming_tx =
|
||||
IncomingTxBuilder::new(DandelionTx(Bytes::copy_from_slice(&tx.tx_blob)), tx.tx_hash);
|
||||
|
||||
let TxpoolWriteResponse::AddTransaction(double_spend) = txpool_write_handle
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(TxpoolWriteRequest::AddTransaction {
|
||||
tx: Box::new(tx),
|
||||
state_stem: state.is_stem_stage(),
|
||||
})
|
||||
.await
|
||||
.expect("TODO")
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// TODO: track double spends to quickly ignore them from their blob hash.
|
||||
if let Some(tx_hash) = double_spend {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: There is a race condition possible if a tx and block come in at the same time: <https://github.com/Cuprate/cuprate/issues/314>.
|
||||
|
||||
let incoming_tx = incoming_tx
|
||||
.with_routing_state(state)
|
||||
.with_state_in_db(None)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
dandelion_pool_manager
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(incoming_tx)
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR);
|
||||
}
|
||||
|
||||
/// Re-relay a tx that was already in our stem pool.
|
||||
async fn rerelay_stem_tx(
|
||||
tx_hash: &TxId,
|
||||
|
||||
497
binaries/cuprated/src/txpool/manager.rs
Normal file
497
binaries/cuprated/src/txpool/manager.rs
Normal file
@@ -0,0 +1,497 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
use indexmap::IndexMap;
|
||||
use rand::Rng;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio_util::{time::delay_queue, time::DelayQueue};
|
||||
use tower::{Service, ServiceExt};
|
||||
use tracing::{instrument, Instrument, Span};
|
||||
|
||||
use cuprate_dandelion_tower::{
|
||||
pool::{DandelionPoolService, IncomingTx, IncomingTxBuilder},
|
||||
traits::DiffuseRequest,
|
||||
TxState,
|
||||
};
|
||||
use cuprate_helper::time::current_unix_timestamp;
|
||||
use cuprate_p2p_core::ClearNet;
|
||||
use cuprate_txpool::service::{
|
||||
interface::{TxpoolReadRequest, TxpoolReadResponse, TxpoolWriteRequest, TxpoolWriteResponse},
|
||||
TxpoolReadHandle, TxpoolWriteHandle,
|
||||
};
|
||||
use cuprate_types::TransactionVerificationData;
|
||||
|
||||
use crate::{
|
||||
config::TxpoolConfig,
|
||||
constants::PANIC_CRITICAL_SERVICE_ERROR,
|
||||
p2p::{CrossNetworkInternalPeerId, NetworkInterfaces},
|
||||
txpool::{
|
||||
dandelion::DiffuseService,
|
||||
incoming_tx::{DandelionTx, TxId},
|
||||
},
|
||||
};
|
||||
|
||||
const INCOMING_TX_QUEUE_SIZE: usize = 100;
|
||||
|
||||
/// Starts the transaction pool manager service.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function may panic if any inner service has an unrecoverable error.
|
||||
pub async fn start_txpool_manager(
|
||||
mut txpool_write_handle: TxpoolWriteHandle,
|
||||
mut txpool_read_handle: TxpoolReadHandle,
|
||||
promote_tx_channel: mpsc::Receiver<[u8; 32]>,
|
||||
diffuse_service: DiffuseService<ClearNet>,
|
||||
dandelion_pool_manager: DandelionPoolService<DandelionTx, TxId, CrossNetworkInternalPeerId>,
|
||||
config: TxpoolConfig,
|
||||
) -> TxpoolManagerHandle {
|
||||
let TxpoolReadResponse::Backlog(backlog) = txpool_read_handle
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(TxpoolReadRequest::Backlog)
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
tracing::info!(txs_in_pool = backlog.len(), "starting txpool manager");
|
||||
|
||||
let mut stem_txs = Vec::new();
|
||||
|
||||
let mut tx_timeouts = DelayQueue::with_capacity(backlog.len());
|
||||
let current_txs = backlog
|
||||
.into_iter()
|
||||
.map(|tx| {
|
||||
let timeout_key = if tx.private {
|
||||
stem_txs.push(tx.id);
|
||||
None
|
||||
} else {
|
||||
let next_timeout = calculate_next_timeout(tx.received_at, config.maximum_age_secs);
|
||||
Some(tx_timeouts.insert(tx.id, Duration::from_secs(next_timeout)))
|
||||
};
|
||||
|
||||
(
|
||||
tx.id,
|
||||
TxInfo {
|
||||
weight: tx.weight,
|
||||
fee: tx.fee,
|
||||
received_at: tx.received_at,
|
||||
private: tx.private,
|
||||
timeout_key,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut manager = TxpoolManager {
|
||||
current_txs,
|
||||
tx_timeouts,
|
||||
txpool_write_handle,
|
||||
txpool_read_handle,
|
||||
dandelion_pool_manager,
|
||||
promote_tx_channel,
|
||||
diffuse_service,
|
||||
config,
|
||||
};
|
||||
|
||||
tracing::info!(stem_txs = stem_txs.len(), "promoting stem txs");
|
||||
|
||||
for tx in stem_txs {
|
||||
manager.promote_tx(tx).await;
|
||||
}
|
||||
|
||||
let (tx_tx, tx_rx) = mpsc::channel(INCOMING_TX_QUEUE_SIZE);
|
||||
let (spent_kis_tx, spent_kis_rx) = mpsc::channel(1);
|
||||
|
||||
tokio::spawn(manager.run(tx_rx, spent_kis_rx));
|
||||
|
||||
TxpoolManagerHandle {
|
||||
tx_tx,
|
||||
spent_kis_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to the tx-pool manager.
|
||||
#[derive(Clone)]
|
||||
pub struct TxpoolManagerHandle {
|
||||
/// The incoming tx channel.
|
||||
pub tx_tx: mpsc::Sender<(
|
||||
TransactionVerificationData,
|
||||
TxState<CrossNetworkInternalPeerId>,
|
||||
)>,
|
||||
|
||||
/// The spent key images in a new block tx.
|
||||
spent_kis_tx: mpsc::Sender<(Vec<[u8; 32]>, oneshot::Sender<()>)>,
|
||||
}
|
||||
|
||||
impl TxpoolManagerHandle {
|
||||
/// Create a mock [`TxpoolManagerHandle`] that does nothing.
|
||||
///
|
||||
/// Useful for testing.
|
||||
#[expect(clippy::let_underscore_must_use)]
|
||||
pub fn mock() -> Self {
|
||||
let (spent_kis_tx, mut spent_kis_rx) = mpsc::channel(1);
|
||||
let (tx_tx, mut tx_rx) = mpsc::channel(100);
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let Some(rec): Option<(_, oneshot::Sender<()>)> = spent_kis_rx.recv().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = rec.1.send(());
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if tx_rx.recv().await.is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
tx_tx,
|
||||
spent_kis_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tell the tx-pool about spent key images in an incoming block.
|
||||
pub async fn new_block(&mut self, spent_key_images: Vec<[u8; 32]>) -> anyhow::Result<()> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
drop(self.spent_kis_tx.send((spent_key_images, tx)).await);
|
||||
|
||||
rx.await
|
||||
.map_err(|_| anyhow::anyhow!("txpool manager stopped"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Information on a transaction in the tx-pool.
|
||||
struct TxInfo {
|
||||
/// The weight of the transaction.
|
||||
weight: usize,
|
||||
/// The fee the transaction paid.
|
||||
fee: u64,
|
||||
/// The UNIX timestamp when the tx was received.
|
||||
received_at: u64,
|
||||
/// Whether the tx is in the private pool.
|
||||
private: bool,
|
||||
|
||||
/// The [`delay_queue::Key`] for the timeout queue in the manager.
|
||||
///
|
||||
/// This will be [`None`] if the tx is private as timeouts for them are handled in the dandelion pool.
|
||||
timeout_key: Option<delay_queue::Key>,
|
||||
}
|
||||
|
||||
struct TxpoolManager {
|
||||
current_txs: IndexMap<[u8; 32], TxInfo>,
|
||||
|
||||
/// A [`DelayQueue`] for waiting on tx timeouts.
|
||||
///
|
||||
/// Timeouts can be for re-relaying or removal from the pool.
|
||||
tx_timeouts: DelayQueue<[u8; 32]>,
|
||||
|
||||
txpool_write_handle: TxpoolWriteHandle,
|
||||
txpool_read_handle: TxpoolReadHandle,
|
||||
|
||||
dandelion_pool_manager: DandelionPoolService<DandelionTx, TxId, CrossNetworkInternalPeerId>,
|
||||
/// The channel the dandelion manager will use to communicate that a tx should be promoted to the
|
||||
/// public pool.
|
||||
promote_tx_channel: mpsc::Receiver<[u8; 32]>,
|
||||
/// The [`DiffuseService`] to diffuse txs to the p2p network.
|
||||
///
|
||||
/// Used for re-relays.
|
||||
diffuse_service: DiffuseService<ClearNet>,
|
||||
|
||||
config: TxpoolConfig,
|
||||
}
|
||||
|
||||
impl TxpoolManager {
|
||||
/// Removes a transaction from the tx-pool manager, and optionally the database too.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if the tx is not in the tx-pool manager.
|
||||
#[instrument(level = "debug", skip_all, fields(tx_id = hex::encode(tx)))]
|
||||
async fn remove_tx_from_pool(&mut self, tx: [u8; 32], remove_from_db: bool) {
|
||||
tracing::debug!("removing tx from pool");
|
||||
|
||||
let tx_info = self.current_txs.swap_remove(&tx).unwrap();
|
||||
|
||||
tx_info
|
||||
.timeout_key
|
||||
.and_then(|key| self.tx_timeouts.try_remove(&key));
|
||||
|
||||
if remove_from_db {
|
||||
self.txpool_write_handle
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(TxpoolWriteRequest::RemoveTransaction(tx))
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-relay a tx to the network.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if the tx is not in the tx-pool.
|
||||
#[instrument(level = "debug", skip_all, fields(tx_id = hex::encode(tx)))]
|
||||
async fn rerelay_tx(&mut self, tx: [u8; 32]) {
|
||||
tracing::debug!("re-relaying tx to network");
|
||||
|
||||
let TxpoolReadResponse::TxBlob {
|
||||
tx_blob,
|
||||
state_stem: _,
|
||||
} = self
|
||||
.txpool_read_handle
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(TxpoolReadRequest::TxBlob(tx))
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.diffuse_service
|
||||
.call(DiffuseRequest(DandelionTx(Bytes::from(tx_blob))))
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR);
|
||||
}
|
||||
|
||||
/// Handles a transaction timeout, be either rebroadcasting or dropping the tx from the pool.
|
||||
/// If a rebroadcast happens, this function will handle adding another timeout to the queue.
|
||||
#[instrument(level = "debug", skip_all, fields(tx_id = hex::encode(tx)))]
|
||||
async fn handle_tx_timeout(&mut self, tx: [u8; 32]) {
|
||||
let Some(tx_info) = self.current_txs.get(&tx) else {
|
||||
tracing::warn!("tx timed out, but tx not in pool");
|
||||
return;
|
||||
};
|
||||
|
||||
let time_in_pool = current_unix_timestamp() - tx_info.received_at;
|
||||
|
||||
// Check if the tx has timed out, with a small buffer to prevent rebroadcasting if the time is
|
||||
// slightly off.
|
||||
if time_in_pool + 10 > self.config.maximum_age_secs {
|
||||
tracing::warn!("tx has been in pool too long, removing from pool");
|
||||
self.remove_tx_from_pool(tx, true).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let received_at = tx_info.received_at;
|
||||
|
||||
tracing::debug!(time_in_pool, "tx timed out, resending to network");
|
||||
|
||||
self.rerelay_tx(tx).await;
|
||||
|
||||
let tx_info = self.current_txs.get_mut(&tx).unwrap();
|
||||
|
||||
let next_timeout = calculate_next_timeout(received_at, self.config.maximum_age_secs);
|
||||
tracing::trace!(in_secs = next_timeout, "setting next tx timeout");
|
||||
|
||||
tx_info.timeout_key = Some(
|
||||
self.tx_timeouts
|
||||
.insert(tx, Duration::from_secs(next_timeout)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a tx to the tx-pool manager.
|
||||
#[instrument(level = "trace", skip_all, fields(tx_id = hex::encode(tx)))]
|
||||
fn track_tx(&mut self, tx: [u8; 32], weight: usize, fee: u64, private: bool) {
|
||||
let now = current_unix_timestamp();
|
||||
|
||||
let timeout_key = if private {
|
||||
// The dandelion pool handles stem tx embargo.
|
||||
None
|
||||
} else {
|
||||
let timeout = calculate_next_timeout(now, self.config.maximum_age_secs);
|
||||
|
||||
tracing::trace!(in_secs = timeout, "setting next tx timeout");
|
||||
|
||||
Some(self.tx_timeouts.insert(tx, Duration::from_secs(timeout)))
|
||||
};
|
||||
|
||||
self.current_txs.insert(
|
||||
tx,
|
||||
TxInfo {
|
||||
weight,
|
||||
fee,
|
||||
received_at: now,
|
||||
private,
|
||||
timeout_key,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles an incoming tx, adding it to the pool and routing it.
|
||||
#[instrument(level = "debug", skip_all, fields(tx_id = hex::encode(tx.tx_hash), state))]
|
||||
async fn handle_incoming_tx(
|
||||
&mut self,
|
||||
tx: TransactionVerificationData,
|
||||
state: TxState<CrossNetworkInternalPeerId>,
|
||||
) {
|
||||
tracing::debug!("handling new tx");
|
||||
|
||||
let incoming_tx =
|
||||
IncomingTxBuilder::new(DandelionTx(Bytes::copy_from_slice(&tx.tx_blob)), tx.tx_hash);
|
||||
|
||||
let (tx_hash, tx_weight, tx_fee) = (tx.tx_hash, tx.tx_weight, tx.fee);
|
||||
|
||||
let TxpoolWriteResponse::AddTransaction(double_spend) = self
|
||||
.txpool_write_handle
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(TxpoolWriteRequest::AddTransaction {
|
||||
tx: Box::new(tx),
|
||||
state_stem: state.is_stem_stage(),
|
||||
})
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
if let Some(tx_hash) = double_spend {
|
||||
tracing::debug!(
|
||||
double_spent = hex::encode(tx_hash),
|
||||
"transaction is a double spend, ignoring"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
self.track_tx(tx_hash, tx_weight, tx_fee, state.is_stem_stage());
|
||||
|
||||
let incoming_tx = incoming_tx
|
||||
.with_routing_state(state)
|
||||
.with_state_in_db(None)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
self.dandelion_pool_manager
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(incoming_tx)
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR);
|
||||
}
|
||||
|
||||
/// Promote a tx to the public pool.
|
||||
#[instrument(level = "debug", skip_all, fields(tx_id = hex::encode(tx)))]
|
||||
async fn promote_tx(&mut self, tx: [u8; 32]) {
|
||||
let Some(tx_info) = self.current_txs.get_mut(&tx) else {
|
||||
tracing::debug!("not promoting tx, tx not in pool");
|
||||
return;
|
||||
};
|
||||
|
||||
if !tx_info.private {
|
||||
tracing::trace!("not promoting tx, tx is already public");
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::debug!("promoting tx");
|
||||
|
||||
// It's now in the public pool, pretend we just saw it.
|
||||
tx_info.received_at = current_unix_timestamp();
|
||||
|
||||
let next_timeout =
|
||||
calculate_next_timeout(tx_info.received_at, self.config.maximum_age_secs);
|
||||
tracing::trace!(in_secs = next_timeout, "setting next tx timeout");
|
||||
tx_info.timeout_key = Some(
|
||||
self.tx_timeouts
|
||||
.insert(tx, Duration::from_secs(next_timeout)),
|
||||
);
|
||||
|
||||
self.txpool_write_handle
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(TxpoolWriteRequest::Promote(tx))
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR);
|
||||
}
|
||||
|
||||
/// Handles removing all transactions that have been included/double spent in an incoming block.
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
async fn new_block(&mut self, spent_key_images: Vec<[u8; 32]>) {
|
||||
tracing::debug!("handling new block");
|
||||
|
||||
let TxpoolWriteResponse::NewBlock(removed_txs) = self
|
||||
.txpool_write_handle
|
||||
.ready()
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
.call(TxpoolWriteRequest::NewBlock { spent_key_images })
|
||||
.await
|
||||
.expect(PANIC_CRITICAL_SERVICE_ERROR)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
for tx in removed_txs {
|
||||
self.remove_tx_from_pool(tx, false).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::let_underscore_must_use)]
|
||||
async fn run(
|
||||
mut self,
|
||||
mut tx_rx: mpsc::Receiver<(
|
||||
TransactionVerificationData,
|
||||
TxState<CrossNetworkInternalPeerId>,
|
||||
)>,
|
||||
mut block_rx: mpsc::Receiver<(Vec<[u8; 32]>, oneshot::Sender<()>)>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(tx) = self.tx_timeouts.next() => {
|
||||
self.handle_tx_timeout(tx.into_inner()).await;
|
||||
}
|
||||
Some((tx, state)) = tx_rx.recv() => {
|
||||
self.handle_incoming_tx(tx, state).await;
|
||||
}
|
||||
Some(tx) = self.promote_tx_channel.recv() => {
|
||||
self.promote_tx(tx).await;
|
||||
}
|
||||
Some((spent_kis, tx)) = block_rx.recv() => {
|
||||
self.new_block(spent_kis).await;
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the amount of time to wait before resending a tx to the network.
|
||||
fn calculate_next_timeout(received_at: u64, max_time_in_pool: u64) -> u64 {
|
||||
/// The base time between re-relays to the p2p network.
|
||||
const TX_RERELAY_TIME: u64 = 300;
|
||||
|
||||
/*
|
||||
This is a simple exponential backoff.
|
||||
The first timeout is TX_RERELAY_TIME seconds, the second is 2 * TX_RERELAY_TIME seconds, then 4, 8, 16, etc.
|
||||
*/
|
||||
let now = current_unix_timestamp();
|
||||
|
||||
let time_in_pool = now - received_at;
|
||||
|
||||
let time_till_max_timeout = max_time_in_pool.saturating_sub(time_in_pool);
|
||||
|
||||
let timeouts = time_in_pool / TX_RERELAY_TIME;
|
||||
|
||||
min((timeouts + 1) * TX_RERELAY_TIME, time_till_max_timeout)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ The currently implemented backends are:
|
||||
- [`heed`](https://github.com/meilisearch/heed) (LMDB)
|
||||
- [`redb`](https://github.com/cberner/redb)
|
||||
|
||||
Said precicely, `cuprate_database` is the embedded database other Cuprate
|
||||
Said precisely, `cuprate_database` is the embedded database other Cuprate
|
||||
crates interact with instead of using any particular backend implementation.
|
||||
This allows the backend to be swapped and/or future backends to be implemented.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["hinto-janai"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Cuprate User Book - v0.0.4"
|
||||
title = "Cuprate User Book - v0.0.5"
|
||||
git-repository-url = "https://github.com/Cuprate/cuprate/books/user"
|
||||
|
||||
[output.html]
|
||||
|
||||
@@ -3,10 +3,10 @@ Cuprate offers pre-built binaries for `cuprated` for the platforms listed in [`P
|
||||
|
||||
| Platform | Download |
|
||||
|------------------------------|----------|
|
||||
| Windows x86_64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.4/cuprated-0.0.4-x86_64-pc-windows-msvc.zip>
|
||||
| macOS x86_64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.4/cuprated-0.0.4-x86_64-apple-darwin.tar.gz>
|
||||
| macOS ARM64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.4/cuprated-0.0.4-aarch64-apple-darwin.tar.gz>
|
||||
| Linux x86_64 (glibc >= 2.36) | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.4/cuprated-0.0.4-x86_64-unknown-linux.tar.gz>
|
||||
| Linux ARM64 (glibc >= 2.36) | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.4/cuprated-0.0.4-aarch64-unknown-linux.tar.gz>
|
||||
| Windows x86_64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-x86_64-pc-windows-msvc.zip>
|
||||
| macOS x86_64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-x86_64-apple-darwin.tar.gz>
|
||||
| macOS ARM64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-aarch64-apple-darwin.tar.gz>
|
||||
| Linux x86_64 (glibc >= 2.36) | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-x86_64-unknown-linux.tar.gz>
|
||||
| Linux ARM64 (glibc >= 2.36) | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-aarch64-unknown-linux.tar.gz>
|
||||
|
||||
All release files are archived and also available at <https://archive.hinto.rs>.
|
||||
|
||||
@@ -60,7 +60,7 @@ This section contains the development status of endpoints/methods in `cuprated`.
|
||||
| `prune_blockchain` | ⚫ |
|
||||
| `relay_tx` | ⚪ |
|
||||
| `set_bans` | ⚪ |
|
||||
| `submit_block` | ⚪ |
|
||||
| `submit_block` | 🟠 |
|
||||
| `sync_info` | ⚪ |
|
||||
|
||||
## JSON endpoints
|
||||
@@ -83,7 +83,7 @@ This section contains the development status of endpoints/methods in `cuprated`.
|
||||
| `/out_peers` | ⚪ |
|
||||
| `/pop_blocks` | ⚪ |
|
||||
| `/save_bc` | ⚪ |
|
||||
| `/send_raw_transaction` | ⚪ |
|
||||
| `/send_raw_transaction` | 🟠 |
|
||||
| `/set_bootstrap_daemon` | ⚪ | Requires bootstrap implementation
|
||||
| `/set_limit` | ⚪ |
|
||||
| `/set_log_categories` | ⚪ | Could be re-purposed to use `tracing` filters
|
||||
|
||||
@@ -303,7 +303,7 @@ mod tests {
|
||||
|
||||
let entry = ChainEntry {
|
||||
ids,
|
||||
peer: InternalPeerID::Unknown(1),
|
||||
peer: InternalPeerID::Unknown([1; 16]),
|
||||
handle: handle.1
|
||||
};
|
||||
|
||||
@@ -335,7 +335,7 @@ mod tests {
|
||||
let entries = (0..len).map(|i| {
|
||||
ChainEntry {
|
||||
ids: vec![HASHES.get(i).copied().unwrap_or_default()],
|
||||
peer: InternalPeerID::Unknown(1),
|
||||
peer: InternalPeerID::Unknown([1; 16]),
|
||||
handle: handle.1.clone()
|
||||
}
|
||||
}).collect();
|
||||
@@ -369,7 +369,7 @@ mod tests {
|
||||
let handle = HandleBuilder::new().build();
|
||||
let entry = ChainEntry {
|
||||
ids: HASHES[hashes_start_height..(hashes_start_height + len)].to_vec(),
|
||||
peer: InternalPeerID::Unknown(1),
|
||||
peer: InternalPeerID::Unknown([1; 16]),
|
||||
handle: handle.1
|
||||
};
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ const fn check_time_lock(time_lock: &Timelock, chain_height: usize) -> Result<()
|
||||
Err(MinerTxError::InvalidLockTime)
|
||||
}
|
||||
}
|
||||
_ => Err(MinerTxError::InvalidLockTime),
|
||||
Timelock::None | Timelock::Time(_) => Err(MinerTxError::InvalidLockTime),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use curve25519_dalek::EdwardsPoint;
|
||||
use monero_serai::{
|
||||
io::decompress_point,
|
||||
@@ -400,9 +398,8 @@ fn check_inputs_sorted(inputs: &[Input], hf: HardFork) -> Result<(), Transaction
|
||||
|
||||
if hf >= HardFork::V7 {
|
||||
for inps in inputs.windows(2) {
|
||||
match get_ki(&inps[0])?.cmp(&get_ki(&inps[1])?) {
|
||||
Ordering::Greater => (),
|
||||
_ => return Err(TransactionError::InputsAreNotOrdered),
|
||||
if get_ki(&inps[0])? <= get_ki(&inps[1])? {
|
||||
return Err(TransactionError::InputsAreNotOrdered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,13 @@ fn check_rct_type(ty: RctType, hf: HardFork, tx_hash: &[u8; 32]) -> Result<(), R
|
||||
T::MlsagBulletproofsCompactAmount if GRANDFATHERED_TRANSACTIONS.contains(tx_hash) => Ok(()),
|
||||
T::ClsagBulletproof if hf >= F::V13 && hf < F::V16 => Ok(()),
|
||||
T::ClsagBulletproofPlus if hf >= F::V15 => Ok(()),
|
||||
_ => Err(RingCTError::TypeNotAllowed),
|
||||
|
||||
T::AggregateMlsagBorromean
|
||||
| T::MlsagBorromean
|
||||
| T::MlsagBulletproofs
|
||||
| T::MlsagBulletproofsCompactAmount
|
||||
| T::ClsagBulletproof
|
||||
| T::ClsagBulletproofPlus => Err(RingCTError::TypeNotAllowed),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,10 @@ impl Service<BlockchainReadRequest> for DummyDatabase {
|
||||
let dummy_height = self.dummy_height;
|
||||
|
||||
async move {
|
||||
#[expect(
|
||||
clippy::wildcard_enum_match_arm,
|
||||
reason = "the context svc should not need other requests"
|
||||
)]
|
||||
Ok(match req {
|
||||
BlockchainReadRequest::BlockExtendedHeader(id) => {
|
||||
let mut id = id;
|
||||
|
||||
@@ -158,7 +158,7 @@ impl VerificationWanted {
|
||||
hf: HardFork,
|
||||
database: D,
|
||||
batch_prep_cache: Option<&BatchPrepareCache>,
|
||||
) -> FullVerification<D> {
|
||||
) -> FullVerification<'_, D> {
|
||||
FullVerification {
|
||||
prepped_txs: self.prepped_txs,
|
||||
current_chain_height,
|
||||
@@ -554,7 +554,7 @@ where
|
||||
.iter()
|
||||
.filter_map(|lock| match lock {
|
||||
Timelock::Time(time) => Some(time),
|
||||
_ => None,
|
||||
Timelock::None | Timelock::Block(_) => None,
|
||||
})
|
||||
.min();
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ pub fn new_ring_member_info(
|
||||
.iter()
|
||||
.filter_map(|out| match out.time_lock {
|
||||
Timelock::None => None,
|
||||
lock => Some(lock),
|
||||
Timelock::Block(_) | Timelock::Time(_) => Some(out.time_lock),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
|
||||
@@ -26,6 +26,10 @@ use cuprate_test_utils::data::TX_E2D393;
|
||||
fn dummy_database(outputs: BTreeMap<u64, OutputOnChain>) -> impl Database + Clone {
|
||||
let outputs = Arc::new(outputs);
|
||||
|
||||
#[expect(
|
||||
clippy::wildcard_enum_match_arm,
|
||||
reason = "Other database requests are not needed for this test"
|
||||
)]
|
||||
service_fn(move |req: BlockchainReadRequest| {
|
||||
ready(Ok(match req {
|
||||
BlockchainReadRequest::NumberOutputsWithAmount(_) => {
|
||||
@@ -48,7 +52,7 @@ fn dummy_database(outputs: BTreeMap<u64, OutputOnChain>) -> impl Database + Clon
|
||||
BlockchainResponse::Outputs(ret)
|
||||
}
|
||||
BlockchainReadRequest::KeyImagesSpent(_) => BlockchainResponse::KeyImagesSpent(false),
|
||||
_ => panic!("Database request not needed for this test"),
|
||||
_ => panic!(),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,23 +8,30 @@ fn set_commit_env() {
|
||||
|
||||
println!("cargo:rerun-if-changed={PATH}");
|
||||
|
||||
// FIXME: This could also be `std::fs::read({PATH}/{branch})`
|
||||
// so the machine building doesn't need `git`, although:
|
||||
// 1. Having `git` as a build dependency is probably ok
|
||||
// 2. It causes issues on PRs that aren't the `main` branch
|
||||
let output = std::process::Command::new("git")
|
||||
.arg("rev-parse")
|
||||
.arg("HEAD")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let commit = std::str::from_utf8(&output.stdout)
|
||||
let commit = if let Ok(t) = std::env::var("GITHUB_SHA") {
|
||||
t
|
||||
} else {
|
||||
// FIXME: This could also be `std::fs::read({PATH}/{branch})`
|
||||
// so the machine building doesn't need `git`, although:
|
||||
// 1. Having `git` as a build dependency is probably ok
|
||||
// 2. It causes issues on PRs that aren't the `main` branch
|
||||
String::from_utf8(
|
||||
std::process::Command::new("git")
|
||||
.args(["show", "-s", "--format=%H"])
|
||||
.output()
|
||||
.unwrap()
|
||||
.stdout,
|
||||
)
|
||||
.unwrap()
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
}
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
// Commit hash should always be 40 bytes long.
|
||||
assert_eq!(commit.len(), 40);
|
||||
assert_eq!(
|
||||
commit.len(),
|
||||
40,
|
||||
"Commit hash should always be 40 bytes long."
|
||||
);
|
||||
|
||||
println!("cargo:rustc-env=COMMIT={commit}");
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ mod tests {
|
||||
let array = [1_u8, 2, 3, 4, 5];
|
||||
let sub: &[u8; 3] = subarray(&array, 1);
|
||||
assert_eq!(sub, &[2, 3, 4]);
|
||||
assert!(std::ptr::eq(&array[1], &sub[0])); // same memory, not copy
|
||||
assert!(std::ptr::eq(&raw const array[1], &raw const sub[0])); // same memory, not copy
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -83,7 +83,8 @@ ignore = [
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
|
||||
# TODO: check this is sorted before a beta release.
|
||||
{ id = "RUSTSEC-2024-0436", reason = "`paste` unmaintained, not necessarily vulnerable yet." }
|
||||
{ id = "RUSTSEC-2024-0436", reason = "`paste` unmaintained, not necessarily vulnerable yet." },
|
||||
{ id = "RUSTSEC-2023-0071", reason = "rsa is used in arti." }
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
@@ -101,6 +102,7 @@ ignore = [
|
||||
allow = [
|
||||
# Nothing required - free to use without permission.
|
||||
"CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/
|
||||
"Unlicense",
|
||||
|
||||
# Must include copyright notice.
|
||||
"BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)
|
||||
|
||||
@@ -166,7 +166,7 @@ impl_path_lazylock! {
|
||||
fn path_with_network(path: &Path, network: Network) -> PathBuf {
|
||||
match network {
|
||||
Network::Mainnet => path.to_path_buf(),
|
||||
network => path.join(network.to_string()),
|
||||
Network::Testnet | Network::Stagenet => path.join(network.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
27
misc/changelogs/cuprated/0.0.5.md
Normal file
27
misc/changelogs/cuprated/0.0.5.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# cuprated 0.0.5 Molybdenite (2025-07-16)
|
||||
Cuprate is an alternative Monero node implementation. To get started, see: <https://user.cuprate.org>.
|
||||
|
||||
## Changes
|
||||
- Define Tor Zone, add onion addressing and more ([#481](https://github.com/Cuprate/cuprate/pull/481))
|
||||
- Update `fast-sync` to height `3456000` ([#502](https://github.com/Cuprate/cuprate/pull/502))
|
||||
- Fix `get_txid` for pre-RCT outputs ([#504](https://github.com/Cuprate/cuprate/pull/504))
|
||||
- RPC: enable `submit_block` and `/send_raw_transaction` ([#515](https://github.com/Cuprate/cuprate/pull/515))
|
||||
|
||||
## Downloads
|
||||
For convenience, the following binaries are produced using GitHub CI in a non-reproducible way; it is highly recommended to build `cuprated` from source instead, see <https://user.cuprate.org/getting-started/source>.
|
||||
|
||||
| OS | Architecture | Download |
|
||||
|---------|--------------|----------|
|
||||
| Windows | 64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-x86_64-pc-windows-msvc.zip>
|
||||
| macOS | x64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-x86_64-apple-darwin.tar.gz>
|
||||
| macOS | ARM64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-aarch64-apple-darwin.tar.gz>
|
||||
| Linux | x64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-x86_64-unknown-linux.tar.gz>
|
||||
| Linux | ARM64 | <https://github.com/Cuprate/cuprate/releases/download/cuprated-0.0.5/cuprated-0.0.5-aarch64-unknown-linux.tar.gz>
|
||||
|
||||
## Contributors
|
||||
Thank you to everyone who directly contributed to this release:
|
||||
|
||||
- @hinto-janai
|
||||
- @SyntheticBird45
|
||||
|
||||
There are other contributors that are not listed here, thank you to them as well.
|
||||
@@ -1 +1 @@
|
||||
0.0.4.md
|
||||
0.0.5.md
|
||||
@@ -14,7 +14,7 @@ pub use paste::paste;
|
||||
///
|
||||
/// struct Example {
|
||||
/// a: u8
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// epee_object!(
|
||||
/// Example,
|
||||
@@ -127,11 +127,12 @@ macro_rules! epee_object {
|
||||
|
||||
) => {
|
||||
cuprate_epee_encoding::macros::paste!(
|
||||
#[allow(non_snake_case)]
|
||||
#[allow(non_snake_case, clippy::empty_structs_with_brackets)]
|
||||
mod [<__epee_builder_ $obj>] {
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
#[allow(clippy::empty_structs_with_brackets)]
|
||||
pub struct [<__Builder $obj>] {
|
||||
$($field: Option<cuprate_epee_encoding::epee_object!(@internal_field_type $ty, $($ty_as)?)>,)*
|
||||
$($flat_field: <$flat_ty as cuprate_epee_encoding::EpeeObject>::Builder,)*
|
||||
|
||||
@@ -17,14 +17,16 @@ cuprate-fixed-bytes = { workspace = true }
|
||||
cuprate-types = { workspace = true, default-features = false, features = ["epee"] }
|
||||
cuprate-helper = { workspace = true, default-features = false, features = ["map"] }
|
||||
|
||||
bitflags = { workspace = true, features = ["std"] }
|
||||
bytes = { workspace = true, features = ["std"] }
|
||||
thiserror = { workspace = true }
|
||||
bitflags = { workspace = true, features = ["std"] }
|
||||
borsh = { workspace = true, features = ["derive"] }
|
||||
bytes = { workspace = true, features = ["std"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
arbitrary = { workspace = true, features = ["derive"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { workspace = true, features = ["std"]}
|
||||
hex = { workspace = true, features = ["std"]}
|
||||
proptest = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -26,7 +26,7 @@ pub mod network_address;
|
||||
pub mod p2p;
|
||||
|
||||
pub use cuprate_levin::BucketError;
|
||||
pub use network_address::{NetZone, NetworkAddress};
|
||||
pub use network_address::{NetZone, NetworkAddress, OnionAddr};
|
||||
pub use p2p::*;
|
||||
|
||||
// re-export.
|
||||
|
||||
@@ -26,6 +26,9 @@ use cuprate_epee_encoding::EpeeObject;
|
||||
mod epee_builder;
|
||||
use epee_builder::*;
|
||||
|
||||
mod onion_addr;
|
||||
pub use onion_addr::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum NetZone {
|
||||
Public,
|
||||
@@ -38,6 +41,7 @@ pub enum NetZone {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum NetworkAddress {
|
||||
Clear(SocketAddr),
|
||||
Tor(OnionAddr),
|
||||
}
|
||||
|
||||
impl EpeeObject for NetworkAddress {
|
||||
@@ -56,6 +60,7 @@ impl NetworkAddress {
|
||||
pub const fn get_zone(&self) -> NetZone {
|
||||
match self {
|
||||
Self::Clear(_) => NetZone::Public,
|
||||
Self::Tor(_) => NetZone::Tor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +77,7 @@ impl NetworkAddress {
|
||||
pub const fn port(&self) -> u16 {
|
||||
match self {
|
||||
Self::Clear(ip) => ip.port(),
|
||||
Self::Tor(addr) => addr.port(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +112,7 @@ impl TryFrom<NetworkAddress> for SocketAddr {
|
||||
fn try_from(value: NetworkAddress) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
NetworkAddress::Clear(addr) => Ok(addr),
|
||||
//_ => Err(NetworkAddressIncorrectZone)
|
||||
NetworkAddress::Tor(_) => Err(NetworkAddressIncorrectZone),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
//! Address epee serialization
|
||||
//!
|
||||
//! Addresses needs to be serialized into a specific format before being sent to other peers.
|
||||
//! This module is handling this particular construction.
|
||||
//!
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Imports
|
||||
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use bytes::Buf;
|
||||
use thiserror::Error;
|
||||
|
||||
use cuprate_epee_encoding::{epee_object, EpeeObjectBuilder};
|
||||
use cuprate_types::AddressType;
|
||||
|
||||
use crate::NetworkAddress;
|
||||
|
||||
use super::OnionAddr;
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Network address construction
|
||||
|
||||
#[derive(Default)]
|
||||
/// A serialized network address being communicated to or from a peer.
|
||||
pub struct TaggedNetworkAddress {
|
||||
ty: Option<u8>,
|
||||
/// Type of the network address (used later for conversion)
|
||||
ty: Option<AddressType>,
|
||||
/// All possible fields for a network address
|
||||
addr: Option<AllFieldsNetworkAddress>,
|
||||
}
|
||||
|
||||
epee_object!(
|
||||
TaggedNetworkAddress,
|
||||
ty("type"): Option<u8>,
|
||||
ty("type"): Option<AddressType>,
|
||||
addr: Option<AllFieldsNetworkAddress>,
|
||||
);
|
||||
|
||||
@@ -75,31 +91,57 @@ impl From<NetworkAddress> for TaggedNetworkAddress {
|
||||
match value {
|
||||
NetworkAddress::Clear(addr) => match addr {
|
||||
SocketAddr::V4(addr) => Self {
|
||||
ty: Some(1),
|
||||
ty: Some(AddressType::Ipv4),
|
||||
addr: Some(AllFieldsNetworkAddress {
|
||||
m_ip: Some(u32::from_le_bytes(addr.ip().octets())),
|
||||
m_port: Some(addr.port()),
|
||||
addr: None,
|
||||
host: None,
|
||||
port: None,
|
||||
}),
|
||||
},
|
||||
SocketAddr::V6(addr) => Self {
|
||||
ty: Some(2),
|
||||
ty: Some(AddressType::Ipv6),
|
||||
addr: Some(AllFieldsNetworkAddress {
|
||||
addr: Some(addr.ip().octets()),
|
||||
m_port: Some(addr.port()),
|
||||
m_ip: None,
|
||||
host: None,
|
||||
port: None,
|
||||
}),
|
||||
},
|
||||
},
|
||||
NetworkAddress::Tor(onion_addr) => Self {
|
||||
ty: Some(AddressType::Tor),
|
||||
addr: Some(AllFieldsNetworkAddress {
|
||||
m_ip: None,
|
||||
m_port: None,
|
||||
addr: None,
|
||||
host: Some(onion_addr.addr_string()),
|
||||
port: Some(onion_addr.port()),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// There are no ordering guarantees in epee format and as such all potential fields can be collected during deserialization.
|
||||
/// The [`AllFieldsNetworkAddress`] is containing, as its name suggest, all optional field describing an address , as if it
|
||||
/// could be of any type.
|
||||
struct AllFieldsNetworkAddress {
|
||||
/// IPv4 address
|
||||
m_ip: Option<u32>,
|
||||
/// IP port field
|
||||
m_port: Option<u16>,
|
||||
|
||||
/// IPv6 address
|
||||
addr: Option<[u8; 16]>,
|
||||
|
||||
/// Alternative network domain name (<domain>.onion or <domain>.i2p)
|
||||
host: Option<String>,
|
||||
/// Alternative network virtual port
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
epee_object!(
|
||||
@@ -107,22 +149,28 @@ epee_object!(
|
||||
m_ip: Option<u32>,
|
||||
m_port: Option<u16>,
|
||||
addr: Option<[u8; 16]>,
|
||||
host: Option<String>,
|
||||
port: Option<u16>,
|
||||
);
|
||||
|
||||
impl AllFieldsNetworkAddress {
|
||||
fn try_into_network_address(self, ty: u8) -> Option<NetworkAddress> {
|
||||
fn try_into_network_address(self, ty: AddressType) -> Option<NetworkAddress> {
|
||||
Some(match ty {
|
||||
1 => NetworkAddress::from(SocketAddrV4::new(
|
||||
AddressType::Ipv4 => NetworkAddress::from(SocketAddrV4::new(
|
||||
Ipv4Addr::from(self.m_ip?.to_le_bytes()),
|
||||
self.m_port?,
|
||||
)),
|
||||
2 => NetworkAddress::from(SocketAddrV6::new(
|
||||
AddressType::Ipv6 => NetworkAddress::from(SocketAddrV6::new(
|
||||
Ipv6Addr::from(self.addr?),
|
||||
self.m_port?,
|
||||
0,
|
||||
0,
|
||||
)),
|
||||
_ => return None,
|
||||
AddressType::Tor => {
|
||||
NetworkAddress::from(OnionAddr::new(self.host?.as_str(), self.port?).ok()?)
|
||||
}
|
||||
// Invalid
|
||||
AddressType::Invalid | AddressType::I2p => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
234
net/wire/src/network_address/onion_addr.rs
Normal file
234
net/wire/src/network_address/onion_addr.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
//! Onion address
|
||||
//!
|
||||
//! This module define v3 Tor onion addresses
|
||||
//!
|
||||
|
||||
use std::{
|
||||
fmt::Display,
|
||||
str::{self, FromStr},
|
||||
};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{NetworkAddress, NetworkAddressIncorrectZone};
|
||||
|
||||
/// A v3, `Copy`able onion address.
|
||||
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize)]
|
||||
pub struct OnionAddr {
|
||||
/// 56 characters encoded onion v3 domain without the .onion suffix
|
||||
/// <https://spec.torproject.org/rend-spec/encoding-onion-addresses.html>
|
||||
domain: [u8; 56],
|
||||
/// Virtual port of the peer
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
/// Error enum at parsing onion addresses
|
||||
#[derive(Debug, Error)]
|
||||
pub enum OnionAddrParsingError {
|
||||
#[error("Address is either too long or short, length: {0}")]
|
||||
InvalidLength(usize),
|
||||
#[error("Address contain non-utf8 code point at tld byte location: {0:x}")]
|
||||
NonUtf8Char(u8),
|
||||
#[error("This is not an onion address, Tld: {0}")]
|
||||
InvalidTld(String),
|
||||
#[error("Domain contains non base32 characters")]
|
||||
NonBase32Char,
|
||||
#[error("Invalid version. Found: {0}")]
|
||||
InvalidVersion(u8),
|
||||
#[error("The checksum is invalid.")]
|
||||
InvalidChecksum,
|
||||
#[error("Invalid port specified")]
|
||||
InvalidPort,
|
||||
}
|
||||
|
||||
impl OnionAddr {
|
||||
/// Attempt to create an [`OnionAddr`] from a complete .onion address string and a port.
|
||||
///
|
||||
/// Return an [`OnionAddrParsingError`] if the supplied `addr` is invalid.
|
||||
pub fn new(addr: &str, port: u16) -> Result<Self, OnionAddrParsingError> {
|
||||
Self::check_addr(addr).map(|d| Self { domain: d, port })
|
||||
}
|
||||
|
||||
/// Establish if the .onion address is valid.
|
||||
///
|
||||
/// Return the 56 character domain bytes if valid, `OnionAddrParsingError` otherwise.
|
||||
pub fn check_addr(addr: &str) -> Result<[u8; 56], OnionAddrParsingError> {
|
||||
// v3 onion addresses are 62 characters long
|
||||
if addr.len() != 62 {
|
||||
return Err(OnionAddrParsingError::InvalidLength(addr.len()));
|
||||
}
|
||||
|
||||
let Some((domain, tld)) = addr.split_at_checked(56) else {
|
||||
return Err(OnionAddrParsingError::NonUtf8Char(addr.as_bytes()[56]));
|
||||
};
|
||||
|
||||
// The ".onion" suffix must be located at the 57th byte.
|
||||
if tld != ".onion" {
|
||||
return Err(OnionAddrParsingError::InvalidTld(String::from(tld)));
|
||||
}
|
||||
|
||||
// The domain part must only contain base32 characters.
|
||||
if !domain
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.copied()
|
||||
.all(|c| c.is_ascii_lowercase() || (b'2'..=b'7').contains(&c))
|
||||
{
|
||||
return Err(OnionAddrParsingError::NonBase32Char);
|
||||
}
|
||||
|
||||
Ok(addr.as_bytes()[..56]
|
||||
.try_into()
|
||||
.unwrap_or_else(|e| panic!("We just validated address: {addr} : {e}")))
|
||||
}
|
||||
|
||||
/// Generate an onion address string.
|
||||
///
|
||||
/// Returns a `String` containing the onion domain name and ".onion" TLD only, in form of `zbjkbs...ofptid.onion`.
|
||||
pub fn addr_string(&self) -> String {
|
||||
let mut domain = str::from_utf8(&self.domain)
|
||||
.expect("Onion addresses are always containing UTF-8 characters.")
|
||||
.to_string();
|
||||
|
||||
domain.push_str(".onion");
|
||||
domain
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn domain(&self) -> [u8; 56] {
|
||||
self.domain
|
||||
}
|
||||
}
|
||||
|
||||
/// Display for [`OnionAddr`]. It prints the onion address and port, in the form of `<domain>.onion:<port>`
|
||||
impl Display for OnionAddr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let domain = str::from_utf8(&self.domain)
|
||||
.expect("Onion addresses are always containing UTF-8 characters.");
|
||||
|
||||
f.write_str(domain)?;
|
||||
f.write_str(".onion:")?;
|
||||
self.port.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`OnionAddr`] parses an onion address **and a port**.
|
||||
impl FromStr for OnionAddr {
|
||||
type Err = OnionAddrParsingError;
|
||||
|
||||
fn from_str(addr: &str) -> Result<Self, Self::Err> {
|
||||
let (addr, port) = addr
|
||||
.split_at_checked(62)
|
||||
.ok_or(OnionAddrParsingError::InvalidLength(addr.len()))?;
|
||||
|
||||
// Port
|
||||
let port: u16 = port
|
||||
.starts_with(':')
|
||||
.then(|| port[1..].parse().ok())
|
||||
.flatten()
|
||||
.ok_or(OnionAddrParsingError::InvalidPort)?;
|
||||
|
||||
// Address
|
||||
let domain = Self::check_addr(addr)?;
|
||||
|
||||
Ok(Self { domain, port })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<NetworkAddress> for OnionAddr {
|
||||
type Error = NetworkAddressIncorrectZone;
|
||||
fn try_from(value: NetworkAddress) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
NetworkAddress::Tor(addr) => Ok(addr),
|
||||
NetworkAddress::Clear(_) => Err(NetworkAddressIncorrectZone),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OnionAddr> for NetworkAddress {
|
||||
fn from(value: OnionAddr) -> Self {
|
||||
Self::Tor(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::{collection::vec, prelude::*};
|
||||
|
||||
use super::OnionAddr;
|
||||
|
||||
const VALID_ONION_ADDRESSES: &[&str] = &[
|
||||
"2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", // Tor Website
|
||||
"pzhdfe7jraknpj2qgu5cz2u3i4deuyfwmonvzu5i3nyw4t4bmg7o5pad.onion", // Tor Blog
|
||||
"monerotoruzizulg5ttgat2emf4d6fbmiea25detrmmy7erypseyteyd.onion", // Monero Website
|
||||
"sfprivg7qec6tdle7u6hdepzjibin6fn3ivm6qlwytr235rh5vc6bfqd.onion", // SethForPrivacy
|
||||
"yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion", // P2Pool
|
||||
"p2pool2giz2r5cpqicajwoazjcxkfujxswtk3jolfk2ubilhrkqam2id.onion", // P2Pool Observer
|
||||
"d6ac5qatnyodxisdehb3i4m7edfvtukxzhhtyadbgaxghcxee2xadpid.onion", // Rucknium ♥
|
||||
"duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion", // DuckDuckGo
|
||||
"featherdvtpi7ckdbkb2yxjfwx3oyvr3xjz3oo4rszylfzjdg6pbm3id.onion", // Feather wallet
|
||||
"revuo75joezkbeitqmas4ab6spbrkr4vzbhjmeuv75ovrfqfp47mtjid.onion", // Revuo
|
||||
"xoe4vn5uwdztif6goazfbmogh6wh5jc4up35bqdflu6bkdc5cas5vjqd.onion", // PrivacyGuides.org
|
||||
"allyouhavetodecideiswhattodowiththetimethatisgiventoyouu.onion", // Gandalf the Grey
|
||||
// Tor mainnet seed nodes as of 2025-05-15 with random ports
|
||||
"zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion",
|
||||
"qz43zul2x56jexzoqgkx2trzwcfnr6l3hbtfcfx54g4r3eahy3bssjyd.onion",
|
||||
"plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion",
|
||||
"plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion",
|
||||
"plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion",
|
||||
"aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn valid_onion_address() {
|
||||
for addr in VALID_ONION_ADDRESSES {
|
||||
assert!(
|
||||
OnionAddr::check_addr(addr).is_ok(),
|
||||
"Address {addr} has been reported as invalid."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn parse_valid_onion_address_w_port(ports in vec(any::<u16>(), 18)) {
|
||||
for (addr,port) in VALID_ONION_ADDRESSES.iter().zip(ports) {
|
||||
|
||||
let mut s = (*addr).to_string();
|
||||
s.push(':');
|
||||
s.push_str(&port.to_string());
|
||||
|
||||
assert!(
|
||||
s.parse::<OnionAddr>().is_ok(),
|
||||
"Address {addr} has been reported as invalid."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_onion_address(addresses in vec("[a-z][2-7]{56}.onion", 250)) {
|
||||
for addr in addresses {
|
||||
assert!(
|
||||
OnionAddr::check_addr(&addr).is_err(),
|
||||
"Address {addr} has been reported as valid."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invalid_onion_address_w_port(addresses in vec("[a-z][2-7]{56}.onion:[0-9]{1,5}", 250)) {
|
||||
for addr in addresses {
|
||||
assert!(
|
||||
addr.parse::<OnionAddr>().is_err(),
|
||||
"Address {addr} has been reported as valid."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,10 @@ impl ProtocolMessage {
|
||||
decode_message(ProtocolMessage::FluffyMissingTransactionsRequest, buf)?
|
||||
}
|
||||
C::GetTxPoolCompliment => decode_message(ProtocolMessage::GetTxPoolCompliment, buf)?,
|
||||
_ => return Err(BucketError::UnknownCommand),
|
||||
|
||||
C::Handshake | C::TimedSync | C::Ping | C::SupportFlags | C::Unknown(_) => {
|
||||
return Err(BucketError::UnknownCommand);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -296,7 +299,17 @@ impl AdminRequestMessage {
|
||||
|
||||
Self::SupportFlags
|
||||
}
|
||||
_ => return Err(BucketError::UnknownCommand),
|
||||
|
||||
C::NewBlock
|
||||
| C::NewTransactions
|
||||
| C::GetObjectsRequest
|
||||
| C::GetObjectsResponse
|
||||
| C::ChainRequest
|
||||
| C::ChainResponse
|
||||
| C::NewFluffyBlock
|
||||
| C::FluffyMissingTxsRequest
|
||||
| C::GetTxPoolCompliment
|
||||
| C::Unknown(_) => return Err(BucketError::UnknownCommand),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -343,7 +356,17 @@ impl AdminResponseMessage {
|
||||
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),
|
||||
|
||||
C::NewBlock
|
||||
| C::NewTransactions
|
||||
| C::GetObjectsRequest
|
||||
| C::GetObjectsResponse
|
||||
| C::ChainRequest
|
||||
| C::ChainResponse
|
||||
| C::NewFluffyBlock
|
||||
| C::FluffyMissingTxsRequest
|
||||
| C::GetTxPoolCompliment
|
||||
| C::Unknown(_) => return Err(BucketError::UnknownCommand),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -66,12 +66,12 @@ pub struct AddressBook<Z: BorshNetworkZone> {
|
||||
peer_save_task_handle: Option<JoinHandle<std::io::Result<()>>>,
|
||||
peer_save_interval: Interval,
|
||||
|
||||
cfg: AddressBookConfig,
|
||||
cfg: AddressBookConfig<Z>,
|
||||
}
|
||||
|
||||
impl<Z: BorshNetworkZone> AddressBook<Z> {
|
||||
pub fn new(
|
||||
cfg: AddressBookConfig,
|
||||
cfg: AddressBookConfig<Z>,
|
||||
white_peers: Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>,
|
||||
gray_peers: Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>,
|
||||
anchor_peers: Vec<Z::Addr>,
|
||||
@@ -394,7 +394,7 @@ impl<Z: BorshNetworkZone> Service<AddressBookRequest<Z>> for AddressBook<Z> {
|
||||
},
|
||||
)
|
||||
.map(|()| AddressBookResponse::Ok),
|
||||
AddressBookRequest::IncomingPeerList(peer_list) => {
|
||||
AddressBookRequest::IncomingPeerList(_, peer_list) => {
|
||||
self.handle_incoming_peer_list(peer_list);
|
||||
Ok(AddressBookResponse::Ok)
|
||||
}
|
||||
@@ -417,6 +417,9 @@ impl<Z: BorshNetworkZone> Service<AddressBookRequest<Z>> for AddressBook<Z> {
|
||||
AddressBookRequest::GetBan(addr) => Ok(AddressBookResponse::GetBan {
|
||||
unban_instant: self.peer_unban_instant(&addr).map(Instant::into_std),
|
||||
}),
|
||||
AddressBookRequest::OwnAddress => {
|
||||
Ok(AddressBookResponse::OwnAddress(self.cfg.our_own_address))
|
||||
}
|
||||
AddressBookRequest::Peerlist
|
||||
| AddressBookRequest::PeerlistSize
|
||||
| AddressBookRequest::ConnectionCount
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{path::PathBuf, time::Duration};
|
||||
use futures::StreamExt;
|
||||
use tokio::time::interval;
|
||||
|
||||
use cuprate_p2p_core::handles::HandleBuilder;
|
||||
use cuprate_p2p_core::{handles::HandleBuilder, NetworkZone};
|
||||
use cuprate_pruning::PruningSeed;
|
||||
|
||||
use super::{AddressBook, ConnectionPeerEntry, InternalPeerID};
|
||||
@@ -11,12 +11,13 @@ use crate::{peer_list::tests::make_fake_peer_list, AddressBookConfig, AddressBoo
|
||||
|
||||
use cuprate_test_utils::test_netzone::{TestNetZone, TestNetZoneAddr};
|
||||
|
||||
fn test_cfg() -> AddressBookConfig {
|
||||
fn test_cfg<Z: NetworkZone>() -> AddressBookConfig<Z> {
|
||||
AddressBookConfig {
|
||||
max_white_list_length: 100,
|
||||
max_gray_list_length: 500,
|
||||
peer_store_directory: PathBuf::new(),
|
||||
peer_save_period: Duration::from_secs(60),
|
||||
our_own_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ mod store;
|
||||
|
||||
/// The address book config.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AddressBookConfig {
|
||||
pub struct AddressBookConfig<Z: NetworkZone> {
|
||||
/// The maximum number of white peers in the peer list.
|
||||
///
|
||||
/// White peers are peers we have connected to before.
|
||||
@@ -33,6 +33,9 @@ pub struct AddressBookConfig {
|
||||
pub peer_store_directory: PathBuf,
|
||||
/// The amount of time between saving the address book to disk.
|
||||
pub peer_save_period: Duration,
|
||||
|
||||
/// Our own address to advertise to peers. (Only set if `Z::BROADCAST_OWN_ADDR` = `true`)
|
||||
pub our_own_address: Option<Z::Addr>,
|
||||
}
|
||||
|
||||
/// Possible errors when dealing with the address book.
|
||||
@@ -61,7 +64,7 @@ pub enum AddressBookError {
|
||||
|
||||
/// Initializes the P2P address book for a specific network zone.
|
||||
pub async fn init_address_book<Z: BorshNetworkZone>(
|
||||
cfg: AddressBookConfig,
|
||||
cfg: AddressBookConfig<Z>,
|
||||
) -> Result<book::AddressBook<Z>, std::io::Error> {
|
||||
let (white_list, gray_list) = match store::read_peers_from_disk::<Z>(&cfg).await {
|
||||
Ok(res) => res,
|
||||
|
||||
@@ -90,7 +90,9 @@ fn peer_list_remove_specific_peer() {
|
||||
let peers = peer_list.peers;
|
||||
|
||||
for (_, addrs) in pruning_idxs {
|
||||
addrs.iter().for_each(|adr| assert_ne!(adr, &peer.adr));
|
||||
for adr in &addrs {
|
||||
assert_ne!(adr, &peer.adr);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(!peers.contains_key(&peer.adr));
|
||||
|
||||
@@ -27,7 +27,7 @@ struct DeserPeerDataV1<A: NetZoneAddress> {
|
||||
}
|
||||
|
||||
pub(crate) fn save_peers_to_disk<Z: BorshNetworkZone>(
|
||||
cfg: &AddressBookConfig,
|
||||
cfg: &AddressBookConfig<Z>,
|
||||
white_list: &PeerList<Z>,
|
||||
gray_list: &PeerList<Z>,
|
||||
) -> JoinHandle<std::io::Result<()>> {
|
||||
@@ -51,7 +51,7 @@ pub(crate) fn save_peers_to_disk<Z: BorshNetworkZone>(
|
||||
}
|
||||
|
||||
pub(crate) async fn read_peers_from_disk<Z: BorshNetworkZone>(
|
||||
cfg: &AddressBookConfig,
|
||||
cfg: &AddressBookConfig<Z>,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>,
|
||||
|
||||
@@ -11,7 +11,7 @@ txpool = ["dep:rand_distr", "dep:tokio-util", "dep:tokio"]
|
||||
|
||||
[dependencies]
|
||||
tower = { workspace = true, features = ["util"] }
|
||||
tracing = { workspace = true, features = ["std"] }
|
||||
tracing = { workspace = true, features = ["std", "attributes"] }
|
||||
|
||||
futures = { workspace = true, features = ["std"] }
|
||||
tokio = { workspace = true, features = ["rt", "sync", "macros"], optional = true}
|
||||
|
||||
@@ -12,8 +12,9 @@ use tokio::{
|
||||
sync::{mpsc, oneshot},
|
||||
task::JoinSet,
|
||||
};
|
||||
use tokio_util::time::DelayQueue;
|
||||
use tokio_util::time::{delay_queue, DelayQueue};
|
||||
use tower::{Service, ServiceExt};
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{
|
||||
pool::IncomingTx,
|
||||
@@ -41,6 +42,7 @@ pub struct DandelionPoolManager<P, R, Tx, TxId, PeerId> {
|
||||
|
||||
/// Current stem pool embargo timers.
|
||||
pub(crate) embargo_timers: DelayQueue<TxId>,
|
||||
pub(crate) embargo_timer_keys: HashMap<TxId, delay_queue::Key>,
|
||||
/// The distrobution to sample to get embargo timers.
|
||||
pub(crate) embargo_dist: Exp<f64>,
|
||||
|
||||
@@ -68,8 +70,10 @@ where
|
||||
embargo_timer
|
||||
);
|
||||
|
||||
self.embargo_timers
|
||||
.insert(tx_id, Duration::from_secs_f64(embargo_timer));
|
||||
let key = self
|
||||
.embargo_timers
|
||||
.insert(tx_id.clone(), Duration::from_secs_f64(embargo_timer));
|
||||
self.embargo_timer_keys.insert(tx_id, key);
|
||||
}
|
||||
|
||||
/// Stems the tx, setting the stem origin, if it wasn't already set.
|
||||
@@ -164,9 +168,9 @@ where
|
||||
// Remove the tx from the maps used during the stem phase.
|
||||
self.stem_origins.remove(&tx_id);
|
||||
|
||||
// The key for this is *Not* the tx_id, it is given on insert, so just keep the timer in the
|
||||
// map. These timers should be relatively short, so it shouldn't be a problem.
|
||||
//self.embargo_timers.try_remove(&tx_id);
|
||||
if let Some(key) = self.embargo_timer_keys.remove(&tx_id) {
|
||||
self.embargo_timers.try_remove(&key);
|
||||
}
|
||||
|
||||
self.backing_pool
|
||||
.ready()
|
||||
@@ -220,43 +224,47 @@ where
|
||||
Ok(tx.map(|tx| tx.0))
|
||||
}
|
||||
|
||||
#[expect(clippy::type_complexity)]
|
||||
/// Starts the [`DandelionPoolManager`].
|
||||
pub(crate) async fn run(
|
||||
mut self,
|
||||
mut rx: mpsc::Receiver<(IncomingTx<Tx, TxId, PeerId>, oneshot::Sender<()>)>,
|
||||
mut rx: mpsc::Receiver<(
|
||||
(IncomingTx<Tx, TxId, PeerId>, tracing::Span),
|
||||
oneshot::Sender<()>,
|
||||
)>,
|
||||
) {
|
||||
tracing::debug!("Starting dandelion++ tx-pool, config: {:?}", self.config);
|
||||
|
||||
loop {
|
||||
tracing::trace!("Waiting for next event.");
|
||||
tokio::select! {
|
||||
// biased to handle current txs before routing new ones.
|
||||
biased;
|
||||
Some(fired) = self.embargo_timers.next() => {
|
||||
tracing::debug!("Embargo timer fired, did not see stem tx in time.");
|
||||
let span = tracing::debug_span!("embargo_timer_fired");
|
||||
tracing::debug!(parent: &span,"Embargo timer fired, did not see stem tx in time.");
|
||||
|
||||
let tx_id = fired.into_inner();
|
||||
if let Err(e) = self.promote_and_fluff_tx(tx_id).await {
|
||||
tracing::error!("Error handling fired embargo timer: {e}");
|
||||
if let Err(e) = self.promote_and_fluff_tx(tx_id).instrument(span.clone()).await {
|
||||
tracing::error!(parent: &span, "Error handling fired embargo timer: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Some(Ok((tx_id, res))) = self.routing_set.join_next() => {
|
||||
tracing::trace!("Received d++ routing result.");
|
||||
let span = tracing::debug_span!("dandelion_routing_result");
|
||||
|
||||
let res = match res {
|
||||
Ok(State::Fluff) => {
|
||||
tracing::debug!("Transaction was fluffed upgrading it to the public pool.");
|
||||
self.promote_tx(tx_id).await
|
||||
tracing::debug!(parent: &span, "Transaction was fluffed upgrading it to the public pool.");
|
||||
self.promote_tx(tx_id).instrument(span.clone()).await
|
||||
}
|
||||
Err(tx_state) => {
|
||||
tracing::debug!("Error routing transaction, trying again.");
|
||||
tracing::debug!(parent: &span, "Error routing transaction, trying again.");
|
||||
|
||||
match self.get_tx_from_pool(tx_id.clone()).await {
|
||||
match self.get_tx_from_pool(tx_id.clone()).instrument(span.clone()).await {
|
||||
Ok(Some(tx)) => match tx_state {
|
||||
TxState::Fluff => self.fluff_tx(tx, tx_id).await,
|
||||
TxState::Stem { from } => self.stem_tx(tx, tx_id, Some(from)).await,
|
||||
TxState::Local => self.stem_tx(tx, tx_id, None).await,
|
||||
TxState::Fluff => self.fluff_tx(tx, tx_id).instrument(span.clone()).await,
|
||||
TxState::Stem { from } => self.stem_tx(tx, tx_id, Some(from)).instrument(span.clone()).await,
|
||||
TxState::Local => self.stem_tx(tx, tx_id, None).instrument(span.clone()).await,
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
_ => continue,
|
||||
@@ -266,22 +274,24 @@ where
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
tracing::error!("Error handling transaction routing return: {e}");
|
||||
tracing::error!(parent: &span, "Error handling transaction routing return: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
req = rx.recv() => {
|
||||
tracing::debug!("Received new tx to route.");
|
||||
|
||||
let Some((IncomingTx { tx, tx_id, routing_state }, res_tx)) = req else {
|
||||
let Some(((IncomingTx { tx, tx_id, routing_state }, span), res_tx)) = req else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = self.handle_incoming_tx(tx, routing_state, tx_id).await {
|
||||
let span = tracing::debug_span!(parent: &span, "dandelion_pool_manager");
|
||||
|
||||
tracing::debug!(parent: &span, "Received new tx to route.");
|
||||
|
||||
if let Err(e) = self.handle_incoming_tx(tx, routing_state, tx_id).instrument(span.clone()).await {
|
||||
#[expect(clippy::let_underscore_must_use, reason = "dropped receivers can be ignored")]
|
||||
let _ = res_tx.send(());
|
||||
|
||||
tracing::error!("Error handling transaction in dandelion pool: {e}");
|
||||
tracing::error!(parent: &span, "Error handling transaction in dandelion pool: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ use tokio::{
|
||||
};
|
||||
use tokio_util::{sync::PollSender, time::DelayQueue};
|
||||
use tower::Service;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{
|
||||
pool::manager::DandelionPoolShutDown,
|
||||
@@ -87,14 +86,13 @@ where
|
||||
routing_set: JoinSet::new(),
|
||||
stem_origins: HashMap::new(),
|
||||
embargo_timers: DelayQueue::new(),
|
||||
embargo_timer_keys: HashMap::new(),
|
||||
embargo_dist: Exp::new(1.0 / config.average_embargo_timeout().as_secs_f64()).unwrap(),
|
||||
config,
|
||||
_tx: PhantomData,
|
||||
};
|
||||
|
||||
let span = tracing::debug_span!("dandelion_pool");
|
||||
|
||||
tokio::spawn(pool.run(rx).instrument(span));
|
||||
tokio::spawn(pool.run(rx));
|
||||
|
||||
DandelionPoolService {
|
||||
tx: PollSender::new(tx),
|
||||
@@ -106,8 +104,12 @@ where
|
||||
/// Used to send [`IncomingTx`]s to the [`DandelionPoolManager`]
|
||||
#[derive(Clone)]
|
||||
pub struct DandelionPoolService<Tx, TxId, PeerId> {
|
||||
#[expect(clippy::type_complexity)]
|
||||
/// The channel to [`DandelionPoolManager`].
|
||||
tx: PollSender<(IncomingTx<Tx, TxId, PeerId>, oneshot::Sender<()>)>,
|
||||
tx: PollSender<(
|
||||
(IncomingTx<Tx, TxId, PeerId>, tracing::Span),
|
||||
oneshot::Sender<()>,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl<Tx, TxId, PeerId> Service<IncomingTx<Tx, TxId, PeerId>>
|
||||
@@ -131,7 +133,7 @@ where
|
||||
|
||||
let res = self
|
||||
.tx
|
||||
.send_item((req, tx))
|
||||
.send_item(((req, tracing::Span::current()), tx))
|
||||
.map_err(|_| DandelionPoolShutDown);
|
||||
|
||||
async move {
|
||||
|
||||
@@ -183,8 +183,14 @@ where
|
||||
.map_err(DandelionRouterError::OutboundPeerStreamError))
|
||||
.ok_or(DandelionRouterError::OutboundPeerDiscoverExited)??
|
||||
{
|
||||
OutboundPeer::Peer(key, svc) => {
|
||||
self.stem_peers.insert(key, svc);
|
||||
OutboundPeer::Peer(key, mut svc) => {
|
||||
let poll = svc.poll_ready(cx);
|
||||
|
||||
self.stem_peers.insert(key.clone(), svc);
|
||||
|
||||
if ready!(poll).is_err() {
|
||||
self.stem_peers.remove(&key);
|
||||
}
|
||||
}
|
||||
OutboundPeer::Exhausted => {
|
||||
tracing::warn!("Failed to retrieve enough outbound peers for optimal dandelion++, privacy may be degraded.");
|
||||
@@ -293,8 +299,7 @@ where
|
||||
State::Stem
|
||||
};
|
||||
|
||||
self.span
|
||||
.record("state", format!("{:?}", self.current_state));
|
||||
self.span = tracing::debug_span!("dandelion_router", state = ?self.current_state);
|
||||
tracing::debug!(parent: &self.span, "Starting new d++ epoch",);
|
||||
|
||||
self.epoch_start = Instant::now();
|
||||
@@ -348,13 +353,13 @@ where
|
||||
self.fluff_tx(req.tx)
|
||||
}
|
||||
State::Stem => {
|
||||
tracing::trace!(parent: &self.span, "Steming transaction");
|
||||
tracing::trace!(parent: &self.span, "Stemming transaction");
|
||||
|
||||
self.stem_tx(req.tx, &from)
|
||||
}
|
||||
},
|
||||
TxState::Local => {
|
||||
tracing::debug!(parent: &self.span, "Steming local tx.");
|
||||
tracing::debug!(parent: &self.span, "Stemming local tx.");
|
||||
|
||||
self.stem_local_tx(req.tx)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,12 @@ tower = { workspace = true, features = ["util", "tracing", "make"] }
|
||||
|
||||
cfg-if = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rand = { workspace = true, features = ["std", "std_rng"] }
|
||||
tracing = { workspace = true, features = ["std", "attributes"] }
|
||||
hex-literal = { workspace = true }
|
||||
|
||||
hex = { workspace = true }
|
||||
|
||||
borsh = { workspace = true, features = ["derive", "std"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -15,7 +15,7 @@ use tracing::Instrument;
|
||||
|
||||
use cuprate_helper::asynch::InfallibleOneshotReceiver;
|
||||
use cuprate_pruning::PruningSeed;
|
||||
use cuprate_wire::CoreSyncData;
|
||||
use cuprate_wire::{BasicNodeData, CoreSyncData};
|
||||
|
||||
use crate::{
|
||||
handles::{ConnectionGuard, ConnectionHandle},
|
||||
@@ -40,14 +40,14 @@ pub enum InternalPeerID<A> {
|
||||
/// A known address.
|
||||
KnownAddr(A),
|
||||
/// An unknown address (probably an inbound anonymity network connection).
|
||||
Unknown(u128),
|
||||
Unknown([u8; 16]),
|
||||
}
|
||||
|
||||
impl<A: Display> Display for InternalPeerID<A> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::KnownAddr(addr) => addr.fmt(f),
|
||||
Self::Unknown(id) => f.write_str(&format!("Unknown, ID: {id}")),
|
||||
Self::Unknown(id) => f.write_str(&format!("Unknown, ID: {}", hex::encode(id))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,8 @@ pub struct PeerInformation<A> {
|
||||
pub direction: ConnectionDirection,
|
||||
/// The peer's [`PruningSeed`].
|
||||
pub pruning_seed: PruningSeed,
|
||||
/// The peer's [`BasicNodeData`].
|
||||
pub basic_node_data: BasicNodeData,
|
||||
/// The [`CoreSyncData`] of this peer.
|
||||
///
|
||||
/// Data across fields are not necessarily related, so [`CoreSyncData::top_id`] is not always the
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct ConnectRequest<Z: NetworkZone> {
|
||||
}
|
||||
|
||||
/// The connector service, this service connects to peer and returns the [`Client`].
|
||||
#[derive(Clone)]
|
||||
pub struct Connector<Z: NetworkZone, T: Transport<Z>, AdrBook, CSync, ProtoHdlrMkr, BrdcstStrmMkr> {
|
||||
handshaker: HandShaker<Z, T, AdrBook, CSync, ProtoHdlrMkr, BrdcstStrmMkr>,
|
||||
}
|
||||
|
||||
@@ -347,6 +347,7 @@ where
|
||||
.ready()
|
||||
.await?
|
||||
.call(AddressBookRequest::IncomingPeerList(
|
||||
addr,
|
||||
handshake_res
|
||||
.local_peerlist_new
|
||||
.into_iter()
|
||||
@@ -486,11 +487,11 @@ where
|
||||
handle,
|
||||
direction,
|
||||
pruning_seed,
|
||||
basic_node_data: peer_node_data,
|
||||
core_sync_data: Arc::new(Mutex::new(peer_core_sync)),
|
||||
};
|
||||
|
||||
let protocol_request_handler = protocol_request_svc_maker
|
||||
.as_service()
|
||||
.ready()
|
||||
.await?
|
||||
.call(info.clone())
|
||||
@@ -693,7 +694,7 @@ where
|
||||
allow_ping = false;
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
AdminRequestMessage::Handshake(_) | AdminRequestMessage::TimedSync(_) => {
|
||||
return Err(HandshakeError::PeerSentInvalidMessage(
|
||||
"Peer sent an admin request before responding to the handshake",
|
||||
));
|
||||
|
||||
@@ -102,12 +102,12 @@ impl<N: NetworkZone> Service<AddressBookRequest<N>> for DummyAddressBook {
|
||||
| AddressBookRequest::TakeRandomWhitePeer { .. } => {
|
||||
return ready(Err("dummy address book does not hold peers".into()));
|
||||
}
|
||||
AddressBookRequest::NewConnection { .. } | AddressBookRequest::IncomingPeerList(_) => {
|
||||
AddressBookResponse::Ok
|
||||
}
|
||||
AddressBookRequest::NewConnection { .. }
|
||||
| AddressBookRequest::IncomingPeerList(_, _) => AddressBookResponse::Ok,
|
||||
AddressBookRequest::GetBan(_) => AddressBookResponse::GetBan {
|
||||
unban_instant: None,
|
||||
},
|
||||
AddressBookRequest::OwnAddress => AddressBookResponse::OwnAddress(None),
|
||||
AddressBookRequest::Peerlist
|
||||
| AddressBookRequest::PeerlistSize
|
||||
| AddressBookRequest::ConnectionCount
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use futures::TryFutureExt;
|
||||
use rand::{thread_rng, Rng};
|
||||
use tower::ServiceExt;
|
||||
|
||||
use cuprate_pruning::PruningSeed;
|
||||
use cuprate_wire::{
|
||||
admin::{
|
||||
PingResponse, SupportFlagsResponse, TimedSyncRequest, TimedSyncResponse,
|
||||
@@ -14,6 +16,7 @@ use crate::{
|
||||
constants::MAX_PEERS_IN_PEER_LIST_MESSAGE,
|
||||
services::{
|
||||
AddressBookRequest, AddressBookResponse, CoreSyncDataRequest, CoreSyncDataResponse,
|
||||
ZoneSpecificPeerListEntryBase,
|
||||
},
|
||||
AddressBook, CoreSyncSvc, NetworkZone, PeerRequest, PeerResponse, ProtocolRequestHandler,
|
||||
};
|
||||
@@ -101,18 +104,7 @@ where
|
||||
|
||||
*self.peer_info.core_sync_data.lock().unwrap() = req.payload_data;
|
||||
|
||||
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!");
|
||||
};
|
||||
|
||||
// Fetch core sync data.
|
||||
let CoreSyncDataResponse(core_sync_data) = self
|
||||
.our_sync_svc
|
||||
.ready()
|
||||
@@ -120,6 +112,54 @@ where
|
||||
.call(CoreSyncDataRequest)
|
||||
.await?;
|
||||
|
||||
// Attempt to fetch our own address if supported by this network zone.
|
||||
let own_addr = if Z::BROADCAST_OWN_ADDR {
|
||||
let AddressBookResponse::OwnAddress(own_addr) = self
|
||||
.address_book_svc
|
||||
.ready()
|
||||
.await?
|
||||
.call(AddressBookRequest::OwnAddress)
|
||||
.await?
|
||||
else {
|
||||
panic!("Address book sent incorrect response!");
|
||||
};
|
||||
|
||||
own_addr
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut peer_list_req_size = MAX_PEERS_IN_PEER_LIST_MESSAGE;
|
||||
if own_addr.is_some() {
|
||||
peer_list_req_size -= 1;
|
||||
}
|
||||
|
||||
// Fetch a peerlist to send
|
||||
let AddressBookResponse::Peers(mut peers) = self
|
||||
.address_book_svc
|
||||
.ready()
|
||||
.await?
|
||||
.call(AddressBookRequest::GetWhitePeers(peer_list_req_size))
|
||||
.await?
|
||||
else {
|
||||
panic!("Address book sent incorrect response!");
|
||||
};
|
||||
|
||||
if let Some(own_addr) = own_addr {
|
||||
// Append our address to the final peer list
|
||||
peers.insert(
|
||||
thread_rng().gen_range(0..=peers.len()),
|
||||
ZoneSpecificPeerListEntryBase {
|
||||
adr: own_addr,
|
||||
id: self.our_basic_node_data.peer_id,
|
||||
last_seen: 0,
|
||||
pruning_seed: PruningSeed::NotPruned,
|
||||
rpc_port: self.our_basic_node_data.rpc_port,
|
||||
rpc_credits_per_hash: self.our_basic_node_data.rpc_credits_per_hash,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(TimedSyncResponse {
|
||||
payload_data: core_sync_data,
|
||||
local_peerlist_new: peers.into_iter().map(Into::into).collect(),
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::{
|
||||
fields(addr = %peer_information.id),
|
||||
skip_all,
|
||||
)]
|
||||
pub async fn connection_timeout_monitor_task<N: NetworkZone, AdrBook, CSync>(
|
||||
pub(super) async fn connection_timeout_monitor_task<N: NetworkZone, AdrBook, CSync>(
|
||||
peer_information: PeerInformation<N::Addr>,
|
||||
|
||||
connection_tx: mpsc::Sender<ConnectionTaskRequest>,
|
||||
@@ -119,6 +119,7 @@ where
|
||||
.ready()
|
||||
.await?
|
||||
.call(AddressBookRequest::IncomingPeerList(
|
||||
peer_information.id,
|
||||
timed_sync
|
||||
.local_peerlist_new
|
||||
.into_iter()
|
||||
|
||||
@@ -87,7 +87,7 @@ pub mod transports;
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use network_zones::ClearNet;
|
||||
pub use network_zones::{ClearNet, Tor};
|
||||
pub use protocol::*;
|
||||
use services::*;
|
||||
//re-export
|
||||
@@ -160,7 +160,7 @@ pub trait NetworkZone: Clone + Copy + Send + 'static {
|
||||
/// peer or instantiating a listener for the `NetworkZone` `Z` over a `Transport` method `T`.
|
||||
///
|
||||
/// Ultimately, multiple transports can implement the same trait for providing alternative
|
||||
/// ways for a network zone to operate (example: ClearNet can operate on both TCP and Tor.)
|
||||
/// ways for a network zone to operate (example: [`ClearNet`] can operate on both TCP and Tor.)
|
||||
#[async_trait::async_trait]
|
||||
pub trait Transport<Z: NetworkZone>: Clone + Send + 'static {
|
||||
/// Client configuration necessary when establishing a connection to a peer.
|
||||
@@ -168,9 +168,9 @@ pub trait Transport<Z: NetworkZone>: Clone + Send + 'static {
|
||||
/// Note: Currently, this client config is considered immutable during operational runtime. If one
|
||||
/// wish to apply modifications on the fly, they will need to make use of an inner shared and mutable
|
||||
/// reference to do so.
|
||||
type ClientConfig: Default + Clone + Debug + Send + Sync + 'static;
|
||||
type ClientConfig: Clone + Send + Sync + 'static;
|
||||
/// Server configuration necessary when instantiating a listener for inbound connections.
|
||||
type ServerConfig: Default + Clone + Debug + Send + Sync + 'static;
|
||||
type ServerConfig: Send + Sync + 'static;
|
||||
|
||||
/// The stream (incoming data) type of this transport method.
|
||||
type Stream: Stream<Item = Result<Message, BucketError>> + Unpin + Send + 'static;
|
||||
@@ -280,11 +280,10 @@ impl<T> ProtocolRequestHandler for T where
|
||||
}
|
||||
|
||||
pub trait ProtocolRequestHandlerMaker<Z: NetworkZone>:
|
||||
tower::MakeService<
|
||||
tower::Service<
|
||||
client::PeerInformation<Z::Addr>,
|
||||
ProtocolRequest,
|
||||
MakeError = tower::BoxError,
|
||||
Service: ProtocolRequestHandler,
|
||||
Error = tower::BoxError,
|
||||
Response: ProtocolRequestHandler,
|
||||
Future: Send + 'static,
|
||||
> + Send
|
||||
+ 'static
|
||||
@@ -292,11 +291,10 @@ pub trait ProtocolRequestHandlerMaker<Z: NetworkZone>:
|
||||
}
|
||||
|
||||
impl<T, Z: NetworkZone> ProtocolRequestHandlerMaker<Z> for T where
|
||||
T: tower::MakeService<
|
||||
T: tower::Service<
|
||||
client::PeerInformation<Z::Addr>,
|
||||
ProtocolRequest,
|
||||
MakeError = tower::BoxError,
|
||||
Service: ProtocolRequestHandler,
|
||||
Error = tower::BoxError,
|
||||
Response: ProtocolRequestHandler,
|
||||
Future: Send + 'static,
|
||||
> + Send
|
||||
+ 'static
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
mod clear;
|
||||
mod tor;
|
||||
|
||||
pub use clear::ClearNet;
|
||||
pub use tor::Tor;
|
||||
|
||||
@@ -27,7 +27,6 @@ impl NetZoneAddress for SocketAddr {
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ClearNet {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NetworkZone for ClearNet {
|
||||
const NAME: &'static str = "ClearNet";
|
||||
|
||||
|
||||
52
p2p/p2p-core/src/network_zones/tor.rs
Normal file
52
p2p/p2p-core/src/network_zones/tor.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Tor Zone
|
||||
//!
|
||||
//! This module define the Tor Zone that uses the Tor network and .onion service addressing.
|
||||
//!
|
||||
//! ### Anonymity
|
||||
//!
|
||||
//! This is an anonymous network and is therefore operating under the following behavior:
|
||||
//! - The node address is blend into its own address book.
|
||||
//! - This network is only use for relaying transactions.
|
||||
//!
|
||||
//! ### Addressing
|
||||
//!
|
||||
//! The Tor Zone is using [`OnionAddr`] as its address type.
|
||||
//!
|
||||
|
||||
use cuprate_wire::network_address::OnionAddr;
|
||||
|
||||
use crate::{NetZoneAddress, NetworkZone};
|
||||
|
||||
impl NetZoneAddress for OnionAddr {
|
||||
type BanID = [u8; 56];
|
||||
|
||||
fn set_port(&mut self, port: u16) {
|
||||
self.port = port;
|
||||
}
|
||||
|
||||
fn ban_id(&self) -> Self::BanID {
|
||||
self.domain()
|
||||
}
|
||||
|
||||
fn make_canonical(&mut self) {
|
||||
// There are no canonical form of an onion address...
|
||||
}
|
||||
|
||||
fn should_add_to_peer_list(&self) -> bool {
|
||||
// Validation of the onion address has been done at the type construction...
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Tor;
|
||||
|
||||
impl NetworkZone for Tor {
|
||||
const NAME: &'static str = "Tor";
|
||||
|
||||
const CHECK_NODE_ID: bool = false;
|
||||
|
||||
const BROADCAST_OWN_ADDR: bool = true;
|
||||
|
||||
type Addr = OnionAddr;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ impl TryFrom<PeerRequest> for BroadcastMessage {
|
||||
PeerRequest::Protocol(ProtocolRequest::NewFluffyBlock(block)) => {
|
||||
Ok(Self::NewFluffyBlock(block))
|
||||
}
|
||||
_ => Err(MessageConversionError),
|
||||
PeerRequest::Admin(_) | PeerRequest::Protocol(_) => Err(MessageConversionError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,10 @@ pub enum AddressBookRequest<Z: NetworkZone> {
|
||||
},
|
||||
|
||||
/// Tells the address book about a peer list received from a peer.
|
||||
IncomingPeerList(Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>),
|
||||
IncomingPeerList(
|
||||
InternalPeerID<Z::Addr>,
|
||||
Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>,
|
||||
),
|
||||
|
||||
/// Takes a random white peer from the peer list. If height is specified
|
||||
/// then the peer list should retrieve a peer that should have a full
|
||||
@@ -115,6 +118,9 @@ pub enum AddressBookRequest<Z: NetworkZone> {
|
||||
/// Gets the specified number of white peers, or less if we don't have enough.
|
||||
GetWhitePeers(usize),
|
||||
|
||||
/// Gets our own optionally specified address
|
||||
OwnAddress,
|
||||
|
||||
/// Get info on all peers, white & grey.
|
||||
Peerlist,
|
||||
|
||||
@@ -175,4 +181,10 @@ pub enum AddressBookResponse<Z: NetworkZone> {
|
||||
|
||||
/// Response to [`AddressBookRequest::GetBans`].
|
||||
GetBans(Vec<BanState<Z::Addr>>),
|
||||
|
||||
/// Response to [`AddressBookRequest::OwnAddress`]
|
||||
///
|
||||
/// This returns [`None`] if the address book do
|
||||
/// not contain a self designated address.
|
||||
OwnAddress(Option<Z::Addr>),
|
||||
}
|
||||
|
||||
@@ -141,7 +141,9 @@ impl Encoder<LevinMessage<Message>> for FragmentCodec {
|
||||
self.0.encode(frag.into(), dst)?;
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Handshakes should only send bucket bodys"),
|
||||
LevinMessage::Bucket(_) | LevinMessage::Dummy(_) => {
|
||||
unreachable!("Handshakes should only send bucket bodys");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,25 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["SyntheticBird"]
|
||||
|
||||
[features]
|
||||
default = ["static"]
|
||||
static = ["arti-client/static"]
|
||||
|
||||
[dependencies]
|
||||
cuprate-p2p-core = { workspace = true }
|
||||
cuprate-wire = { workspace = true }
|
||||
|
||||
async-trait = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio = { workspace = true, features = ["net"] }
|
||||
tokio-socks = { workspace = true, features = ["tokio"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
arti-client = { workspace = true, features = ["tokio", "native-tls", "onion-service-client", "onion-service-service", "experimental-api", "static-sqlite"] }
|
||||
tor-config-path = { workspace = true }
|
||||
tor-cell = { workspace = true }
|
||||
tor-hsservice = { workspace = true }
|
||||
tor-proto = { workspace = true }
|
||||
tor-rtcompat = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
216
p2p/p2p-transport/src/arti.rs
Normal file
216
p2p/p2p-transport/src/arti.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
//! Arti Transport
|
||||
//!
|
||||
//! This module defines a transport method for the `Tor` network zone using the `arti_client` library.
|
||||
//!
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Imports
|
||||
|
||||
use std::{
|
||||
io::{self, ErrorKind},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use arti_client::{DataReader, DataWriter, TorClient, TorClientConfig};
|
||||
use async_trait::async_trait;
|
||||
use futures::{Stream, StreamExt};
|
||||
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||
use tor_cell::relaycell::msg::Connected;
|
||||
use tor_config_path::CfgPathResolver;
|
||||
use tor_hsservice::{handle_rend_requests, OnionService, RunningOnionService};
|
||||
use tor_proto::stream::IncomingStreamRequest;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
|
||||
use cuprate_p2p_core::{ClearNet, NetworkZone, Tor, Transport};
|
||||
use cuprate_wire::MoneroWireCodec;
|
||||
|
||||
use crate::DisabledListener;
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Configuration
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ArtiClientConfig {
|
||||
/// Arti bootstrapped client
|
||||
pub client: TorClient<PreferredRuntime>,
|
||||
}
|
||||
|
||||
pub struct ArtiServerConfig {
|
||||
/// Arti onion service
|
||||
pub onion_svc: OnionService,
|
||||
/// Listening port
|
||||
pub port: u16,
|
||||
|
||||
// Mandatory resources for launching the onion service
|
||||
client: TorClient<PreferredRuntime>,
|
||||
path_resolver: Arc<CfgPathResolver>,
|
||||
}
|
||||
|
||||
impl ArtiServerConfig {
|
||||
pub fn new(
|
||||
onion_svc: OnionService,
|
||||
port: u16,
|
||||
client: &TorClient<PreferredRuntime>,
|
||||
config: &TorClientConfig,
|
||||
) -> Self {
|
||||
let path_resolver: &CfgPathResolver = config.as_ref();
|
||||
|
||||
Self {
|
||||
onion_svc,
|
||||
port,
|
||||
client: client.clone(),
|
||||
path_resolver: Arc::new(path_resolver.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Transport
|
||||
|
||||
type PinnedStream<I> = Pin<Box<dyn Stream<Item = I> + Send>>;
|
||||
|
||||
/// An onion service listening for incoming peer connections.
|
||||
pub struct OnionListener {
|
||||
/// A handle to the onion service instance.
|
||||
_onion_svc: Arc<RunningOnionService>,
|
||||
/// A modified stream that produce a data stream and sink from rendez-vous requests.
|
||||
listener: PinnedStream<Result<(DataReader, DataWriter), io::Error>>,
|
||||
}
|
||||
|
||||
impl Stream for OnionListener {
|
||||
type Item = Result<
|
||||
(
|
||||
Option<<Tor as NetworkZone>::Addr>,
|
||||
FramedRead<DataReader, MoneroWireCodec>,
|
||||
FramedWrite<DataWriter, MoneroWireCodec>,
|
||||
),
|
||||
io::Error,
|
||||
>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.listener.poll_next_unpin(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(req) => Poll::Ready(req.map(|r| {
|
||||
r.map(|(stream, sink)| {
|
||||
(
|
||||
None, // Inbound is anonymous
|
||||
FramedRead::new(stream, MoneroWireCodec::default()),
|
||||
FramedWrite::new(sink, MoneroWireCodec::default()),
|
||||
)
|
||||
})
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Arti;
|
||||
|
||||
#[async_trait]
|
||||
impl Transport<Tor> for Arti {
|
||||
type ClientConfig = ArtiClientConfig;
|
||||
type ServerConfig = ArtiServerConfig;
|
||||
|
||||
type Stream = FramedRead<DataReader, MoneroWireCodec>;
|
||||
type Sink = FramedWrite<DataWriter, MoneroWireCodec>;
|
||||
type Listener = OnionListener;
|
||||
|
||||
async fn connect_to_peer(
|
||||
addr: <Tor as NetworkZone>::Addr,
|
||||
config: &Self::ClientConfig,
|
||||
) -> Result<(Self::Stream, Self::Sink), io::Error> {
|
||||
config
|
||||
.client
|
||||
.connect((addr.addr_string(), addr.port()))
|
||||
.await
|
||||
.map_err(|e| io::Error::new(ErrorKind::ConnectionAborted, e.to_string()))
|
||||
.map(|stream| {
|
||||
let (stream, sink) = stream.split();
|
||||
(
|
||||
FramedRead::new(stream, MoneroWireCodec::default()),
|
||||
FramedWrite::new(sink, MoneroWireCodec::default()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn incoming_connection_listener(
|
||||
config: Self::ServerConfig,
|
||||
) -> Result<Self::Listener, io::Error> {
|
||||
// Launch onion service
|
||||
#[expect(clippy::clone_on_ref_ptr)]
|
||||
let (svc, rdv_stream) = config
|
||||
.onion_svc
|
||||
.launch(
|
||||
config.client.runtime().clone(),
|
||||
config.client.dirmgr().clone(),
|
||||
config.client.hs_circ_pool().clone(),
|
||||
config.path_resolver,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Accept all rendez-vous and await correct stream request
|
||||
#[expect(clippy::wildcard_enum_match_arm)]
|
||||
let req_stream = handle_rend_requests(rdv_stream).then(move |sreq| async move {
|
||||
match sreq.request() {
|
||||
// As specified in: <https://spec.torproject.org/rend-spec/managing-streams.html>
|
||||
//
|
||||
// A client that wishes to open a data stream with us needs to send a BEGIN message with an empty address
|
||||
// and no flags. We additionally filter requests to the correct port configured and advertised on P2P.
|
||||
IncomingStreamRequest::Begin(r)
|
||||
if r.port() == config.port && r.addr().is_empty() && r.flags().is_empty() =>
|
||||
{
|
||||
let stream = sreq
|
||||
.accept(Connected::new_empty())
|
||||
.await
|
||||
.map_err(|e| io::Error::new(ErrorKind::BrokenPipe, e.to_string()))?;
|
||||
|
||||
Ok(stream.split())
|
||||
}
|
||||
_ => {
|
||||
sreq.shutdown_circuit()
|
||||
.expect("Should never panic, unless programming error from arti's end.");
|
||||
|
||||
Err(io::Error::other("Received invalid command"))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(OnionListener {
|
||||
_onion_svc: svc,
|
||||
listener: Box::pin(req_stream),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Transport<ClearNet> for Arti {
|
||||
type ClientConfig = ArtiClientConfig;
|
||||
type ServerConfig = ();
|
||||
|
||||
type Stream = FramedRead<DataReader, MoneroWireCodec>;
|
||||
type Sink = FramedWrite<DataWriter, MoneroWireCodec>;
|
||||
type Listener = DisabledListener<ClearNet, DataReader, DataWriter>;
|
||||
|
||||
async fn connect_to_peer(
|
||||
addr: <ClearNet as NetworkZone>::Addr,
|
||||
config: &Self::ClientConfig,
|
||||
) -> Result<(Self::Stream, Self::Sink), io::Error> {
|
||||
config
|
||||
.client
|
||||
.connect(addr.to_string())
|
||||
.await
|
||||
.map_err(|e| io::Error::new(ErrorKind::ConnectionAborted, e.to_string()))
|
||||
.map(|stream| {
|
||||
let (stream, sink) = stream.split();
|
||||
(
|
||||
FramedRead::new(stream, MoneroWireCodec::default()),
|
||||
FramedWrite::new(sink, MoneroWireCodec::default()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn incoming_connection_listener(
|
||||
_config: Self::ServerConfig,
|
||||
) -> Result<Self::Listener, io::Error> {
|
||||
panic!("In anonymized clearnet mode, inbound is disabled!");
|
||||
}
|
||||
}
|
||||
35
p2p/p2p-transport/src/disabled.rs
Normal file
35
p2p/p2p-transport/src/disabled.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use std::{
|
||||
io,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures::Stream;
|
||||
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||
|
||||
use cuprate_p2p_core::NetworkZone;
|
||||
use cuprate_wire::MoneroWireCodec;
|
||||
|
||||
/// In proxied clearnet mode, inbound is disabled.
|
||||
pub struct DisabledListener<Z: NetworkZone, R, W> {
|
||||
_zone: PhantomData<Z>,
|
||||
_reader: PhantomData<R>,
|
||||
_writer: PhantomData<W>,
|
||||
}
|
||||
|
||||
impl<Z: NetworkZone, R, W> Stream for DisabledListener<Z, R, W> {
|
||||
type Item = Result<
|
||||
(
|
||||
Option<Z::Addr>,
|
||||
FramedRead<R, MoneroWireCodec>,
|
||||
FramedWrite<W, MoneroWireCodec>,
|
||||
),
|
||||
io::Error,
|
||||
>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
// Panic within [`Transport::incoming_connection_listener`]
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,15 @@
|
||||
//! ## P2P Transports
|
||||
//!
|
||||
//! This crate will welcome additional transport implementation for Cuprate.
|
||||
//! This crate implement additional transports for Cuprate.
|
||||
|
||||
/// Arti library implementation.
|
||||
mod arti;
|
||||
pub use arti::{Arti, ArtiClientConfig, ArtiServerConfig};
|
||||
|
||||
/// Tor daemon (SOCKS5) implementation
|
||||
mod tor;
|
||||
pub use tor::{Daemon, DaemonClientConfig, DaemonServerConfig};
|
||||
|
||||
/// Disabled listener
|
||||
mod disabled;
|
||||
pub(crate) use disabled::DisabledListener;
|
||||
|
||||
112
p2p/p2p-transport/src/tor.rs
Normal file
112
p2p/p2p-transport/src/tor.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! Tor Daemon Transport
|
||||
//!
|
||||
//! This module defines a transport method for the `Tor` network zone using an external Tor daemon supporting SOCKS5.
|
||||
//!
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Imports
|
||||
|
||||
use std::{
|
||||
io::{self, ErrorKind},
|
||||
net::{IpAddr, SocketAddr},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::Stream;
|
||||
use tokio::net::{
|
||||
tcp::{OwnedReadHalf, OwnedWriteHalf},
|
||||
TcpListener,
|
||||
};
|
||||
use tokio_socks::tcp::Socks5Stream;
|
||||
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||
|
||||
use cuprate_p2p_core::{NetworkZone, Tor, Transport};
|
||||
use cuprate_wire::MoneroWireCodec;
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Configuration
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct DaemonClientConfig {
|
||||
/// Socket address of the external Tor daemon
|
||||
pub tor_daemon: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct DaemonServerConfig {
|
||||
/// Listening IP Address.
|
||||
pub ip: IpAddr,
|
||||
|
||||
/// Listening TCP Port.
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Transport
|
||||
|
||||
/// A simple TCP server waiting for connections from the Tor daemon
|
||||
pub struct DaemonInboundStream {
|
||||
listener: TcpListener,
|
||||
}
|
||||
|
||||
impl Stream for DaemonInboundStream {
|
||||
type Item = Result<
|
||||
(
|
||||
Option<<Tor as NetworkZone>::Addr>,
|
||||
FramedRead<OwnedReadHalf, MoneroWireCodec>,
|
||||
FramedWrite<OwnedWriteHalf, MoneroWireCodec>,
|
||||
),
|
||||
io::Error,
|
||||
>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
self.listener
|
||||
.poll_accept(cx)
|
||||
.map_ok(|(stream, _)| {
|
||||
let (stream, sink) = stream.into_split();
|
||||
|
||||
(
|
||||
None, // Inbound is anonymous
|
||||
FramedRead::new(stream, MoneroWireCodec::default()),
|
||||
FramedWrite::new(sink, MoneroWireCodec::default()),
|
||||
)
|
||||
})
|
||||
.map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Daemon;
|
||||
|
||||
#[async_trait]
|
||||
impl Transport<Tor> for Daemon {
|
||||
type ClientConfig = DaemonClientConfig;
|
||||
type ServerConfig = DaemonServerConfig;
|
||||
|
||||
type Stream = FramedRead<OwnedReadHalf, MoneroWireCodec>;
|
||||
type Sink = FramedWrite<OwnedWriteHalf, MoneroWireCodec>;
|
||||
type Listener = DaemonInboundStream;
|
||||
|
||||
async fn connect_to_peer(
|
||||
addr: <Tor as NetworkZone>::Addr,
|
||||
config: &Self::ClientConfig,
|
||||
) -> Result<(Self::Stream, Self::Sink), io::Error> {
|
||||
Socks5Stream::connect(config.tor_daemon, addr.to_string())
|
||||
.await
|
||||
.map_err(|e| io::Error::new(ErrorKind::ConnectionAborted, e.to_string()))
|
||||
.map(|stream| {
|
||||
let (stream, sink) = stream.into_inner().into_split();
|
||||
(
|
||||
FramedRead::new(stream, MoneroWireCodec::default()),
|
||||
FramedWrite::new(sink, MoneroWireCodec::default()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn incoming_connection_listener(
|
||||
config: Self::ServerConfig,
|
||||
) -> Result<Self::Listener, io::Error> {
|
||||
let listener = TcpListener::bind((config.ip, config.port)).await?;
|
||||
|
||||
Ok(DaemonInboundStream { listener })
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ use crate::{
|
||||
)
|
||||
)]
|
||||
#[expect(clippy::used_underscore_binding)]
|
||||
pub async fn download_batch_task<N: NetworkZone>(
|
||||
pub(super) async fn download_batch_task<N: NetworkZone>(
|
||||
client: ClientDropGuard<N>,
|
||||
ids: ByteArrayVec<32>,
|
||||
previous_id: [u8; 32],
|
||||
|
||||
@@ -79,7 +79,7 @@ pub(crate) async fn request_chain_entry_from_peer<N: NetworkZone>(
|
||||
///
|
||||
/// We then wait for their response and choose the peer who claims the highest cumulative difficulty.
|
||||
#[instrument(level = "error", skip_all)]
|
||||
pub async fn initial_chain_search<N: NetworkZone, C>(
|
||||
pub(super) async fn initial_chain_search<N: NetworkZone, C>(
|
||||
peer_set: &mut BoxCloneService<PeerSetRequest, PeerSetResponse<N>, tower::BoxError>,
|
||||
mut our_chain_svc: C,
|
||||
) -> Result<ChainTracker<N>, BlockDownloadError>
|
||||
|
||||
@@ -26,8 +26,9 @@ use cuprate_p2p_core::{
|
||||
use cuprate_pruning::PruningSeed;
|
||||
use cuprate_types::{BlockCompleteEntry, TransactionBlobs};
|
||||
use cuprate_wire::{
|
||||
common::PeerSupportFlags,
|
||||
protocol::{ChainResponse, GetObjectsResponse},
|
||||
CoreSyncData,
|
||||
BasicNodeData, CoreSyncData,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -242,7 +243,7 @@ fn mock_block_downloader_client(blockchain: Arc<MockBlockchain>) -> Client<Clear
|
||||
},
|
||||
)))
|
||||
}
|
||||
_ => panic!(),
|
||||
PeerRequest::Admin(_) | PeerRequest::Protocol(_) => panic!(),
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
@@ -250,6 +251,14 @@ fn mock_block_downloader_client(blockchain: Arc<MockBlockchain>) -> Client<Clear
|
||||
|
||||
let info = PeerInformation {
|
||||
id: InternalPeerID::Unknown(rand::random()),
|
||||
basic_node_data: BasicNodeData {
|
||||
my_port: 0,
|
||||
network_id: [0; 16],
|
||||
peer_id: 0,
|
||||
support_flags: PeerSupportFlags::FLUFFY_BLOCKS,
|
||||
rpc_port: 0,
|
||||
rpc_credits_per_hash: 0,
|
||||
},
|
||||
handle: connection_handle,
|
||||
direction: ConnectionDirection::Inbound,
|
||||
pruning_seed: PruningSeed::NotPruned,
|
||||
|
||||
@@ -424,8 +424,8 @@ mod tests {
|
||||
let (mut brcst, outbound_mkr, inbound_mkr) =
|
||||
init_broadcast_channels::<TestNetZone<true>>(TEST_CONFIG);
|
||||
|
||||
let mut outbound_stream = pin!(outbound_mkr(InternalPeerID::Unknown(1)));
|
||||
let mut inbound_stream = pin!(inbound_mkr(InternalPeerID::Unknown(1)));
|
||||
let mut outbound_stream = pin!(outbound_mkr(InternalPeerID::Unknown([1; 16])));
|
||||
let mut inbound_stream = pin!(inbound_mkr(InternalPeerID::Unknown([1; 16])));
|
||||
|
||||
// Outbound should get 1 and 3, inbound should get 2 and 3.
|
||||
|
||||
@@ -483,8 +483,8 @@ mod tests {
|
||||
let (mut brcst, outbound_mkr, inbound_mkr) =
|
||||
init_broadcast_channels::<TestNetZone<true>>(TEST_CONFIG);
|
||||
|
||||
let mut outbound_stream = pin!(outbound_mkr(InternalPeerID::Unknown(1)));
|
||||
let mut inbound_stream = pin!(inbound_mkr(InternalPeerID::Unknown(1)));
|
||||
let mut outbound_stream = pin!(outbound_mkr(InternalPeerID::Unknown([1; 16])));
|
||||
let mut inbound_stream = pin!(inbound_mkr(InternalPeerID::Unknown([1; 16])));
|
||||
|
||||
brcst
|
||||
.ready()
|
||||
@@ -509,11 +509,11 @@ mod tests {
|
||||
let (mut brcst, outbound_mkr, inbound_mkr) =
|
||||
init_broadcast_channels::<TestNetZone<true>>(TEST_CONFIG);
|
||||
|
||||
let mut outbound_stream = pin!(outbound_mkr(InternalPeerID::Unknown(1)));
|
||||
let mut outbound_stream_from = pin!(outbound_mkr(InternalPeerID::Unknown(0)));
|
||||
let mut outbound_stream = pin!(outbound_mkr(InternalPeerID::Unknown([1; 16])));
|
||||
let mut outbound_stream_from = pin!(outbound_mkr(InternalPeerID::Unknown([0; 16])));
|
||||
|
||||
let mut inbound_stream = pin!(inbound_mkr(InternalPeerID::Unknown(1)));
|
||||
let mut inbound_stream_from = pin!(inbound_mkr(InternalPeerID::Unknown(0)));
|
||||
let mut inbound_stream = pin!(inbound_mkr(InternalPeerID::Unknown([1; 16])));
|
||||
let mut inbound_stream_from = pin!(inbound_mkr(InternalPeerID::Unknown([0; 16])));
|
||||
|
||||
brcst
|
||||
.ready()
|
||||
@@ -522,7 +522,7 @@ mod tests {
|
||||
.call(BroadcastRequest::Transaction {
|
||||
tx_bytes: Bytes::from_static(&[1]),
|
||||
direction: None,
|
||||
received_from: Some(InternalPeerID::Unknown(0)),
|
||||
received_from: Some(InternalPeerID::Unknown([0; 16])),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct P2PConfig<Z: NetworkZone> {
|
||||
pub rpc_port: u16,
|
||||
|
||||
/// The [`AddressBookConfig`].
|
||||
pub address_book_config: AddressBookConfig,
|
||||
pub address_book_config: AddressBookConfig<Z>,
|
||||
}
|
||||
|
||||
/// Configuration part responsible of transportation
|
||||
|
||||
@@ -34,12 +34,13 @@ use crate::{
|
||||
/// Starts the inbound server. This function will listen to all incoming connections
|
||||
/// and initiate handshake if needed, after verifying the address isn't banned.
|
||||
#[instrument(level = "warn", skip_all)]
|
||||
pub async fn inbound_server<Z, T, HS, A>(
|
||||
pub(super) async fn inbound_server<Z, T, HS, A>(
|
||||
new_connection_tx: mpsc::Sender<Client<Z>>,
|
||||
mut handshaker: HS,
|
||||
mut address_book: A,
|
||||
config: P2PConfig<Z>,
|
||||
transport_config: Option<T::ServerConfig>,
|
||||
inbound_semaphore: Arc<Semaphore>,
|
||||
) -> Result<(), tower::BoxError>
|
||||
where
|
||||
Z: NetworkZone,
|
||||
@@ -67,8 +68,8 @@ where
|
||||
|
||||
let mut listener = pin!(listener);
|
||||
|
||||
// Create semaphore for limiting to maximum inbound connections.
|
||||
let semaphore = Arc::new(Semaphore::new(config.max_inbound_connections));
|
||||
// Use the provided semaphore for limiting to maximum inbound connections.
|
||||
let semaphore = inbound_semaphore;
|
||||
// Create ping request handling JoinSet
|
||||
let mut ping_join_set = JoinSet::new();
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::FutureExt;
|
||||
use tokio::{sync::mpsc, task::JoinSet};
|
||||
use tokio::{
|
||||
sync::mpsc,
|
||||
task::JoinSet,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
use tower::{buffer::Buffer, util::BoxCloneService, Service, ServiceExt};
|
||||
use tracing::{instrument, Instrument, Span};
|
||||
|
||||
@@ -31,6 +35,40 @@ use connection_maintainer::MakeConnectionRequest;
|
||||
use peer_set::PeerSet;
|
||||
pub use peer_set::{ClientDropGuard, PeerSetRequest, PeerSetResponse};
|
||||
|
||||
/// Interval for checking inbound connection status (1 hour)
|
||||
const INBOUND_CONNECTION_MONITOR_INTERVAL: Duration = Duration::from_secs(3600);
|
||||
|
||||
/// Monitors for inbound connections and logs a warning if none are detected.
|
||||
///
|
||||
/// This task runs every hour to check if there are inbound connections available.
|
||||
/// If `max_inbound_connections` is 0, the task will exit without logging.
|
||||
#[expect(clippy::infinite_loop)]
|
||||
async fn inbound_connection_monitor(
|
||||
inbound_semaphore: Arc<tokio::sync::Semaphore>,
|
||||
max_inbound_connections: usize,
|
||||
p2p_port: u16,
|
||||
) {
|
||||
// Skip monitoring if inbound connections are disabled
|
||||
if max_inbound_connections == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
// Wait for the monitoring interval
|
||||
sleep(INBOUND_CONNECTION_MONITOR_INTERVAL).await;
|
||||
|
||||
// Check if we have any inbound connections
|
||||
// If available permits equals max_inbound_connections, no peers are connected
|
||||
let available_permits = inbound_semaphore.available_permits();
|
||||
if available_permits == max_inbound_connections {
|
||||
tracing::warn!(
|
||||
"No incoming connections - check firewalls/routers allow port {}",
|
||||
p2p_port
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the P2P [`NetworkInterface`] for a specific [`NetworkZone`].
|
||||
///
|
||||
/// This function starts all the tasks to maintain/accept/make connections.
|
||||
@@ -39,7 +77,7 @@ pub use peer_set::{ClientDropGuard, PeerSetRequest, PeerSetResponse};
|
||||
/// You must provide:
|
||||
/// - 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 = Z::NAME))]
|
||||
#[instrument(level = "error", name = "net", skip_all, fields(zone = Z::NAME))]
|
||||
pub async fn initialize_network<Z, T, PR, CS>(
|
||||
protocol_request_handler_maker: PR,
|
||||
core_sync_svc: CS,
|
||||
@@ -111,6 +149,9 @@ where
|
||||
|
||||
let peer_set = PeerSet::new(new_connection_rx);
|
||||
|
||||
// Create semaphore for limiting inbound connections and monitoring
|
||||
let inbound_semaphore = Arc::new(tokio::sync::Semaphore::new(config.max_inbound_connections));
|
||||
|
||||
let mut background_tasks = JoinSet::new();
|
||||
|
||||
background_tasks.spawn(
|
||||
@@ -118,6 +159,17 @@ where
|
||||
.run()
|
||||
.instrument(Span::current()),
|
||||
);
|
||||
|
||||
// Spawn inbound connection monitor task
|
||||
background_tasks.spawn(
|
||||
inbound_connection_monitor(
|
||||
Arc::clone(&inbound_semaphore),
|
||||
config.max_inbound_connections,
|
||||
config.p2p_port,
|
||||
)
|
||||
.instrument(tracing::info_span!("inbound_connection_monitor")),
|
||||
);
|
||||
|
||||
background_tasks.spawn(
|
||||
inbound_server::inbound_server(
|
||||
new_connection_tx,
|
||||
@@ -125,6 +177,7 @@ where
|
||||
address_book.clone(),
|
||||
config,
|
||||
transport_config.server_config,
|
||||
inbound_semaphore,
|
||||
)
|
||||
.map(|res| {
|
||||
if let Err(e) = res {
|
||||
|
||||
@@ -231,6 +231,7 @@ impl Ord for DecompressedPruningSeed {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// Compare the `log_stripes` first so peers which store more blocks are greater than peers
|
||||
// storing less.
|
||||
#[expect(clippy::wildcard_enum_match_arm)]
|
||||
match self.log_stripes.cmp(&other.log_stripes) {
|
||||
Ordering::Equal => self.stripe.cmp(&other.stripe),
|
||||
ord => ord,
|
||||
|
||||
@@ -28,7 +28,7 @@ futures = { workspace = true, optional = true }
|
||||
[dev-dependencies]
|
||||
cuprate-test-utils = { workspace = true }
|
||||
|
||||
axum = { version = "0.7.5", features = ["json", "tokio", "http2"] }
|
||||
axum = { workspace = true, features = ["json", "tokio", "http2"] }
|
||||
serde_json = { workspace = true, features = ["std"] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
ureq = { version = "2.10.1", features = ["json"] }
|
||||
|
||||
@@ -88,7 +88,7 @@ impl Id {
|
||||
pub const fn as_u64(&self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Num(n) => Some(*n),
|
||||
_ => None,
|
||||
Self::Null | Self::Str(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ impl Id {
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Str(s) => Some(s.as_ref()),
|
||||
_ => None,
|
||||
Self::Null | Self::Num(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ macro_rules! define_request_and_response {
|
||||
}
|
||||
) => { paste::paste! {
|
||||
$crate::macros::define_request! {
|
||||
#[allow(dead_code, missing_docs, reason = "inside a macro")]
|
||||
#[allow(dead_code, missing_docs, clippy::empty_structs_with_brackets, reason = "inside a macro")]
|
||||
#[doc = $crate::macros::define_request_and_response_doc!(
|
||||
"response" => [<$type_name Response>],
|
||||
$monero_daemon_rpc_doc_link,
|
||||
@@ -107,6 +107,7 @@ macro_rules! define_request_and_response {
|
||||
///
|
||||
$( #[$type_attr] )*
|
||||
///
|
||||
#[allow(clippy::empty_structs_with_brackets)]
|
||||
$( #[$request_type_attr] )*
|
||||
[<$type_name Request>] $(($restricted $(, $empty)?))? {
|
||||
$(
|
||||
@@ -119,7 +120,7 @@ macro_rules! define_request_and_response {
|
||||
}
|
||||
|
||||
$crate::macros::define_response! {
|
||||
#[allow(dead_code, missing_docs, reason = "inside a macro")]
|
||||
#[allow(dead_code, missing_docs, clippy::empty_structs_with_brackets, reason = "inside a macro")]
|
||||
#[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!(
|
||||
@@ -236,7 +237,7 @@ macro_rules! define_request {
|
||||
)*
|
||||
}
|
||||
) => {
|
||||
#[allow(dead_code, missing_docs, reason = "inside a macro")]
|
||||
#[allow(dead_code, missing_docs, clippy::empty_structs_with_brackets, reason = "inside a macro")]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
$( #[$attr] )*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user