Skip to content

Commit c057445

Browse files
committed
[glue] Add glue crate
1 parent b65ed8f commit c057445

46 files changed

Lines changed: 10054 additions & 17 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/publish.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ jobs:
115115
continue-on-error: true
116116
env:
117117
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
118+
- name: Publish glue
119+
run: cargo publish --manifest-path glue/Cargo.toml
120+
continue-on-error: true
121+
env:
122+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
118123
- name: Publish chat
119124
run: cargo publish --manifest-path examples/chat/Cargo.toml
120125
continue-on-error: true

Cargo.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ members = [
99
"consensus",
1010
"cryptography",
1111
"deployer",
12+
"glue",
1213
"macros",
1314
"macros/impl",
1415
"math",
@@ -96,6 +97,7 @@ commonware-conformance-macros = { version = "2026.3.0", path = "conformance/macr
9697
commonware-consensus = { version = "2026.3.0", path = "consensus" }
9798
commonware-cryptography = { version = "2026.3.0", path = "cryptography", default-features = false }
9899
commonware-deployer = { version = "2026.3.0", path = "deployer", default-features = false }
100+
commonware-glue = { version = "2026.3.0", path = "glue" }
99101
commonware-invariants = { version = "2026.3.0", path = "invariants" }
100102
commonware-macros = { version = "2026.3.0", path = "macros", default-features = false }
101103
commonware-macros-impl = { version = "2026.3.0", path = "macros/impl" }

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ _Primitives are designed for deployment in adversarial environments. If you find
2020
* [consensus](./consensus/README.md): Order opaque messages in a Byzantine environment.
2121
* [cryptography](./cryptography/README.md): Generate keys, sign arbitrary messages, and deterministically verify signatures.
2222
* [deployer](./deployer/README.md): Deploy infrastructure across cloud providers.
23+
* [glue](./glue/README.md): Bootstrap applications with commonware primitive compositions.
2324
* [math](./math/README.md): Create and manipulate mathematical objects.
2425
* [p2p](./p2p/README.md): Communicate with authenticated peers over encrypted connections.
2526
* [parallel](./parallel/README.md): Parallelize fold operations with pluggable execution strategies.

consensus/src/marshal/ancestry.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use crate::{types::Height, Block, Heightable};
44
use commonware_cryptography::Digestible;
5+
use commonware_utils::sync::Mutex;
56
use futures::{
67
future::{BoxFuture, OptionFuture},
78
FutureExt, Stream,
@@ -10,6 +11,7 @@ use pin_project::pin_project;
1011
use std::{
1112
future::Future,
1213
pin::Pin,
14+
sync::Arc,
1315
task::{Context, Poll},
1416
};
1517

@@ -34,6 +36,45 @@ pub trait BlockProvider: Clone + Send + 'static {
3436
) -> impl Future<Output = Option<Self::Block>> + Send;
3537
}
3638

39+
/// A type-erased [`BlockProvider`] that wraps any concrete provider behind
40+
/// a shared closure.
41+
///
42+
/// Used by actor mailboxes and other channel-based patterns where a generic
43+
/// `BlockProvider` type parameter must be eliminated before sending across
44+
/// a channel.
45+
#[derive(Clone)]
46+
pub struct ErasedBlockProvider<B: Block> {
47+
fetch: Arc<dyn Fn(<B as Digestible>::Digest) -> BoxFuture<'static, Option<B>> + Send + Sync>,
48+
}
49+
50+
impl<B: Block> ErasedBlockProvider<B> {
51+
/// Erase a concrete [`BlockProvider`] into a type-erased provider.
52+
///
53+
/// The provider is wrapped in a `Mutex` so that the resulting closure
54+
/// is `Sync` (required by `Arc<dyn Fn + Send + Sync>`). The lock is
55+
/// held only for the duration of `clone()`, never across an await.
56+
pub fn new<M: BlockProvider<Block = B>>(provider: M) -> Self {
57+
let provider = Arc::new(Mutex::new(provider));
58+
Self {
59+
fetch: Arc::new(move |digest| {
60+
let p = provider.lock().clone();
61+
Box::pin(async move { p.fetch_block(digest).await })
62+
}),
63+
}
64+
}
65+
}
66+
67+
impl<B: Block> BlockProvider for ErasedBlockProvider<B> {
68+
type Block = B;
69+
70+
fn fetch_block(
71+
self,
72+
digest: <B as Digestible>::Digest,
73+
) -> impl Future<Output = Option<B>> + Send {
74+
(self.fetch)(digest)
75+
}
76+
}
77+
3778
/// Yields the ancestors of a block while prefetching parents, _not_ including the genesis block.
3879
///
3980
// TODO(<https://github.com/commonwarexyz/monorepo/issues/2982>): Once marshal can also yield the genesis block,
@@ -47,6 +88,31 @@ pub struct AncestorStream<M, B: Block> {
4788
}
4889

4990
impl<M, B: Block> AncestorStream<M, B> {
91+
/// Returns a reference to the next block that will be yielded by the
92+
/// stream, without consuming it.
93+
///
94+
/// Returns `None` if the buffer is empty and the next block is being
95+
/// fetched asynchronously.
96+
pub fn peek(&self) -> Option<&B> {
97+
self.buffered.last()
98+
}
99+
100+
/// Erase the block provider type, producing an
101+
/// `AncestorStream<ErasedBlockProvider<B>, B>`.
102+
///
103+
/// The returned stream behaves identically but can be sent across
104+
/// channels that require a concrete type.
105+
pub fn erase(self) -> AncestorStream<ErasedBlockProvider<B>, B>
106+
where
107+
M: BlockProvider<Block = B>,
108+
{
109+
AncestorStream {
110+
buffered: self.buffered,
111+
marshal: ErasedBlockProvider::new(self.marshal),
112+
pending: self.pending,
113+
}
114+
}
115+
50116
/// Creates a new [AncestorStream] starting from the given ancestry.
51117
///
52118
/// # Panics

consensus/src/marshal/coding/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ mod tests {
157157
harness::prune_finalized_archives::<CodingHarness>();
158158
}
159159

160+
#[test_traced("WARN")]
161+
fn test_coding_set_floor_without_pruning_preserves_archives() {
162+
harness::set_floor_without_pruning_preserves_archives::<CodingHarness>();
163+
}
164+
160165
#[test_traced("WARN")]
161166
fn test_coding_rejects_block_delivery_below_floor() {
162167
harness::reject_stale_block_delivery_after_floor_update::<CodingHarness>();

consensus/src/marshal/core/actor.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,9 @@ where
625625
let finalization = self.get_finalization_by_height(height).await;
626626
response.send_lossy(finalization);
627627
}
628+
Message::GetProcessedHeight { response } => {
629+
response.send_lossy(self.last_processed_height);
630+
}
628631
Message::HintFinalized { height, targets } => {
629632
// Skip if height is at or below the floor
630633
if height <= self.last_processed_height {
@@ -670,7 +673,10 @@ where
670673
)
671674
.await;
672675
}
673-
Message::SetFloor { height } => {
676+
Message::SetFloor {
677+
height,
678+
prune_archives,
679+
} => {
674680
if self.last_processed_height >= height {
675681
warn!(
676682
%height,
@@ -692,10 +698,12 @@ where
692698
// updating `last_processed_height`.
693699
self.pending_acks.clear();
694700

695-
// Prune data in the finalized archives below the new floor.
696-
if let Err(err) = self.prune_finalized_archives(height).await {
697-
error!(?err, %height, "failed to prune finalized archives");
698-
return;
701+
if prune_archives {
702+
// Prune data in the finalized archives below the new floor.
703+
if let Err(err) = self.prune_finalized_archives(height).await {
704+
error!(?err, %height, "failed to prune finalized archives");
705+
return;
706+
}
699707
}
700708

701709
// Intentionally keep existing block subscriptions alive. Canceling

consensus/src/marshal/core/mailbox.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ pub(crate) enum Message<S: Scheme, V: Variant> {
4545
/// A channel to send the retrieved finalization.
4646
response: oneshot::Sender<Option<Finalization<S, V::Commitment>>>,
4747
},
48+
/// A request to retrieve the latest processed height acknowledged by the application.
49+
GetProcessedHeight {
50+
/// A channel to send the latest processed height.
51+
response: oneshot::Sender<Height>,
52+
},
4853
/// A hint that a finalized block may be available at a given height.
4954
///
5055
/// This triggers a network fetch if the finalization is not available locally.
@@ -111,7 +116,7 @@ pub(crate) enum Message<S: Scheme, V: Variant> {
111116
/// Sets the sync starting point (advances if higher than current).
112117
///
113118
/// Marshal will sync and deliver blocks starting at `floor + 1`. Data below
114-
/// the floor is pruned.
119+
/// the floor is pruned when `prune_archives` is `true`.
115120
///
116121
/// To prune data without affecting the sync starting point (say at some trailing depth
117122
/// from tip), use [Message::Prune] instead.
@@ -120,6 +125,9 @@ pub(crate) enum Message<S: Scheme, V: Variant> {
120125
SetFloor {
121126
/// The candidate floor height.
122127
height: Height,
128+
129+
/// Whether to prune finalized archives below the new floor.
130+
prune_archives: bool,
123131
},
124132
/// Prunes finalized blocks and certificates below the given height.
125133
///
@@ -194,6 +202,13 @@ impl<S: Scheme, V: Variant> Mailbox<S, V> {
194202
.flatten()
195203
}
196204

205+
/// Retrieve the latest processed height acknowledged by the application.
206+
pub async fn get_processed_height(&self) -> Option<Height> {
207+
self.sender
208+
.request(|response| Message::GetProcessedHeight { response })
209+
.await
210+
}
211+
197212
/// Hints that a finalized block may be available at the given height.
198213
///
199214
/// This method will request the finalization from the network via the resolver
@@ -300,14 +315,19 @@ impl<S: Scheme, V: Variant> Mailbox<S, V> {
300315
/// Sets the sync starting point (advances if higher than current).
301316
///
302317
/// Marshal will sync and deliver blocks starting at `floor + 1`. Data below
303-
/// the floor is pruned.
318+
/// the floor is pruned when `prune_archives` is `true`.
304319
///
305320
/// To prune data without affecting the sync starting point (say at some trailing depth
306321
/// from tip), use [Self::prune] instead.
307322
///
308323
/// The default floor is 0.
309-
pub async fn set_floor(&self, height: Height) {
310-
self.sender.send_lossy(Message::SetFloor { height }).await;
324+
pub async fn set_floor(&self, height: Height, prune_archives: bool) {
325+
self.sender
326+
.send_lossy(Message::SetFloor {
327+
height,
328+
prune_archives,
329+
})
330+
.await;
311331
}
312332

313333
/// Prunes finalized blocks and certificates below the given height.

0 commit comments

Comments
 (0)