Skip to main content

hashiverse_lib/protocol/posting/
amplification.rs

1//! # Post PoW amplification rules
2//!
3//! A single function — [`get_minimum_post_pow`] — that decides how many bits of
4//! proof-of-work a post requires before a server will accept it. The base level comes
5//! from [`crate::tools::config::POW_MINIMUM_PER_POST`], on top of which the
6//! amplification is:
7//!
8//! - **Size**: log-scale above 1024 bytes. Longer posts cost more — fair for storage,
9//!   punishes spam-generated wall-of-text content.
10//! - **Fan-out**: posts that link a large number of `base_id`s (hashtags, mentions,
11//!   replies) scale quadratically. Main lever against hashtag spam farms that link
12//!   hundreds of trending tags from one post.
13//! - **Bucket granularity**: submissions into tighter (shorter-duration) buckets need
14//!   more PoW than broader ones. Hot, fine-grained buckets are the most valuable
15//!   attack surface, so they get the most expensive admission control.
16
17use crate::tools::config;
18use crate::tools::time::{DurationMillis, MILLIS_IN_MINUTE, MILLIS_IN_MONTH};
19use crate::tools::types::Pow;
20
21pub fn get_minimum_post_pow(post_length: usize, linked_base_ids_len: usize, duration: DurationMillis) -> Pow {
22    assert!(duration > MILLIS_IN_MINUTE);
23
24    let pow_amplification_post_length = if post_length <= 1024 { 1.0f64 } else { 1.0f64 + 1.75 * (post_length as f64 / 1024f64).log2() };
25    let pow_amplification_linked_base_ids = if linked_base_ids_len < 2 {1.0f64} else { 1.0f64 + 0.2f64 * (linked_base_ids_len as f64).powi(2) };
26    let pow_amplification_bucket = 1.0f64 + (MILLIS_IN_MONTH.0 as f64 / duration.0 as f64).log2();
27
28    let pow_amplification = pow_amplification_post_length * pow_amplification_linked_base_ids * pow_amplification_bucket;
29    let additional_pow_bits = pow_amplification.log2().ceil();
30    Pow(config::POW_MINIMUM_PER_POST.0 + additional_pow_bits as u8)
31}
32
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37    use crate::tools::config;
38    use crate::tools::time::{MILLIS_IN_DAY, MILLIS_IN_HOUR, MILLIS_IN_MONTH};
39    use crate::tools::types::Pow;
40
41    // baseline: small post, client_id only (< 2 links → no extra amp), month → total=1.0, ceil to +0 bits
42    #[test]
43    fn baseline() {
44        let pow = get_minimum_post_pow(512, 1, MILLIS_IN_MONTH);
45        assert_eq!(pow, config::POW_MINIMUM_PER_POST);
46    }
47
48    // day bucket: bucket_amp≈5.81, linked_amp=1.0 → total=5.81, log2≈2.54 → ceil to +3 bits
49    #[test]
50    fn day_bucket() {
51        let pow = get_minimum_post_pow(512, 1, MILLIS_IN_DAY);
52        assert_eq!(pow, Pow(config::POW_MINIMUM_PER_POST.0 + 3));
53    }
54
55    // hour bucket: bucket_amp≈10.39, linked_amp=1.0 → total=10.39, log2≈3.38 → ceil to +4 bits
56    #[test]
57    fn hour_bucket() {
58        let pow = get_minimum_post_pow(512, 1, MILLIS_IN_HOUR);
59        assert_eq!(pow, Pow(config::POW_MINIMUM_PER_POST.0 + 4));
60    }
61
62    // linked ids (3): amplification kicks in at ≥ 2 links; linked_amp = 1 + 0.2*9 = 2.8, log2≈1.49 → ceil to +2 bits
63    #[test]
64    fn linked_ids() {
65        let pow = get_minimum_post_pow(512, 3, MILLIS_IN_MONTH);
66        assert_eq!(pow, Pow(config::POW_MINIMUM_PER_POST.0 + 2));
67    }
68
69    // combined: post_amp=4.5, linked_amp=2.8, bucket_amp≈5.81 → product≈73.2, log2≈6.19 → ceil to +7 bits
70    #[test]
71    fn combined() {
72        let pow = get_minimum_post_pow(4096, 3, MILLIS_IN_DAY);
73        assert_eq!(pow, Pow(config::POW_MINIMUM_PER_POST.0 + 7));
74    }
75
76    // medium post (4 KB): post_amp=4.5, linked_amp=1.0 → total=4.5, log2≈2.17 → ceil to +3 bits
77    #[test]
78    fn medium_post_4kb() {
79        let pow = get_minimum_post_pow(4 * 1024, 1, MILLIS_IN_MONTH);
80        assert_eq!(pow, Pow(config::POW_MINIMUM_PER_POST.0 + 3));
81    }
82
83    // large post (64 KB): post_amp=11.5, linked_amp=1.0 → total=11.5, log2≈3.52 → ceil to +4 bits
84    #[test]
85    fn large_post_64kb() {
86        let pow = get_minimum_post_pow(64 * 1024, 1, MILLIS_IN_MONTH);
87        assert_eq!(pow, Pow(config::POW_MINIMUM_PER_POST.0 + 4));
88    }
89
90    // large post (512 KB): post_amp=16.75, linked_amp=1.0 → total=16.75, log2≈4.07 → ceil to +5 bits
91    #[test]
92    fn large_post_512kb() {
93        let pow = get_minimum_post_pow(512 * 1024, 1, MILLIS_IN_MONTH);
94        assert_eq!(pow, Pow(config::POW_MINIMUM_PER_POST.0 + 5));
95    }
96
97    // minute-granularity bucket (5 min): bucket_amp≈13.977, linked_amp=1.0 → total=13.977, log2≈3.80 → ceil to +4 bits
98    #[test]
99    fn minute_granularity_5min() {
100        let pow = get_minimum_post_pow(512, 1, MILLIS_IN_MINUTE.const_mul(5));
101        assert_eq!(pow, Pow(config::POW_MINIMUM_PER_POST.0 + 4));
102    }
103
104    // many linked ids (10): linked_amp = 1 + 0.2*100 = 21.0, total log2 ≈ 4.39 → ceil to +5 bits
105    #[test]
106    fn many_linked_ids_10() {
107        let pow = get_minimum_post_pow(512, 10, MILLIS_IN_MONTH);
108        assert_eq!(pow, Pow(config::POW_MINIMUM_PER_POST.0 + 5));
109    }
110
111    // shorter duration → higher or equal pow
112    #[test]
113    fn monotone_duration() {
114        let pow_month = get_minimum_post_pow(512, 1, MILLIS_IN_MONTH);
115        let pow_day = get_minimum_post_pow(512, 1, MILLIS_IN_DAY);
116        let pow_hour = get_minimum_post_pow(512, 1, MILLIS_IN_HOUR);
117        assert!(pow_month <= pow_day);
118        assert!(pow_day <= pow_hour);
119        assert!(pow_month < pow_hour);
120    }
121
122    // more linked ids → higher or equal pow (minimum real-world value is 1 — the client_id)
123    #[test]
124    fn monotone_linked_ids() {
125        let pow_1 = get_minimum_post_pow(512, 1, MILLIS_IN_DAY);
126        let pow_2 = get_minimum_post_pow(512, 2, MILLIS_IN_DAY);
127        let pow_3 = get_minimum_post_pow(512, 3, MILLIS_IN_DAY);
128        let pow_4 = get_minimum_post_pow(512, 4, MILLIS_IN_DAY);
129        assert!(pow_1 <= pow_2);
130        assert!(pow_2 <= pow_3);
131        assert!(pow_3 <= pow_4);
132        assert!(pow_1 < pow_4);
133    }
134
135    // larger posts → higher or equal pow (only kicks in above 1024 bytes)
136    #[test]
137    fn monotone_post_length() {
138        let pow_small = get_minimum_post_pow(1024, 1, MILLIS_IN_DAY);
139        let pow_medium = get_minimum_post_pow(8192, 1, MILLIS_IN_DAY);
140        let pow_large = get_minimum_post_pow(1024 * 1024, 1, MILLIS_IN_DAY);
141        assert!(pow_small <= pow_medium);
142        assert!(pow_medium <= pow_large);
143        assert!(pow_small < pow_large);
144    }
145
146    // every combination on a sample grid must be at or above the minimum
147    #[test]
148    fn never_below_minimum() {
149        let lengths = [1, 512, 1024, 4096, 64 * 1024];
150        let links = [1, 2, 5, 10];
151        let durations = [MILLIS_IN_HOUR, MILLIS_IN_DAY, MILLIS_IN_MONTH];
152        for post_length in lengths {
153            for linked_base_ids_len in links {
154                for duration in durations {
155                    let pow = get_minimum_post_pow(post_length, linked_base_ids_len, duration);
156                    assert!(
157                        pow >= config::POW_MINIMUM_PER_POST,
158                        "pow={:?} below minimum for post_length={} links={} duration={:?}",
159                        pow, post_length, linked_base_ids_len, duration
160                    );
161                }
162            }
163        }
164    }
165
166    // durations shorter than a minute are not valid bucket lengths and must panic
167    #[test]
168    #[should_panic]
169    fn panic_on_short_duration() {
170        get_minimum_post_pow(512, 1, MILLIS_IN_MINUTE);
171    }
172}