fleetmesh-core 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fleetmesh_core/__init__.py +10 -0
- fleetmesh_core/accounting/__init__.py +7 -0
- fleetmesh_core/accounting/accounting.py +215 -0
- fleetmesh_core/accounting/capacity.py +142 -0
- fleetmesh_core/accounting/cashu.py +661 -0
- fleetmesh_core/accounting/market.py +111 -0
- fleetmesh_core/accounting/paidfetch.py +97 -0
- fleetmesh_core/accounting/standing.py +210 -0
- fleetmesh_core/accounting/strain.py +124 -0
- fleetmesh_core/accounting/window.py +425 -0
- fleetmesh_core/conformance/__init__.py +7 -0
- fleetmesh_core/conformance/__main__.py +6 -0
- fleetmesh_core/conformance/conformance.py +124 -0
- fleetmesh_core/conformance/conformance_ops.py +724 -0
- fleetmesh_core/conformance/errors.py +24 -0
- fleetmesh_core/conformance/harness.py +64 -0
- fleetmesh_core/crypto/__init__.py +7 -0
- fleetmesh_core/crypto/bech32.py +74 -0
- fleetmesh_core/crypto/crypto.py +204 -0
- fleetmesh_core/crypto/curve.py +236 -0
- fleetmesh_core/crypto/giftwrap.py +169 -0
- fleetmesh_core/crypto/http_signatures.py +250 -0
- fleetmesh_core/crypto/keys.py +89 -0
- fleetmesh_core/crypto/nip44.py +144 -0
- fleetmesh_core/crypto/ssb.py +562 -0
- fleetmesh_core/crypto/zk.py +812 -0
- fleetmesh_core/identity/__init__.py +7 -0
- fleetmesh_core/identity/atproto.py +509 -0
- fleetmesh_core/identity/cid.py +199 -0
- fleetmesh_core/identity/did.py +431 -0
- fleetmesh_core/identity/didcomm.py +285 -0
- fleetmesh_core/identity/identity.py +326 -0
- fleetmesh_core/identity/iroh_ticket.py +111 -0
- fleetmesh_core/identity/origin.py +42 -0
- fleetmesh_core/identity/relaylist.py +75 -0
- fleetmesh_core/identity/web_auth.py +518 -0
- fleetmesh_core/peering/__init__.py +7 -0
- fleetmesh_core/peering/badpeer.py +182 -0
- fleetmesh_core/peering/complaint.py +181 -0
- fleetmesh_core/peering/coverage.py +487 -0
- fleetmesh_core/peering/peering.py +592 -0
- fleetmesh_core/peering/projection.py +362 -0
- fleetmesh_core/peering/release.py +219 -0
- fleetmesh_core/peering/reports.py +306 -0
- fleetmesh_core/services/__init__.py +3 -0
- fleetmesh_core/services/discover.py +89 -0
- fleetmesh_core/services/discovery.py +191 -0
- fleetmesh_core/services/dvm.py +614 -0
- fleetmesh_core/services/lifecycle.py +200 -0
- fleetmesh_core/services/nsite.py +327 -0
- fleetmesh_core/services/telemetry.py +89 -0
- fleetmesh_core/services/unplug.py +187 -0
- fleetmesh_core/storage/__init__.py +7 -0
- fleetmesh_core/storage/blossom_auth.py +117 -0
- fleetmesh_core/storage/car.py +139 -0
- fleetmesh_core/storage/linked_data.py +346 -0
- fleetmesh_core/storage/mst.py +498 -0
- fleetmesh_core/storage/negentropy.py +394 -0
- fleetmesh_core/storage/projection.py +630 -0
- fleetmesh_core/storage/replication.py +187 -0
- fleetmesh_core/storage/store.py +824 -0
- fleetmesh_core/storage/store_postgres.py +283 -0
- fleetmesh_core/storage/sync.py +478 -0
- fleetmesh_core/wire/__init__.py +3 -0
- fleetmesh_core/wire/cbor.py +223 -0
- fleetmesh_core/wire/interfaces.py +629 -0
- fleetmesh_core/wire/p2panda.py +409 -0
- fleetmesh_core/wire/schema_files.py +93 -0
- fleetmesh_core/wire/spelling.py +37 -0
- fleetmesh_core/wire/validate.py +618 -0
- fleetmesh_core/wire/varint.py +62 -0
- fleetmesh_core/wire/video_transit.py +541 -0
- fleetmesh_core/wire/wire_types.py +564 -0
- fleetmesh_core-0.2.0.dist-info/METADATA +70 -0
- fleetmesh_core-0.2.0.dist-info/RECORD +78 -0
- fleetmesh_core-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_core-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_core-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""fleetmesh executable reference implementation.
|
|
2
|
+
|
|
3
|
+
Standard library only, no I/O, no clock, no network, and every constant read
|
|
4
|
+
from schema/. Contains reference mechanisms for Nostr event storage,
|
|
5
|
+
reconciliation, peering, accounting, capacity management, and reporting.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__version__ = '0.2.0'
|
|
10
|
+
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Receipts, their hash chain, and telling a dead peer from a lying one.
|
|
2
|
+
|
|
3
|
+
The chain is what stops a subject rewriting an earlier claim once it learns what
|
|
4
|
+
the issuer measured. The issuer keeps the head, which is 32 bytes per window,
|
|
5
|
+
and the most recent body, because that body is the claim the window is
|
|
6
|
+
evaluated against and the evidence any report about it must carry.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
from ..crypto import crypto
|
|
14
|
+
|
|
15
|
+
from ..wire import schema_files
|
|
16
|
+
from ..wire.interfaces import ReceiptBody
|
|
17
|
+
|
|
18
|
+
CONSTANTS = schema_files.load_constants()
|
|
19
|
+
|
|
20
|
+
ZERO: str = '00' * 32
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def canonical(receipt: Mapping[str, Any]) -> bytes:
|
|
24
|
+
"""constants.json canonical.form: keys sorted by code point, no whitespace -
|
|
25
|
+
crypto.canonical, with one difference:
|
|
26
|
+
|
|
27
|
+
`sig` is excluded, because it is a signature over this. Excluding it means
|
|
28
|
+
the chain link is the same value whether or not the receipt has been signed
|
|
29
|
+
yet, so signing changes no head and breaks no chain.
|
|
30
|
+
"""
|
|
31
|
+
return crypto.canonical({k: v for k, v in receipt.items() if k != 'sig'})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def link(receipt: Mapping[str, Any]) -> str:
|
|
35
|
+
return hashlib.sha256(canonical(receipt)).hexdigest()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def sign_receipt(body: ReceiptBody, sk_hex: str) -> ReceiptBody:
|
|
39
|
+
"""The subject signs its own claim. BIP-340 over the chain link, which is
|
|
40
|
+
already SHA-256 of the canonical body, so no second construction is defined
|
|
41
|
+
and the head the issuer acknowledges is the message that was signed.
|
|
42
|
+
|
|
43
|
+
This is what makes a receipt evidence. Without it a report about divergence
|
|
44
|
+
cites a number only the reporter has ever seen the subject state, which is
|
|
45
|
+
hearsay; with it the subject's half of the disagreement is pinned and the
|
|
46
|
+
reporter is trusted only for its own measurement.
|
|
47
|
+
"""
|
|
48
|
+
signed: ReceiptBody = {**body, 'sig': crypto.schnorr_sign(bytes.fromhex(link(body)), sk_hex)}
|
|
49
|
+
return signed
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def verify_receipt(body: Mapping[str, Any], subject_pubkey: str) -> bool:
|
|
53
|
+
"""True when this receipt carries the subject's signature over its own link."""
|
|
54
|
+
sig = body.get('sig')
|
|
55
|
+
if not sig:
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
return crypto.schnorr_verify(bytes.fromhex(link(body)), subject_pubkey, sig)
|
|
59
|
+
except (ValueError, TypeError):
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_chain(grant_id: str, window: int, counter_series: Sequence[Mapping[str, int]], start_at: int = 1787000000, step: int = 600) -> Mapping[str, Any]:
|
|
64
|
+
"""Produce a well-formed chain. Counters are cumulative and monotonic."""
|
|
65
|
+
chain = []
|
|
66
|
+
prev = ZERO
|
|
67
|
+
for seq, counters in enumerate(counter_series):
|
|
68
|
+
body = {'grant': grant_id, 'window': window, 'seq': seq, 'prev': prev,
|
|
69
|
+
'counters': dict(counters), 'at': start_at + seq * step}
|
|
70
|
+
h = link(body)
|
|
71
|
+
chain.append({'receipt': body, 'canonical': canonical(body).decode(), 'sha256': h})
|
|
72
|
+
prev = h
|
|
73
|
+
return {'chain': chain, 'head': prev}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def verify_chain(chain: Sequence[Mapping[str, Any]], head: Optional[str] = None) -> list[str]:
|
|
77
|
+
"""Check every link, sequence number and monotonicity claim.
|
|
78
|
+
|
|
79
|
+
Returns a list of failures; empty means the chain holds. A subject that
|
|
80
|
+
rewrote seq 3 after the fact cannot also produce a matching seq 4.
|
|
81
|
+
|
|
82
|
+
This is strict about sequence, and stays strict, because what it checks is
|
|
83
|
+
a *complete* chain carried as evidence: a report cites what its subject
|
|
84
|
+
signed, and a chain with a hole in it is not the artefact it claims to be.
|
|
85
|
+
Live intake is the other case and is deliberately not this one -
|
|
86
|
+
`node.budget.on_receipt` tolerates a gap, because there the missing link is
|
|
87
|
+
the wire rather than the subject, and refusing it costs a whole window.
|
|
88
|
+
"""
|
|
89
|
+
out: list[str] = []
|
|
90
|
+
prev = ZERO
|
|
91
|
+
last: Optional[Mapping[str, int]] = None
|
|
92
|
+
for i, entry in enumerate(chain):
|
|
93
|
+
r = entry['receipt']
|
|
94
|
+
if r.get('seq') != i:
|
|
95
|
+
out.append('link %d: seq is %s' % (i, r.get('seq')))
|
|
96
|
+
if r.get('prev') != prev:
|
|
97
|
+
out.append('link %d: prev does not chain' % i)
|
|
98
|
+
if entry.get('canonical') is not None and entry['canonical'] != canonical(r).decode():
|
|
99
|
+
out.append('link %d: canonical form does not match the body' % i)
|
|
100
|
+
h = link(r)
|
|
101
|
+
if entry.get('sha256') and entry['sha256'] != h:
|
|
102
|
+
out.append('link %d: stated hash is wrong' % i)
|
|
103
|
+
if last is not None:
|
|
104
|
+
for k, v in r.get('counters', {}).items():
|
|
105
|
+
if k in last and v < last[k]:
|
|
106
|
+
out.append('link %d: counter %s went backwards' % (i, k))
|
|
107
|
+
last = r.get('counters', {})
|
|
108
|
+
prev = h
|
|
109
|
+
if head is not None and prev != head:
|
|
110
|
+
out.append('head does not match the final link')
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def admissible_prefix(receipts: Sequence[Mapping[str, Any]]) -> tuple[list[Mapping[str, Any]], bool]:
|
|
115
|
+
"""The longest gap-free, chain-verified prefix of a stored receipt list,
|
|
116
|
+
and whether anything was cut.
|
|
117
|
+
|
|
118
|
+
Live intake (`node.budget.on_receipt`) tolerates a lost link: a receipt
|
|
119
|
+
at seq 5 after seq 3 is stored, because the hole is the wire's and
|
|
120
|
+
refusing it costs a window. `verify_chain` is strict for the same reason
|
|
121
|
+
in the opposite direction: a chain cited as evidence must be the artefact
|
|
122
|
+
the subject signed, whole. Reporter used to attach every stored body, so
|
|
123
|
+
an S2 about a peer that lost one receipt on the wire carried a chain a
|
|
124
|
+
verifier would reject on sight - and a report with inadmissible evidence
|
|
125
|
+
is discarded unread. This keeps what verifies, from seq 0 up to the first
|
|
126
|
+
hole or broken link, so what is published is always admissible; partial
|
|
127
|
+
is reported so the caller can say so.
|
|
128
|
+
"""
|
|
129
|
+
kept: list[Mapping[str, Any]] = []
|
|
130
|
+
prev = ZERO
|
|
131
|
+
last: Optional[Mapping[str, int]] = None
|
|
132
|
+
for i, r in enumerate(receipts):
|
|
133
|
+
if r.get('seq') != i or r.get('prev') != prev:
|
|
134
|
+
break
|
|
135
|
+
if last is not None and any(k in last and v < last[k] for k, v in r.get('counters', {}).items()):
|
|
136
|
+
break
|
|
137
|
+
kept.append(dict(r))
|
|
138
|
+
prev = link(r)
|
|
139
|
+
last = r.get('counters', {})
|
|
140
|
+
return kept, len(kept) != len(receipts)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def silence(measured: Mapping[str, int]) -> Mapping[str, Any]:
|
|
144
|
+
"""Active or inactive silence, per the partition-tolerance rule.
|
|
145
|
+
|
|
146
|
+
Receipts missing while the peer moved bytes is a misreport. Receipts missing
|
|
147
|
+
while it moved nothing is a partition, and FLP says you cannot tell a dead
|
|
148
|
+
node from a quiet one - so the protocol does not try, and prices only the
|
|
149
|
+
case where physical traffic contradicts the silence.
|
|
150
|
+
"""
|
|
151
|
+
egress = sum(v for k, v in measured.items()
|
|
152
|
+
if k in ('events_in', 'bytes_in', 'bytes_out', 'req', 'sync_ranges'))
|
|
153
|
+
if egress > 0:
|
|
154
|
+
return {'state': 'active-silence', 'severity': 'S2',
|
|
155
|
+
'remedy': 'limit x %s, tier to probation, report published'
|
|
156
|
+
% CONSTANTS['grant']['aimd_decrease_s2']}
|
|
157
|
+
return {'state': 'inactive-silence', 'severity': None,
|
|
158
|
+
'remedy': 'not evaluated, streak neither advanced nor broken, no penalty, no report'}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def window_claim(receipts: Sequence[Mapping[str, Any]], measured: Mapping[str, int]) -> tuple[str, Optional[dict[str, int]]]:
|
|
162
|
+
"""What a window is evaluated against, per constants.json grant.window_evaluation.
|
|
163
|
+
|
|
164
|
+
receipts are the bodies received this window, in order. The most recent one
|
|
165
|
+
is the claim: counters are cumulative, so it already states the whole window,
|
|
166
|
+
and the chain makes it binding. A subject that sent receipts and then went
|
|
167
|
+
quiet is evaluated on what it last said against what was measured after -
|
|
168
|
+
which is how a leaf that syncs and then sleeps produces no finding, and a
|
|
169
|
+
peer that kept moving bytes after its last receipt is priced by divergence.
|
|
170
|
+
Returns (state, counters): ('claim', counters), ('active-silence', None) or
|
|
171
|
+
('not-evaluated', None).
|
|
172
|
+
|
|
173
|
+
None means there is no claim, not that there is nothing to evaluate. A
|
|
174
|
+
window measured with no receipt is still evaluated - against the empty
|
|
175
|
+
claim, which is what vectors/window.json's "no receipt at all" case does -
|
|
176
|
+
and active silence is a *floor* of S2 on top of that. Skipping the
|
|
177
|
+
arithmetic would price a peer that overran and said nothing below the same
|
|
178
|
+
peer reporting it honestly, which is the inversion docs/mechanisms.md § 9
|
|
179
|
+
describes for divergence, in its other form.
|
|
180
|
+
"""
|
|
181
|
+
if receipts:
|
|
182
|
+
return 'claim', dict(receipts[-1]['counters'])
|
|
183
|
+
return ('active-silence' if silence(measured)['severity'] else 'not-evaluated'), None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def close_is_final(close_at: int, window_end: int, window_seconds: int) -> bool:
|
|
187
|
+
"""A close arriving within the grace period is accepted as the window's final
|
|
188
|
+
receipt; later than that it is evaluated like any other receipt.
|
|
189
|
+
|
|
190
|
+
The grace span is half-open like the window itself: [window_end,
|
|
191
|
+
window_end + grace). The evaluator fires once `now >= window_end + grace`
|
|
192
|
+
(node budget/windows.py), so a close stamped at exactly that instant
|
|
193
|
+
arrives at or after evaluation and is late, not final."""
|
|
194
|
+
return close_at < window_end + window_grace(window_seconds)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def epoch(unix_seconds: int, window_seconds: int) -> int:
|
|
198
|
+
"""floor(t / window). Derived from the clock, so it cannot pause.
|
|
199
|
+
|
|
200
|
+
A dormant span advances the epoch counter and simply is not evaluated:
|
|
201
|
+
the streak neither advances nor breaks, and the next active window starts
|
|
202
|
+
a fresh chain at seq 0. The window is [epoch * window, (epoch+1) * window).
|
|
203
|
+
"""
|
|
204
|
+
return unix_seconds // window_seconds
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def window_bounds(epoch_index: int, window_seconds: int) -> tuple[int, int]:
|
|
208
|
+
return (epoch_index * window_seconds, (epoch_index + 1) * window_seconds)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def window_grace(window_seconds: int) -> int:
|
|
212
|
+
"""Clock-skew allowance at window close, in whole seconds (floored, so
|
|
213
|
+
the allowance never exceeds the published fraction)."""
|
|
214
|
+
return int(window_seconds * CONSTANTS['grant']['window_grace_fraction'])
|
|
215
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Reference implementation of the node's own ceiling and the shed ladder.
|
|
3
|
+
|
|
4
|
+
Standard library only. Every constant is read from schema/constants.json rather
|
|
5
|
+
than hardcoded, so this file and the protocol version digest cannot disagree.
|
|
6
|
+
|
|
7
|
+
A grant bounds one peer. Nothing bounded the node. `Meter` is per peer and the
|
|
8
|
+
tick loops them, so twenty anchor peerings each perfectly inside its grant is
|
|
9
|
+
960,000 events an hour with nobody at fault anywhere: every branch within its
|
|
10
|
+
limit, and the trunk with none. That is the fragility this file answers.
|
|
11
|
+
|
|
12
|
+
Two decisions worth stating here rather than leaving to a reader.
|
|
13
|
+
|
|
14
|
+
**The total is the operator's number and this file states no default.** The
|
|
15
|
+
specification says connectivity and not capability is what separates one
|
|
16
|
+
deployment from another, so a Raspberry Pi and a forty-core gateway both declare
|
|
17
|
+
anchor and no per-class table could be honest about either. What is normative is
|
|
18
|
+
the ladder. A node with no declared total measures its pressure and reports it
|
|
19
|
+
and sheds nothing, because refusing an honest peer against a limit nobody chose
|
|
20
|
+
is worse than the overload it would be avoiding. `suggest` exists so an operator
|
|
21
|
+
has a number to start from rather than a blank.
|
|
22
|
+
|
|
23
|
+
**Shedding is announced, and it is not adverse.** Every rung says what it is
|
|
24
|
+
doing - a decline code, a republished coverage claim, an amend carrying
|
|
25
|
+
`cause: capacity`. A node that shed quietly would be read as partitioned, which
|
|
26
|
+
is the one conclusion this protocol keeps having to unlearn, and a subject that
|
|
27
|
+
read a capacity shed as a remedy would price its issuer for owning a small
|
|
28
|
+
machine.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from typing import Iterable, Mapping, Optional, Sequence
|
|
33
|
+
|
|
34
|
+
from . import window
|
|
35
|
+
|
|
36
|
+
from ..wire import schema_files# window.as_limits: a grant carries integers
|
|
37
|
+
from ..wire.interfaces import CapacitySummary
|
|
38
|
+
|
|
39
|
+
C = schema_files.load_constants()['capacity']
|
|
40
|
+
|
|
41
|
+
COUNTERS: tuple[str, ...] = tuple(C['counters'])
|
|
42
|
+
LADDER: tuple[tuple[float, str], ...] = tuple((r['at'], r['action']) for r in C['shed'])
|
|
43
|
+
ACTIONS: tuple[str, ...] = tuple(a for _, a in LADDER)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def pressure(measured: Mapping[str, int | float], total: Mapping[str, int | float]) -> tuple[float, Optional[str]]:
|
|
47
|
+
"""(fraction, binding counter) for one window's aggregate against a total.
|
|
48
|
+
|
|
49
|
+
The fraction is the max over counters, not a mean: a node that has run out
|
|
50
|
+
of one thing has run out. `total` is a dict of the same counter names; a
|
|
51
|
+
counter the operator did not declare is not measured, and a total of zero or
|
|
52
|
+
less is not a declaration.
|
|
53
|
+
"""
|
|
54
|
+
worst: float = 0.0
|
|
55
|
+
binding: Optional[str] = None
|
|
56
|
+
for name in COUNTERS:
|
|
57
|
+
cap = total.get(name)
|
|
58
|
+
if not cap or cap <= 0:
|
|
59
|
+
continue
|
|
60
|
+
share = measured.get(name, 0) / float(cap)
|
|
61
|
+
if share > worst:
|
|
62
|
+
worst, binding = share, name
|
|
63
|
+
return worst, binding
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def rungs(p: float) -> list[str]:
|
|
67
|
+
"""Every shed action engaged at pressure p, in ladder order."""
|
|
68
|
+
return [a for at, a in LADDER if p >= at]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def engaged(p: float, held: Sequence[str] = ()) -> list[str]:
|
|
72
|
+
"""The rungs a node should be holding now, given what it holds already.
|
|
73
|
+
|
|
74
|
+
Rungs engage each at its own threshold and release together below
|
|
75
|
+
`restore_below`. Releasing each at the threshold that engaged it would flap
|
|
76
|
+
a node sitting on a boundary, and each flap costs its peers an amend.
|
|
77
|
+
"""
|
|
78
|
+
if p < C['restore_below']:
|
|
79
|
+
return []
|
|
80
|
+
now = set(rungs(p)) | set(held)
|
|
81
|
+
return [a for a in ACTIONS if a in now]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def shed_floor(probation: Mapping[str, int | float]) -> dict[str, int]:
|
|
85
|
+
"""The lowest a capacity shed may take a limit, from the probation ceilings.
|
|
86
|
+
|
|
87
|
+
Deliberately not the probation ceiling itself. A peering opens at that
|
|
88
|
+
ceiling, so a floor there would make the amend_down rung do nothing at all
|
|
89
|
+
for exactly the peers a busy node has most of - declared and never read,
|
|
90
|
+
which is a shape this specification keeps finding in itself. Deliberately
|
|
91
|
+
not zero either: zero for 24 hours is what S3 does to an offender, and a
|
|
92
|
+
shed is not a remedy. What is left is enough for a receipt and a trickle,
|
|
93
|
+
which is what keeps a small peering accountable rather than silent.
|
|
94
|
+
"""
|
|
95
|
+
f: float = C['shed_floor_fraction']
|
|
96
|
+
# Floored, like every other limit. A fractional floor and an integer result
|
|
97
|
+
# do not compare the way anyone expects: max(335544.3, x) written into a
|
|
98
|
+
# grant becomes 335544, which is below the floor it was just held to.
|
|
99
|
+
return window.as_limits({k: v * f for k, v in probation.items()})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def reduce(limits: Mapping[str, int | float], floor: Mapping[str, int | float], fraction: Optional[float] = None) -> dict[str, int]:
|
|
103
|
+
"""Scale a grant's limits for a capacity shed, never below the floor.
|
|
104
|
+
|
|
105
|
+
A capacity shed makes a peer smaller and never makes it a stranger: the tier
|
|
106
|
+
does not move and the peering is not ended. That is what separates it from
|
|
107
|
+
an S2, which is the ladder's business and not this file's. Pass the floor
|
|
108
|
+
from `shed_floor`.
|
|
109
|
+
"""
|
|
110
|
+
f = C['amend_down_fraction'] if fraction is None else fraction
|
|
111
|
+
return window.as_limits({k: max(floor.get(k, 0), v * f) if k in floor else v * f
|
|
112
|
+
for k, v in limits.items()})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def suggest(windows: Iterable[Mapping[str, int | float]]) -> dict[str, int | float]:
|
|
116
|
+
"""A total to start from, per counter: the largest window observed.
|
|
117
|
+
|
|
118
|
+
`windows` is an iterable of aggregate measurements, one per closed window.
|
|
119
|
+
This is a report of what the machine has already carried and not a claim
|
|
120
|
+
about what it can. An operator raises it deliberately or lowers it after
|
|
121
|
+
watching the box, and either way the number is theirs.
|
|
122
|
+
"""
|
|
123
|
+
out: dict[str, int | float] = {}
|
|
124
|
+
for w in windows:
|
|
125
|
+
for name in COUNTERS:
|
|
126
|
+
v = w.get(name, 0)
|
|
127
|
+
if v > out.get(name, 0):
|
|
128
|
+
out[name] = v
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def summary(measured: Mapping[str, int | float], total: Mapping[str, int | float]) -> CapacitySummary:
|
|
133
|
+
"""What the health block prints. Undeclared is a state, not a zero."""
|
|
134
|
+
if not any(total.get(n) for n in COUNTERS):
|
|
135
|
+
return {'declared': False, 'pressure': None, 'binding': None,
|
|
136
|
+
'rungs': [], 'measured': {k: measured.get(k, 0) for k in COUNTERS
|
|
137
|
+
if measured.get(k)}}
|
|
138
|
+
p, binding = pressure(measured, total)
|
|
139
|
+
return {'declared': True, 'pressure': round(p, 4), 'binding': binding,
|
|
140
|
+
'rungs': rungs(p),
|
|
141
|
+
'measured': {k: measured.get(k, 0) for k in COUNTERS if measured.get(k)}}
|
|
142
|
+
|