Lesson 38 · Risk-Graph Math · Deeper Track

How well-governed is a node?

node_risk_score opened all the way — including how much of it is real today. ~13 min.

Builds on: L13 · L23 · L9 Anchor: multisig thresholds, timelocks, immutability New: min-over-admins vs max-over-channels New: what's a stub today

L13 gave you node_risk_score as "a noisy-OR with two stubs" and moved on. This lesson opens the file. Three payoffs: the exact governance scoring (gorgeously EVM-flavoured), a sharp combinator insight that contrasts with the at_risk math, and an honest reckoning with how much of this field is actually computed today — the kind of thing you must know before you ever build a rule on it.

Your anchor: you already grade governance by structure
Shown a protocol's admin setup, you judge it instantly: a 4-of-7 Gnosis Safe behind a 48-hour timelock is well-governed; a single EOA with upgrade rights is terrifying; a truly immutable contract has no governance risk at all. governance_risk encodes exactly that intuition as a number — and it's the one term in node_risk_score that's fully alive.

1 · The composite — a noisy-OR of four factors

node_risk_score combines four independent risk components as a noisy-OR — the probability that at least one goes wrong (L13's combinator for compounding independent probabilities):

node_risk_score = 1 − (1−gov)(1−lon)(1−audit)(1−firewall)

Each term is a probability in [0,1]; the product of their complements is "nothing goes wrong," so 1 minus that is "something does." Rounded to 4 decimals. Simple — until you look at what the four terms actually are right now.

The reckoning: three of the four are 0.5 stubs
In the live code, audit = 0.5 and firewall = 0.5 are hardcoded constants (L13 flagged these), and longevityRisk() also just returns 0.5 — "RT graph doesn't have creation_timestamp; TODO." So only gov is computed. Substitute: score = 1 − (1−gov)(0.5)(0.5)(0.5) = 1 − 0.125·(1−gov). With gov in its real range [0.1, 0.8], every node scores between 0.887 and 0.975 — a dynamic range of ~0.09, riding high near 1.0. The flagship "composite risk" field is, today, governance nudging a number that's pinned near the ceiling by three placeholders. Know this before you threshold a rule on it. (node_risk_score.go lines 130–133, 390–394.)

2 · governance_risk — and the combinator twist

Now the live term. For a node with admins, governanceRisk scores each admin and keeps the minimum:

bestScore := 1.0
for adminIdx := range adminIdxs {
    score := scoreOneAdmin(admin)        // multisig / timelock / EOA / …
    if score < bestScore { bestScore = score }   // keep the SAFEST admin
}
return bestScore
The insight: the question sets the direction
admin_risk (L13) took the MAX over attack channels — the worst path, because an attacker picks the easiest one. governance_risk takes the MIN over admins — the safest controller, because it asks a different question: "how well-governed is this node?", and a node is considered as well-governed as its best-structured controller. Same engine, opposite direction. Worst-case for "can it be attacked," best-case for "is it well-run." (Worth a critical eye: is min-over-admins the right call when an attacker would target the weakest admin? It's parity-locked to the Python model — a legitimate design question to raise, not a bug to fix.)

The admin scoring table (this is the EVM-rich part)

Admin shapegovernance scoreWhy
Timelock, delay ≥ 48h0.15ample window for users to react to a malicious upgrade
Timelock, delay ≥ 1h / < 1h0.25 / 0.30shorter delay, weaker mitigation
Multisig, k≥3 of n≥50.20strong threshold + signer diversity
Multisig, k≥2 of n≥3 / weaker0.30 / 0.40graded down by threshold and signer count
smart-contract-wallet0.50structured but unclassified
unknown admin type0.60conservative default
EOA admin0.80a single key — the most dangerous controller

And the no-admin branch: if a node has no admin edges and is immutable (is_proxy == false), governance_risk = 0.1 — you can't govern-attack what can't be changed; otherwise 0.5 (unknown). EOA-type nodes are skipped entirely (governance = 0 — "they ARE the key," from L13). isTimelock sniffs four signals: subcategory, a non-zero delay attribute, or "timelock" in the verified contract name / Blockscout nametag.

3 · The architecture around the math (two systems lessons)

Doc vs code, one more time
The field doc string in this very file calls node_risk_score "max across 6 channels: governance, longevity, holdings, admin, oracle, lending." The code is a 4-term noisy-OR (gov · lon · audit · firewall), three of them stubbed. The prose and the code disagree about the operator (max vs noisy-OR), the terms, and the count. As in L13: when the doc and the code conflict, the code is the truth — and here the gap is wide enough to mislead a rule author badly. (Compare line 82's rule:field doc with line 133's formula.)
What you can now defend
You can derive node_risk_score exactly, explain that it's governance-only-live today (compressed to [0.89, 0.975]), contrast min-over-admins with max-over-channels, read the multisig/timelock/immutability tiers off real EVM structure, and explain the partial-graph + changed-only-write machinery that makes computing it affordable. That's the field, end to end — including the parts a field doc would never tell you.

Check yourself

1. node_risk_score = 1 − (1−gov)(1−lon)(1−audit)(1−firewall). What does this noisy-OR express?
2. In the live code, which of the four terms is actually computed from graph data?
3. Given three terms pinned at 0.5, what's the real dynamic range of node_risk_score?
4. governance_risk takes the MIN over a node's admins, while admin_risk (L13) takes the MAX over channels. Why the opposite directions?
5. A node's only admin is a 48-hour timelock. What governance score does it get, and why?
6. A node has no admin edges and is_proxy == false. Its governance_risk is…
7. Why does filterChangedScores read the stored triple and skip unchanged nodes before writing?
8. The in-file doc says node_risk_score is "max across 6 channels"; the code is a 4-term noisy-OR with 3 stubs. The right move?
↳ Ask your teacher
Try: "What real signal would replace the audit / firewall / longevity stubs?" · "Show me AdminsOfIdx — how are a node's admins resolved on the partial graph?" · "Would min-over-admins change if we modelled the weakest admin instead?" · "How does the rules engine consume governance_risk vs node_risk_score?" · "What sets is_proxy, and how reliable is the immutability branch?"

What you can now do

Grounded in: pkg/risk/node_risk_score.go (computeScoresForGraph noisy-OR composite + audit/firewall=0.5, longevityRisk→0.5 stub, governanceRisk min-over-admins + multisig/timelock/EOA tiers + immutable 0.1 branch, isTimelock/timelockScore, eoaNodeTypes gov=0, ComputeForTokenOnGraph partial-graph FORTA-2676, filterChangedScores/readCurrentScores/scoreEpsilon changed-only writes; the line-82 doc vs line-133 code gap). Verify against source — the code is the truth.