Files
observer/Cargo.toml
rob thijssen 0b3c1b2e1b feat(telemetry): decode the whole of NodeDetails, and locate nodes by country
We were reading two fields of eleven — the name and the peer id — and dropping
the rest, including the hardware every node already announces. `NodeMetadata`
now carries the slow-moving half: implementation, version, the target triple,
CPU, cores, memory, kernel, distro, whether it is virtualised, and the process
start time.

The volatile half stays out on purpose. Peer count, transaction queue,
bandwidth and state cache change every few seconds, are already a rolling series
in the feed, and would be a lot of rows for something nobody would page back
through.

`startup_time` is kept rather than derived, so node uptime becomes a fact about
the node rather than about how long this observer has been watching it.

**Location is resolved to a country and no finer.** The feed sends
`[latitude, longitude, city]` and no country, so `blackbeard_core::geo` derives
one offline from a 508 KiB boundary set — no network call, and no third party
told where these nodes are. The city is read and discarded: those coordinates
come from an IP geolocation database and at city resolution are wrong often
enough that publishing one would assert something we do not know. Nothing
downstream can render a city because nothing downstream is given one. The
lookup returns subdivisions before countries, and `GB-ENG` is exactly the extra
precision this is avoiding, so only the two-letter code survives.

`LocatedNode` is a separate frame because the telemetry server geolocates
asynchronously — `AddedNode` almost always carries a null location and the
country follows a moment later. Which means a reconnect, which re-sends every
node with a null location again, must not erase a country already resolved.
There is a test for that, because it would present as countries silently
vanishing hours after a restart.

The mapping is pinned by tests built from frames captured off the live feed
rather than from a spec: the format is positional and differs between telemetry
releases, so bytes the feed actually sent are the only honest fixture. If a
future release renumbers a field, those tests are what says so.

Refs #15

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-13 17:10:32 +03:00

73 lines
3.3 KiB
TOML

[workspace]
resolver = "3"
members = ["crates/*"]
[workspace.package]
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
license = "GPL-3.0-or-later"
authors = ["Rob Thijssen <rob@lair.cafe>"]
repository = "https://git.lair.cafe/Quantus-Network/blackbeard.observer"
[workspace.dependencies]
blackbeard-entities = { path = "crates/blackbeard-entities", version = "=0.1.0" }
blackbeard-core = { path = "crates/blackbeard-core", version = "=0.1.0" }
blackbeard-data = { path = "crates/blackbeard-data", version = "=0.1.0" }
anyhow = "1"
axum = { version = "0.8", features = ["ws", "macros"] }
chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"] }
clap = { version = "4", features = ["derive", "env"] }
figment = { version = "0.10", features = ["toml", "env"] }
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
hex = "0.4"
# The chain's own description of itself, and a decoder driven by it. This is the
# alternative to hand-written SCALE readers that rot on every runtime upgrade,
# and to pulling in subxt or the runtime crate to avoid writing them.
frame-metadata = { version = "23", default-features = false, features = ["current", "decode"] }
parity-scale-codec = { version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false }
scale-value = { version = "0.18", default-features = false }
# Only for `IgnoreVisitor`: the pre-flight walk in `Runtime::decode_checked`
# that keeps a bogus length prefix from sizing an allocation. Pinned to the
# version `scale-value` itself resolves, so both see one type registry.
scale-decode = { version = "0.16", default-features = false }
# Offline reverse geocoding, country level only. The embedded 180x90 dataset is
# 508 KiB and needs no network at all — see `blackbeard_core::geo`.
country-boundaries = "1"
rust_iso3166 = "0.2"
# The chain's own reward-address derivation, so the observer computes the same
# account `pallets/mining-rewards` pays rather than guessing or asking.
qp-poseidon-core = { version = "3.1.0", default-features = false }
# SS58 checksums only. Base58 is hand-rolled; this is not.
blake2 = { version = "0.10", default-features = false }
twox-hash = { version = "2", default-features = false, features = ["xxhash64"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", default-features = false, features = [
"postgres",
"runtime-tokio-rustls",
"macros", "json",
"migrate",
"chrono",
"bigdecimal",
] }
bigdecimal = "0.4"
primitive-types = { version = "0.13", default-features = false }
thiserror = "2"
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect", "rustls-tls-native-roots"] }
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
ts-rs = { version = "10", features = ["serde-compat", "chrono-impl", "no-serde-warnings"] }
url = "2"
[profile.release]
lto = "thin"
codegen-units = 1
strip = "debuginfo"