Skip to main content

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}