Skip to main content

alto_chain/
application.rs

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
17/// Genesis message to use during initialization.
18const GENESIS: &[u8] = b"commonware is neat";
19
20/// Fixed consensus cutoff for block timestamps: 2200-01-01T00:00:00Z.
21///
22/// Different platforms have different `SystemTime` limits, so we use a fixed
23/// timestamp to ensure consistent application of block validity rules.
24const 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        // Create a new block.
67        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        // Verify the block (waiting until the block timestamp has passed to vote in case of skew).
100        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        // The height and digest invariants are enforced in `Marshaled`:
109        // - The block height must be one greater than the parent's height.
110        // - The block's parent digest must match the parent's digest.
111        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                // Verification should reject timestamps outside the fixed
302                // protocol range before attempting to sleep.
303                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                // Proposing on top of a parent already at the maximum would
324                // require `parent.timestamp + 1`, which must be rejected.
325                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}