kernel/share.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! How much of the whole plan one node is, and why nobody can mint more.
7//! The part `crates/kernel/spec/Share.lean` specifies this module.
8//!
9//! A share exists in exactly two ways: it is the whole plan, or it is
10//! one part of a share that was divided. [`Share::split`] takes its
11//! input by value and hands back parts that add up to it exactly, so
12//! **conservation is not a rule that is checked — it is the only thing
13//! the type can express.** There is no constructor, no arithmetic, and
14//! no `Deserialize`: a number read off a file cannot become a share
15//! without being divided out of the whole first.
16//!
17//! That is what settles the estimation problem. A resident that splits
18//! its own branch chooses how its own share is divided and cannot reach
19//! past it, so an over-eager estimate costs its neighbours nothing and
20//! the total stays 1. Capping replaces refereeing.
21//!
22//! Billionths rather than a fraction: two shares compare and add the
23//! same way on every machine, repeated division never grows a
24//! denominator, and no float ever touches a decision path
25//! (ARCHITECTURE.md section 10 rule 6).
26
27use crate::error::{AxCode, AxError};
28
29/// Billionths of the whole plan.
30///
31/// Ordered so a renderer can sort branches by weight without unwrapping
32/// the number first.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
34pub struct Share(u64);
35
36/// The whole plan, in billionths. Public because a reader comparing a
37/// share against the total needs the same constant the split used.
38pub const WHOLE_PPB: u64 = 1_000_000_000;
39
40impl Share {
41 /// The whole plan. The one origin: every other share descends from
42 /// this one by division.
43 pub const WHOLE: Share = Share(WHOLE_PPB);
44
45 /// Nothing at all — what a branch with no leaves has done.
46 ///
47 /// Safe to expose because it cannot grow: no addition exists on this
48 /// type, so a zero cannot be turned into a claim on the plan.
49 pub const NONE: Share = Share(0);
50
51 /// This share in billionths of the whole plan.
52 #[must_use]
53 pub fn ppb(self) -> u64 {
54 self.0
55 }
56
57 /// Divides this share among `weights`, consuming it.
58 ///
59 /// The parts add up to the input exactly. Integer division leaves a
60 /// remainder of fewer billionths than there are parts, and it goes
61 /// to the earliest parts one at a time — a rule rather than a
62 /// rounding, so the same plan divides the same way on every machine.
63 ///
64 /// # Errors
65 /// Refuses an empty list and refuses weights that are all zero:
66 /// both ask for a division with no dividend, and answering them
67 /// would mean either losing the share or inventing a rule about
68 /// where it went.
69 pub fn split(self, weights: &[u32]) -> Result<Vec<Share>, AxError> {
70 let total: u64 = weights.iter().copied().map(u64::from).sum();
71 if weights.is_empty() || total == 0 {
72 return Err(AxError::failure(
73 AxCode::InvalidArgs,
74 "divide a share",
75 format!("{} parts, {total} in weight", weights.len()),
76 )
77 .with_recovery("give at least one part, and at least one weight above zero"));
78 }
79 let mut parts = Vec::with_capacity(weights.len());
80 let mut handed: u64 = 0;
81 for weight in weights {
82 let exact = u128::from(self.0)
83 .saturating_mul(u128::from(*weight))
84 .checked_div(u128::from(total))
85 .unwrap_or(0);
86 let part = u64::try_from(exact).unwrap_or(self.0);
87 parts.push(part);
88 handed = handed.saturating_add(part);
89 }
90 let mut remainder = self.0.saturating_sub(handed);
91 for part in &mut parts {
92 if remainder == 0 {
93 break;
94 }
95 *part = part.saturating_add(1);
96 remainder = remainder.saturating_sub(1);
97 }
98 Ok(parts.into_iter().map(Share).collect())
99 }
100}
101
102/// Adds shares that were divided out of one whole.
103///
104/// A free function rather than an `Add` impl, and it takes the parts
105/// together rather than two at a time: this is how a branch reports what
106/// its leaves came to, not an arithmetic anyone may reach for. The sum
107/// saturates at the whole, which is unreachable for parts of one plan
108/// and is the honest answer for a caller that mixed two.
109#[must_use]
110pub fn gather(parts: &[Share]) -> Share {
111 let sum = parts
112 .iter()
113 .fold(0u64, |held, part| held.saturating_add(part.0));
114 Share(sum.min(WHOLE_PPB))
115}
116
117#[cfg(test)]
118#[allow(
119 clippy::unwrap_used,
120 clippy::expect_used,
121 clippy::panic,
122 clippy::indexing_slicing,
123 clippy::arithmetic_side_effects,
124 reason = "test code"
125)]
126mod tests {
127 use super::*;
128 use proptest::prelude::*;
129
130 #[test]
131 fn the_parts_add_up_to_what_was_divided() {
132 let parts = Share::WHOLE.split(&[1, 1, 1]).unwrap();
133 assert_eq!(parts.len(), 3);
134 assert_eq!(gather(&parts), Share::WHOLE);
135 // The remainder rule, stated in numbers: a billion into three is
136 // not exact, and the extra billionth goes to the first part.
137 assert_eq!(parts[0].ppb(), 333_333_334);
138 assert_eq!(parts[1].ppb(), 333_333_333);
139 assert_eq!(parts[2].ppb(), 333_333_333);
140 }
141
142 #[test]
143 fn weights_are_ratios_and_nothing_else() {
144 let by_ones = Share::WHOLE.split(&[1, 3]).unwrap();
145 let by_hundreds = Share::WHOLE.split(&[100, 300]).unwrap();
146 assert_eq!(by_ones, by_hundreds, "a ratio is not a quantity");
147 assert_eq!(by_ones[1].ppb(), 750_000_000);
148 }
149
150 #[test]
151 fn a_zero_weight_part_gets_nothing_and_the_rest_still_add_up() {
152 let parts = Share::WHOLE.split(&[0, 1]).unwrap();
153 assert_eq!(parts[0], Share::NONE);
154 assert_eq!(gather(&parts), Share::WHOLE);
155 }
156
157 #[test]
158 fn a_division_with_no_dividend_is_refused_rather_than_guessed() {
159 let empty = Share::WHOLE.split(&[]).unwrap_err();
160 assert_eq!(empty.code(), &AxCode::InvalidArgs);
161 let all_zero = Share::WHOLE.split(&[0, 0]).unwrap_err();
162 assert!(all_zero.recovery().contains("above zero"));
163 }
164
165 /// A share smaller than the number of parts still divides: the
166 /// remainder rule hands out what there is and stops, rather than
167 /// rounding something into existence.
168 #[test]
169 fn a_share_too_small_to_go_round_is_still_conserved() {
170 let mut held = Share::WHOLE;
171 // Ten halvings of a billionth-scaled whole leave a share that
172 // cannot be cut three ways evenly.
173 for _ in 0..10 {
174 held = held.split(&[1, 1]).unwrap()[0];
175 }
176 let tiny = held.split(&[1, 1, 1]).unwrap();
177 assert_eq!(gather(&tiny), held);
178 }
179
180 proptest! {
181 /// The property the type exists for: however a plan is cut up,
182 /// and however deep, the parts still add up to the whole.
183 #[test]
184 fn any_sequence_of_splits_conserves_the_whole(
185 plan in proptest::collection::vec(
186 proptest::collection::vec(0u32..8, 1..5),
187 1..6,
188 ),
189 ) {
190 let mut open = vec![Share::WHOLE];
191 for weights in &plan {
192 if weights.iter().copied().map(u64::from).sum::<u64>() == 0 {
193 continue;
194 }
195 let Some(chosen) = open.pop() else { break };
196 let parts = chosen.split(weights).unwrap();
197 prop_assert_eq!(gather(&parts), chosen, "one split conserves its input");
198 open.extend(parts);
199 }
200 prop_assert_eq!(gather(&open), Share::WHOLE, "and so does every split together");
201 }
202 }
203}