1use crate::indexer;
2use alto_types::{Block, Context, Scheme, EPOCH};
3use commonware_actor::Feedback;
4use commonware_consensus::{
5 marshal::{ancestry::Ancestry, Update},
6 types::{Height, Round, View},
7 Application as ConsensusApplication, Heightable, Reporter,
8};
9use commonware_cryptography::{ed25519, sha256, Digest as _, Digestible, Hasher, Sha256, Signer};
10use commonware_runtime::{Clock, Metrics, Spawner, Storage};
11use commonware_utils::{Acknowledgement, SystemTimeExt};
12use futures::StreamExt;
13use rand::Rng;
14use std::time::{Duration, SystemTime};
15use tracing::info;
16
17const GENESIS: &[u8] = b"commonware is neat";
19
20const MAX_BLOCK_TIMESTAMP_MS: u64 = 7_258_118_400_000;
25
26#[derive(Clone, Default)]
27pub struct Application {
28 backfiller: Option<indexer::Producer>,
29}
30
31impl Application {
32 pub fn genesis() -> Block {
33 let genesis_context = Context {
34 round: Round::new(EPOCH, View::zero()),
35 leader: ed25519::PrivateKey::from_seed(0).public_key(),
36 parent: (View::zero(), sha256::Digest::EMPTY),
37 };
38 Block::new(genesis_context, Sha256::hash(GENESIS), Height::zero(), 0)
39 }
40
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub(crate) fn with_backfiller(mut self, backfiller: indexer::Producer) -> Self {
46 self.backfiller = Some(backfiller);
47 self
48 }
49}
50
51impl<E> ConsensusApplication<E> for Application
52where
53 E: Rng + Spawner + Metrics + Clock + Storage,
54{
55 type SigningScheme = Scheme;
56 type Context = Context;
57 type Block = Block;
58
59 async fn propose(
60 &mut self,
61 (runtime_context, context): (E, Self::Context),
62 mut ancestry: impl Ancestry<Self::Block>,
63 ) -> Option<Self::Block> {
64 let parent = ancestry.next().await?;
65
66 let mut current = runtime_context.current().epoch_millis();
68 if current <= parent.timestamp {
69 current = parent
70 .timestamp
71 .checked_add(1)
72 .expect("parent timestamp overflowed");
73 }
74 assert!(
75 current <= MAX_BLOCK_TIMESTAMP_MS,
76 "proposed timestamp exceeded maximum",
77 );
78
79 Some(Block::new(
80 context,
81 parent.digest(),
82 parent.height.next(),
83 current,
84 ))
85 }
86
87 async fn verify(
88 &mut self,
89 (runtime_context, _): (E, Self::Context),
90 mut ancestry: impl Ancestry<Self::Block>,
91 ) -> bool {
92 let Some(block) = ancestry.next().await else {
93 return false;
94 };
95 let Some(parent) = ancestry.next().await else {
96 return false;
97 };
98
99 if block.timestamp <= parent.timestamp || block.timestamp > MAX_BLOCK_TIMESTAMP_MS {
101 return false;
102 }
103 let deadline = SystemTime::UNIX_EPOCH
104 .checked_add(Duration::from_millis(block.timestamp))
105 .expect("block timestamp exceeded maximum");
106 runtime_context.sleep_until(deadline).await;
107
108 true
112 }
113}
114
115impl Reporter for Application {
116 type Activity = Update<Block>;
117
118 fn report(&mut self, activity: Self::Activity) -> Feedback {
119 if let Update::Block(block, _) = &activity {
120 info!(
121 height = %block.height(),
122 digest = ?block.digest(),
123 timestamp = block.timestamp,
124 "finalized block"
125 );
126 }
127
128 if let Some(backfiller) = &mut self.backfiller {
129 return backfiller.report(activity);
130 }
131
132 if let Update::Block(_, ack_rx) = activity {
133 ack_rx.acknowledge();
134 }
135 Feedback::Ok
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use commonware_consensus::marshal::ancestry;
143 use commonware_runtime::{deterministic, Runner as _, Supervisor as _};
144
145 fn test_context(view: u64, parent: (View, sha256::Digest)) -> Context {
146 Context {
147 round: Round::new(EPOCH, View::new(view)),
148 leader: ed25519::PrivateKey::from_seed(view).public_key(),
149 parent,
150 }
151 }
152
153 async fn verify_block(
154 context: deterministic::Context,
155 application: &mut Application,
156 block: &Block,
157 parent: &Block,
158 ) -> bool {
159 let ancestry = ancestry::from_iter([block.clone(), parent.clone()]);
160 ConsensusApplication::verify(application, (context, block.context.clone()), ancestry).await
161 }
162
163 async fn propose_child(
164 context: deterministic::Context,
165 application: &mut Application,
166 child_context: Context,
167 parent: &Block,
168 ) -> Block {
169 let ancestry = ancestry::from_iter([parent.clone()]);
170 ConsensusApplication::propose(application, (context, child_context), ancestry)
171 .await
172 .expect("expected proposal")
173 }
174
175 #[test]
176 fn verify_waits_for_far_future_block_timestamp() {
177 let runner = deterministic::Runner::default();
178 runner.start(|context| async move {
179 let mut application = Application::new();
180
181 let now = context.current().epoch_millis();
182 let parent = Block::new(
183 test_context(1, (View::zero(), sha256::Digest::EMPTY)),
184 Sha256::hash(b"genesis"),
185 Height::new(1),
186 now,
187 );
188 let block = Block::new(
189 test_context(2, (View::new(1), parent.digest())),
190 parent.digest(),
191 parent.height.next(),
192 now + 5_000,
193 );
194
195 let start = context.current();
196 assert!(verify_block(context.child("verify"), &mut application, &block, &parent).await);
197 let finished = context.current();
198 assert!(finished.duration_since(start).unwrap() > Duration::ZERO);
199 assert!(finished.epoch_millis() >= block.timestamp);
200 });
201 }
202
203 #[test]
204 fn verify_rejects_equal_parent_timestamp() {
205 let runner = deterministic::Runner::default();
206 runner.start(|context| async move {
207 let mut application = Application::new();
208
209 let now = context.current().epoch_millis();
210 let parent = Block::new(
211 test_context(1, (View::zero(), sha256::Digest::EMPTY)),
212 Sha256::hash(b"genesis"),
213 Height::new(1),
214 now,
215 );
216 let block = Block::new(
217 test_context(2, (View::new(1), parent.digest())),
218 parent.digest(),
219 parent.height.next(),
220 now,
221 );
222
223 assert!(
224 !verify_block(context.child("verify"), &mut application, &block, &parent).await
225 );
226 });
227 }
228
229 #[test]
230 fn verify_returns_immediately_for_mature_block_timestamp() {
231 let runner = deterministic::Runner::default();
232 runner.start(|context| async move {
233 let mut application = Application::new();
234
235 context.sleep(Duration::from_millis(10)).await;
236 let now = context.current().epoch_millis();
237 let parent = Block::new(
238 test_context(1, (View::zero(), sha256::Digest::EMPTY)),
239 Sha256::hash(b"genesis"),
240 Height::new(1),
241 now - 1,
242 );
243 let block = Block::new(
244 test_context(2, (View::new(1), parent.digest())),
245 parent.digest(),
246 parent.height.next(),
247 now,
248 );
249
250 let start = context.current();
251 assert!(verify_block(context.child("verify"), &mut application, &block, &parent).await);
252 let finished = context.current();
253 assert!(finished.duration_since(start).unwrap() < Duration::from_millis(10));
254 });
255 }
256
257 #[test]
258 fn propose_uses_parent_timestamp_plus_one_when_clock_is_behind() {
259 let runner = deterministic::Runner::default();
260 runner.start(|context| async move {
261 let mut application = Application::new();
262
263 let now = context.current().epoch_millis();
264 let parent = Block::new(
265 test_context(1, (View::zero(), sha256::Digest::EMPTY)),
266 Sha256::hash(b"genesis"),
267 Height::new(1),
268 now + 5_000,
269 );
270 let proposal = propose_child(
271 context.child("propose"),
272 &mut application,
273 test_context(2, (View::new(1), parent.digest())),
274 &parent,
275 )
276 .await;
277
278 assert_eq!(proposal.parent, parent.digest());
279 assert_eq!(proposal.height, parent.height.next());
280 assert_eq!(proposal.timestamp, parent.timestamp + 1);
281 });
282 }
283
284 #[test]
285 fn verify_rejects_timestamp_above_maximum() {
286 let runner = deterministic::Runner::default();
287 runner.start(|context| async move {
288 let mut application = Application::new();
289
290 let now = context.current().epoch_millis();
291 let parent = Block::new(
292 test_context(1, (View::zero(), sha256::Digest::EMPTY)),
293 Sha256::hash(b"genesis"),
294 Height::new(1),
295 now,
296 );
297 let block = Block::new(
298 test_context(2, (View::new(1), parent.digest())),
299 parent.digest(),
300 parent.height.next(),
301 MAX_BLOCK_TIMESTAMP_MS + 1,
304 );
305
306 assert!(
307 !verify_block(context.child("verify"), &mut application, &block, &parent).await
308 );
309 });
310 }
311
312 #[test]
313 #[should_panic(expected = "proposed timestamp exceeded maximum")]
314 fn propose_panics_when_parent_timestamp_is_maximum() {
315 let runner = deterministic::Runner::default();
316 runner.start(|context| async move {
317 let mut application = Application::new();
318
319 let parent = Block::new(
320 test_context(1, (View::zero(), sha256::Digest::EMPTY)),
321 Sha256::hash(b"genesis"),
322 Height::new(1),
323 MAX_BLOCK_TIMESTAMP_MS,
326 );
327 let _ = propose_child(
328 context.child("propose"),
329 &mut application,
330 test_context(2, (View::new(1), parent.digest())),
331 &parent,
332 )
333 .await;
334 });
335 }
336}