app: /api/state never answers {} again (paid_wei over u64::MAX broke to_value; the field is a decimal string, the error is logged once)

serde_json's to_value refuses a u128 over u64::MAX (18.45 IGN) and state_json turned the error into json!({}). A paid shard is 1.23 IGN on average, so a proving machine's dashboard went blank about 15 paid shards after every app start. Unit test over the boundary; 114 app tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 00:27:35 +00:00
parent 047932f2e1
commit 42f36b3fd3
4 changed files with 58 additions and 1 deletions

View file

@ -92,6 +92,8 @@ pub struct Shared {
/// the app log's path: the one the uploader sends (main.rs names it; the engine must not guess the stamp) /// the app log's path: the one the uploader sends (main.rs names it; the engine must not guess the stamp)
pub log_path: PathBuf, pub log_path: PathBuf,
port: std::sync::atomic::AtomicU16, port: std::sync::atomic::AtomicU16,
/// set once `state_json` has logged a serialisation error (one line, not one per poll)
state_error_logged: std::sync::atomic::AtomicBool,
} }
impl Shared { impl Shared {
@ -135,6 +137,7 @@ impl Shared {
engine_log: Mutex::new(engine_log), engine_log: Mutex::new(engine_log),
log_path, log_path,
port: std::sync::atomic::AtomicU16::new(0), port: std::sync::atomic::AtomicU16::new(0),
state_error_logged: std::sync::atomic::AtomicBool::new(false),
} }
} }
@ -177,7 +180,17 @@ impl Shared {
st.now = crate::platform::unix_now_f(); st.now = crate::platform::unix_now_f();
st.uptime_s = self.started.elapsed().as_secs(); st.uptime_s = self.started.elapsed().as_secs();
st.events = self.rings.lock().unwrap().events.iter().cloned().collect(); st.events = self.rings.lock().unwrap().events.iter().cloned().collect();
serde_json::to_value(st).unwrap_or(json!({})) match serde_json::to_value(st) {
Ok(v) => v,
Err(e) => {
// never a silent "{}": the dashboard and every PC playbook read this reply
let msg = format!("state_json: {e}");
if !self.state_error_logged.swap(true, std::sync::atomic::Ordering::Relaxed) {
self.log(&format!("[error] {msg}"));
}
json!({ "error": msg, "version": env!("CARGO_PKG_VERSION") })
}
}
} }
/// A short state for the window host (menu bar, tray). /// A short state for the window host (menu bar, tray).

View file

@ -172,6 +172,9 @@ pub struct ProvingState {
pub submitted: u32, pub submitted: u32,
pub paid: u32, pub paid: u32,
pub failed: u32, pub failed: u32,
/// wei, serialised as a decimal string: serde_json's `to_value` refuses a u128 over u64::MAX (about 18.45 IGN,
/// 15 paid shards at 1.23 IGN), and that refusal emptied the whole `/api/state` reply to "{}" (6 October 2026)
#[serde(serialize_with = "u128_string")]
pub paid_wei: u128, pub paid_wei: u128,
pub current: String, pub current: String,
pub started_at: f64, pub started_at: f64,
@ -401,3 +404,22 @@ impl Rings {
out out
} }
} }
/// A u128 as a decimal JSON string (the dashboard reads it with `Number()`).
pub fn u128_string<S: serde::Serializer>(v: &u128, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&v.to_string())
}
#[cfg(test)]
mod paid_wei_tests {
use super::*;
#[test]
fn a_paid_total_over_u64_max_still_serialises_the_whole_state() {
let mut st = State::default();
st.proving.paid_wei = u64::MAX as u128 + 1;
let v = serde_json::to_value(&st).expect("the state serialises");
assert_eq!(v["proving"]["paid_wei"], serde_json::Value::String("18446744073709551616".into()));
assert!(v["mining"].is_object());
}
}

View file

@ -1694,3 +1694,18 @@ Reading, with the miner-on pairs above (empty shard 15,670 MiB, full prototype s
|---|---| |---|---|
| Unit tests | `cargo test --release -p kaspa-consensus-core -p igneum-exec --lib -- proving config::params::tests::override_params_carry_the_proving_v1 config::params::tests::consensus_digest` on this Mac (target `vendor/igneum-node/target-pv1`, 19:09Z): consensus core 13 passed (the segment record round trip, signature and the three nested sections; the credit split; the params switch and the digest that moves only once the switch is set), exec 8 passed (the segment grid and the split; the record checks: alignment, block, chain length, the veto naming the field, the deadline, the window; the chain rule both ways; the unproven restart; the shard side at 90%; the pool offering the segment section). The six full node suites go to PC 2 as a build job when the fleet is back | | Unit tests | `cargo test --release -p kaspa-consensus-core -p igneum-exec --lib -- proving config::params::tests::override_params_carry_the_proving_v1 config::params::tests::consensus_digest` on this Mac (target `vendor/igneum-node/target-pv1`, 19:09Z): consensus core 13 passed (the segment record round trip, signature and the three nested sections; the credit split; the params switch and the digest that moves only once the switch is set), exec 8 passed (the segment grid and the split; the record checks: alignment, block, chain length, the veto naming the field, the deadline, the window; the chain rule both ways; the unproven restart; the shard side at 90%; the pool offering the segment section). The six full node suites go to PC 2 as a build job when the fleet is back |
| The fast-time 3-node harness (`tools/proving-v1/net.mjs`, 29950+, suffix 956, every node in trust mode, three vmine voters, v0 at DAA 60, v1 at DAA 120, 4 blocks a segment, unproven after 60 DAA, a tenth to the aggregator; fork b177718e built on this Mac) | run 2, 19:13:01Z to 19:16:19Z, under the run lock: PASSED, 21 checks in 197.3 s (`tools/proving-v1/report-2026-10-05.json`). v1 start = chain block 119 on all three nodes; the native statement identical on all three. Known-finished: segment 119..122's fresh-chain record submitted to n1 at t=131.1 s, relayed, verified (trust) and PAID on n0 1.0 s later at chain block 129, 253,611,648,000,000,000 wei = a tenth of the four credits, the same on every node, the payout address holding it. Chain rule: segment 123..126's fresh-chain record refused ("does not chain to segment 119..122 ... proven (record paid at chain block 129)"), the continuing one (chain_len 8) accepted and paid. Known-failed: segment 127..130 left without a record: a fresh-chain record for 131..134 refused while 127..130 was pending ("pending until DAA 191"); at DAA 192 the status read unproven, a late record for 127..130 refused ("unproven: carried after the deadline"), the fresh-chain record for 131..134 accepted and paid with chain_len 4; `segmentsInWindow` proven 3, unproven 1. The shard side: a v1 shard's `shardWei` = 90% of its block's credit. Run 1 (19:10Z) failed in its own tooling (the signer's argument order), fixed. Run 3 on the FINAL fork tree (ece42979 on the 0.3.10 commit 21d4c73c, protocol 15, N = 8 both in the params default and `--segment 8`, the fast-time file's four fields), 20:52:41Z to 20:56:45Z: PASSED, 21 checks in 244.4 s (segments of 8: 119..126 paid in 1.0 s after submission, 127..134 refused fresh and paid continuing with chain_len 16, 135..142 left unproven and skipped, 143..150 restarted the chain) | | The fast-time 3-node harness (`tools/proving-v1/net.mjs`, 29950+, suffix 956, every node in trust mode, three vmine voters, v0 at DAA 60, v1 at DAA 120, 4 blocks a segment, unproven after 60 DAA, a tenth to the aggregator; fork b177718e built on this Mac) | run 2, 19:13:01Z to 19:16:19Z, under the run lock: PASSED, 21 checks in 197.3 s (`tools/proving-v1/report-2026-10-05.json`). v1 start = chain block 119 on all three nodes; the native statement identical on all three. Known-finished: segment 119..122's fresh-chain record submitted to n1 at t=131.1 s, relayed, verified (trust) and PAID on n0 1.0 s later at chain block 129, 253,611,648,000,000,000 wei = a tenth of the four credits, the same on every node, the payout address holding it. Chain rule: segment 123..126's fresh-chain record refused ("does not chain to segment 119..122 ... proven (record paid at chain block 129)"), the continuing one (chain_len 8) accepted and paid. Known-failed: segment 127..130 left without a record: a fresh-chain record for 131..134 refused while 127..130 was pending ("pending until DAA 191"); at DAA 192 the status read unproven, a late record for 127..130 refused ("unproven: carried after the deadline"), the fresh-chain record for 131..134 accepted and paid with chain_len 4; `segmentsInWindow` proven 3, unproven 1. The shard side: a v1 shard's `shardWei` = 90% of its block's credit. Run 1 (19:10Z) failed in its own tooling (the signer's argument order), fixed. Run 3 on the FINAL fork tree (ece42979 on the 0.3.10 commit 21d4c73c, protocol 15, N = 8 both in the params default and `--segment 8`, the fast-time file's four fields), 20:52:41Z to 20:56:45Z: PASSED, 21 checks in 244.4 s (segments of 8: 119..126 paid in 1.0 s after submission, 127..134 refused fresh and paid continuing with chain_len 16, 135..142 left unproven and skipped, 143..150 restarted the chain) |
### 6 October 2026, 00:4xZ, the empty `/api/state` reply (proving v1 branch)
Reported by the aggregation-cost agent: PC 2's `/api/state` answered `{}` (2 bytes) at 22:22Z, 22:41Z and 00:18Z. Not measured on PC 2 (no job); derived from the app source and node 1's RPC, read-only on the Mac:
| Figure | Value | Source |
|---|---|---|
| Paid shards, devnet, all provers | 663 | `curl -s 127.0.0.1:26790 -d '{"jsonrpc":"2.0","id":1,"method":"igneum_getProvingStatus","params":[]}'` at tip DAA 0x22caf |
| Paid wei, all provers | 0x2c2961a69990745400 = 814.64 IGN | same call |
| Average per paid shard | 1.23 IGN (approximate: the mean over 663) | 814.64 / 663 |
| u64::MAX in IGN | 18.45 | 2^64 - 1 over 1e18 |
| Paid shards per app start before the reply empties | 15 (approximate: at the mean payout) | 18.45 / 1.23 |
Cause: `ProvingState.paid_wei: u128` and serde_json `to_value` (1.0.151, `value/ser.rs` `serialize_u128`: u64 range or an error); the error became `json!({})`. Fix: the field serialises as a decimal string; `state_json` logs the error once. Test `a_paid_total_over_u64_max_still_serialises_the_whole_state` (`cargo test --offline -q paid_wei`, 1 passed).

View file

@ -143,3 +143,10 @@ The aggregation-cost agent's first rows (branch agg-cost, 5 October 2026 night,
The re-plans of block 344 at 2.25 M and 4.5 M pgas peak at 28.3 to 28.4 GB alone (the server's buffers step up between 4.7 M and 20 M cycles and are flat to 60 M), so no shard size between the v1 budget and the prototype one changes a tier; with the miner the adopted shard proves 3.1x slower (13.2 s against 4.2 s) and the chained aggregation 9.7 s against 2.5 s: a mining 24 GB card delivers one adopted-size shard plus one aggregation in about 23 s, inside T by 25x. The re-plans of block 344 at 2.25 M and 4.5 M pgas peak at 28.3 to 28.4 GB alone (the server's buffers step up between 4.7 M and 20 M cycles and are flat to 60 M), so no shard size between the v1 budget and the prototype one changes a tier; with the miner the adopted shard proves 3.1x slower (13.2 s against 4.2 s) and the chained aggregation 9.7 s against 2.5 s: a mining 24 GB card delivers one adopted-size shard plus one aggregation in about 23 s, inside T by 25x.
## The empty `/api/state` reply (6 October 2026)
The aggregation-cost agent's jobs read the two bytes `{}` from `/api/state` on PC 2 at 22:22Z, 22:41Z and 00:18Z (0.3.10 and 0.3.11); the 21:01Z reply was full. Cause, from the app source and node 1's RPC: `ProvingState.paid_wei` is a `u128`, and serde_json's `to_value` refuses a u128 over u64::MAX (18,446,744,073,709,551,615 wei, 18.45 IGN); `state_json()` turned that refusal into `json!({})` with no log line. A paid shard is 1.23 IGN on average (node 1, `igneum_getProvingStatus`: 814.64 IGN over 663 shards at 00:3xZ), so the fifteenth paid shard after an app start empties the reply. PC 2's prover was blind to the root-owned socket from 20:00:56Z to 22:01Z (paid_wei stayed 0, hence the full reply at 21:01Z), proved from 22:02:13Z, and crossed 18.45 IGN inside its first 15 paid shards, before 22:22Z. Every app restart resets the counter, so the reply comes back for about 15 shards and goes again.
What it means: the dashboard on a proving machine shows nothing within about 12 minutes of its prover's first payout; every PC playbook that reads a card from `/api/state` fails the same way (the agent's job 5 reads settings.json instead). Mining, proving and payouts are untouched; it is the status page only.
Fix on the app branch: `paid_wei` serialises as a decimal string (the dashboard already reads it with `Number()`), `state_json` logs `[error] state_json: ...` once instead of answering `{}`, and the reply on any future serialisation error carries `error` and `version` rather than nothing; unit test `a_paid_total_over_u64_max_still_serialises_the_whole_state`. Ships with 0.3.11 if the shipper takes the new code tip, otherwise 0.3.12; until then the workaround is settings.json for the card keys.