Two faults, one visible symptom: both testnet leaderboards frozen for a day at the height their gap opened at, while mainnet was fine. The daemon had aborted 316 times in ten hours, always identically — `memory allocation of 82014765760 bytes failed`, 76.4 GiB. That is 1,025,184,572 x 80, and 80 is `size_of::<scale_value::Value<u32>>()`: scale-value sizes a sequence's `Vec` from the compact length in the blob before decoding a single item, so a blob that disagrees with the registry asks for an allocation of any size at all and Rust aborts on the failed one. The step that would have caught the mismatch is the one that never runs, which is why `decode_events(&blob).ok()` could not help — there was no `Err`, only SIGABRT. Every decode now goes through `Runtime::decode_checked`, which walks the bytes first with scale-decode's `IgnoreVisitor`. That crate has no `with_capacity` anywhere, so an impossible length runs out of input on the first item and comes back as an error. The regression test is the exact four-byte blob, reproduced against the mainnet fixture before the guard went in — the abort message matched production byte for byte. `fill_gap` then turned a short process lifetime into no progress whatsoever: it accumulated the whole gap and wrote once after the loop, so every interruption discarded everything read. Each block is four RPC round trips, one a historical state read; 1,696 blocks against a remote endpoint does not fit in the two minutes the aborts allowed. And this is not only about the aborts — `blackbeard-api-cert.path` restarts the service several times a day, so any gap wider than that interval was already unfillable, logging `filling a gap in the head stream` on every start with the same `from` and looking like progress. It flushes every 256 blocks now, and says how many it recorded. Both traps are in CLAUDE.md, because neither looks broken from the outside: one presents as a service that is merely restarting, the other as a catch-up that is merely slow. Closes #10 Closes #11 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
69 lines
3.1 KiB
TOML
69 lines
3.1 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 }
|
|
# 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"
|