1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! Self-hosted postage batch utilization snapshots.
//!
//! Issuing postage stamps requires per-bucket counters so every stamp assigns a
//! fresh storage slot. This crate serializes those counters into a compact
//! snapshot stored *inside the batch it describes*, as single-owner chunks at
//! addresses derived from the batch id and owner alone, so a user can recover
//! their issuer state on any machine from just their key and batch id.
//!
//! # Layout
//!
//! Snapshot chunk `n` has single-owner chunk id
//! `keccak256("swarm-batch-usage" || batch_id || u16_be(n))`, owned and stamped
//! by the batch owner. Chunk 0 is the root: it commits to the batch geometry, a
//! sequence number, the slots the snapshot chunks occupy, and the digests of the
//! leaf chunks holding the counter table. Chunk ids never change, so each
//! snapshot chunk occupies one storage slot for the life of the batch (a newer
//! timestamp at the same address and stamp index overwrites in place).
//!
//! Counters use patched frame-of-reference packing sized to the *spread* of the
//! counters, not the batch depth. A table is immutable (monotone fill
//! watermarks) or mutable (wrapping ring cursors); see [`UsageTable`].
//!
//! See `README.md` for the full format specification.
//!
//! # Example
//!
//! ```
//! use alloy_primitives::{Address, B256};
//! use nectar_postage_usage::{
//! Mutability, PublishedSequence, RootInfo, Snapshot, SwarmAddress, UsageTable,
//! };
//!
//! let batch_id = B256::repeat_byte(0x42);
//! let owner = Address::repeat_byte(0x11);
//!
//! // Issue a stamp for an uploaded chunk through the snapshot's issuing handle,
//! // then plan a persist.
//! let table = UsageTable::new(batch_id, 20, 16, Mutability::Immutable).unwrap();
//! let mut snapshot = Snapshot::new(table);
//! let address = SwarmAddress::from(B256::repeat_byte(0x99));
//! snapshot.issuer(owner).record_address(&address).unwrap();
//! // This table is fresh and was never published, so the live network read
//! // returns no root chunk and the floor is `NONE`.
//! let plan = snapshot
//! .revalidate(PublishedSequence::NONE)
//! .unwrap()
//! .plan_persist(&owner)
//! .unwrap();
//!
//! // Publish each plan chunk as a single-owner chunk stamped with
//! // `plan.chunks[n].stamp_index`. Reading back:
//! let root = RootInfo::parse(&plan.chunks[0].payload).unwrap();
//! let leaves: Vec<_> = plan.chunks[1..].iter().map(|c| &c.payload).collect();
//! let recovered = root.assemble(&leaves).unwrap();
//! assert_eq!(recovered, snapshot);
//! ```
//!
//! # Recovery
//!
//! [`Snapshot::new`] is for a genuinely fresh, never-persisted table: it starts
//! the persist history at sequence 0 with no allocated slots. Recovered or
//! extracted state must never go through it, because resetting the sequence to 0
//! and dropping the slots would downgrade the version at the snapshot's own chunk
//! addresses and re-allocate colliding slots, overwriting a newer persisted
//! version in place. Recovered state round-trips through [`Snapshot::from_parts`]
//! instead, which keeps the table, sequence, and slots bound together;
//! [`RootInfo::assemble`] uses it when decoding from the network, and
//! [`Snapshot::into_parts`] yields the same indivisible [`SnapshotParts`] value
//! when extracting state from a live snapshot.
//!
//! Both in-memory downgrade routes off a recovered or extracted snapshot are
//! closed. The move route is closed because [`SnapshotParts`] holds its table
//! privately and never yields it by value. The clone route is closed because
//! [`Snapshot::table`] and [`SnapshotParts::table`] return a borrowed
//! [`TableView`] that exposes only read getters and does not deref to the
//! table, so cloning or copying it yields another borrowed view, never an owned
//! table that [`Snapshot::new`] would accept. No public API hands out an owned
//! [`UsageTable`] taken from a recovered snapshot.
//!
//! Two residual paths to a sequence-0 persist are protocol-level rather than
//! in-memory representability concerns, so the type guards here do not close
//! them; the [`PublishedSequence`] floor on [`Snapshot::revalidate`] does
//! (nectar issue #70). First, the public table constructors ([`UsageTable::new`]
//! and friends) must keep minting a fresh table for a genuinely new batch, so a
//! forged fresh table persisted at sequence 0 is caught by the floor, not by the
//! type system here. Second, the reserve overwrites a snapshot chunk by stamp
//! timestamp rather than by snapshot sequence, so full cross-version monotonicity
//! against the *published* sequence needs a compare-and-swap against the live
//! root chunk. The floor precondition implemented on [`Snapshot::revalidate`]
//! supplies exactly that compare-and-swap: the consumer reads the published
//! sequence from the live root chunk, hands it in as the floor, and a persist
//! whose next sequence does not strictly exceed it is rejected. This crate closes
//! the in-memory representability of the downgrade, and the floor closes the
//! persist-time downgrade.
extern crate alloc;
pub use RootInfo;
pub use UsageError;
pub use ;
pub use ;
pub use SnapshotIssuer;
pub use ;
pub use ;
pub use SwarmAddress;
/// Postage types re-exported so a downstream caller naming
/// [`PlannedChunk::stamp_index`] or calling [`UsageTable::from_batch`] does not
/// need a direct `nectar-postage` dependency.
pub use ;
use ;
/// Result alias for this crate.
pub type Result<T> = Result;
/// Domain separator for snapshot chunk ids.
pub const USAGE_DOMAIN: & = b"swarm-batch-usage";
/// The snapshot format magic ("SBU" plus the format version).
pub const MAGIC: = *b"SBU1";
/// Size of the fixed root header in bytes.
pub const ROOT_HEADER_SIZE: usize = 66;
/// Maximum payload size of a snapshot chunk.
pub const MAX_PAYLOAD_SIZE: usize = DEFAULT_BODY_SIZE;
/// Maximum number of exception entries in a snapshot.
pub const MAX_EXCEPTIONS: usize = 128;
/// Maximum bucket (uniformity) depth supported by the format.
pub const MAX_BUCKET_DEPTH: u8 = 16;
/// Maximum value of `depth - bucket_depth` supported by the format, chosen
/// so counters fit in a `u32`.
pub const MAX_COUNTER_BITS: u8 = 31;
/// Maximum delta bit width.
pub const MAX_WIDTH: u8 = 32;
/// Returns the single-owner chunk id of snapshot chunk `index` (0 is the
/// root, `n >= 1` is leaf `n - 1`).
/// Returns the address of snapshot chunk `index` for a batch owned by
/// `owner`, i.e. the single-owner chunk address `keccak256(id || owner)`.