Skip to content

Commit f1e9ae0

Browse files
committed
mtc_worker: Add API for the current landmark bundle
A "landmark bundle" consists of the minimum amount of information needed to validate all MTCs issued before some checkpoint. It has the following components: 1. The checkpoint, including co-signatures 2. The sequence of landmark subtrees determined by the landmark sequence up to the checkpoint 3. A proof of consistency of each subtree with the checkpoint. Add an API to mtc_worker for fetching the current landmark bundle.
1 parent 63ec7b5 commit f1e9ae0

File tree

2 files changed

+184
-31
lines changed

2 files changed

+184
-31
lines changed

crates/mtc_api/src/landmark.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,49 @@ impl LandmarkSequence {
165165
landmarks,
166166
})
167167
}
168+
169+
/// Iterate over the sequence of subtrees determined by the landmark sequence.
170+
pub fn subtrees(&self) -> LandmarkSubtreesIterator<'_> {
171+
LandmarkSubtreesIterator {
172+
index: 1,
173+
landmarks: &self.landmarks,
174+
next_subtree: None,
175+
}
176+
}
177+
}
178+
179+
/// An iterator over the subtrees determined by the landmark sequence.
180+
pub struct LandmarkSubtreesIterator<'a> {
181+
index: usize,
182+
landmarks: &'a VecDeque<u64>,
183+
next_subtree: Option<Subtree>,
184+
}
185+
186+
impl Iterator for LandmarkSubtreesIterator<'_> {
187+
type Item = Subtree;
188+
189+
fn next(&mut self) -> Option<Subtree> {
190+
if self.landmarks.len() < 2 {
191+
return None;
192+
}
193+
194+
if let Some(subtree) = self.next_subtree.take() {
195+
self.next_subtree = None;
196+
return Some(subtree);
197+
}
198+
199+
if self.index == self.landmarks.len() {
200+
return None;
201+
}
202+
203+
let subtree;
204+
(subtree, self.next_subtree) =
205+
Subtree::split_interval(self.landmarks[self.index - 1], self.landmarks[self.index])
206+
.unwrap();
207+
208+
self.index += 1;
209+
Some(subtree)
210+
}
168211
}
169212

170213
#[cfg(test)]
@@ -233,4 +276,28 @@ mod tests {
233276
// Past last landmark.
234277
assert!(seq.subtree_for_index(200).is_none());
235278
}
279+
280+
#[test]
281+
fn test_subtrees() {
282+
let mut seq = LandmarkSequence::create(10);
283+
assert!(seq.subtrees().next().is_none());
284+
285+
for i in 1..=5 {
286+
seq.add(i * 10).unwrap();
287+
}
288+
let got = seq.subtrees().collect::<Vec<_>>();
289+
let want = vec![
290+
Subtree::new(0, 8).unwrap(),
291+
Subtree::new(8, 10).unwrap(),
292+
Subtree::new(8, 16).unwrap(),
293+
Subtree::new(16, 20).unwrap(),
294+
Subtree::new(20, 24).unwrap(),
295+
Subtree::new(24, 30).unwrap(),
296+
Subtree::new(30, 32).unwrap(),
297+
Subtree::new(32, 40).unwrap(),
298+
Subtree::new(40, 48).unwrap(),
299+
Subtree::new(48, 50).unwrap(),
300+
];
301+
assert_eq!(got, want);
302+
}
236303
}

crates/mtc_worker/src/frontend_worker.rs

Lines changed: 117 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ use der::{
1414
use generic_log_worker::{
1515
batcher_id_from_lookup_key, deserialize, get_cached_metadata, get_durable_object_stub,
1616
init_logging, load_cache_kv, load_public_bucket,
17-
log_ops::{prove_subtree_inclusion, read_leaf, ProofError, CHECKPOINT_KEY},
17+
log_ops::{
18+
prove_subtree_consistency, prove_subtree_inclusion, read_leaf, ProofError, CHECKPOINT_KEY,
19+
},
1820
put_cache_entry_metadata, serialize,
1921
util::now_millis,
2022
ObjectBackend, ObjectBucket, ENTRY_ENDPOINT, METRICS_ENDPOINT,
@@ -28,7 +30,9 @@ use serde::{Deserialize, Serialize};
2830
use serde_with::{base64::Base64, serde_as};
2931
use signed_note::{NoteVerifier, VerifierList};
3032
use std::time::Duration;
31-
use tlog_tiles::{open_checkpoint, LeafIndex, PendingLogEntry, PendingLogEntryBlob};
33+
use tlog_tiles::{
34+
open_checkpoint, CheckpointText, LeafIndex, PendingLogEntry, PendingLogEntryBlob,
35+
};
3236
#[allow(clippy::wildcard_imports)]
3337
use worker::*;
3438
use x509_cert::{
@@ -71,6 +75,24 @@ pub struct GetCertificateResponse {
7175
pub landmark_id: usize,
7276
}
7377

78+
/// GET response structure for the `/get-landmark-bundle` endpoint
79+
#[serde_as]
80+
#[derive(Serialize, Deserialize)]
81+
pub struct GetLandmarkBundleResponse {
82+
#[serde_as(as = "Base64")]
83+
pub checkpoint: Vec<u8>,
84+
pub subtrees: Vec<SubtreeWithConsistencyProof>,
85+
}
86+
87+
#[serde_as]
88+
#[derive(Serialize, Deserialize)]
89+
pub struct SubtreeWithConsistencyProof {
90+
pub start: u64,
91+
pub end: u64,
92+
#[serde_as(as = "Base64")]
93+
pub consistency_proof: Vec<u8>,
94+
}
95+
7496
/// Start is the first code run when the Wasm module is loaded.
7597
#[event(start)]
7698
fn start() {
@@ -128,39 +150,13 @@ async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
128150
let object_backend = ObjectBucket::new(load_public_bucket(&ctx.env, name)?);
129151
// Fetch the current checkpoint to know which tiles to fetch
130152
// (full or partials).
131-
let checkpoint_bytes = object_backend
132-
.fetch(CHECKPOINT_KEY)
133-
.await?
134-
.ok_or("no checkpoint in object storage".to_string())?;
135-
let origin = &load_origin(name);
136-
let verifiers = &VerifierList::new(
137-
load_checkpoint_signers(&ctx.env, name)
138-
.iter()
139-
.map(|s| s.verifier())
140-
.collect::<Vec<Box<dyn NoteVerifier>>>(),
141-
);
142-
let checkpoint = open_checkpoint(
143-
origin.as_str(),
144-
verifiers,
145-
now_millis(),
146-
&checkpoint_bytes,
147-
)
148-
.map_err(|e| e.to_string())?
149-
.0;
153+
let (checkpoint, _checkpoint_bytes) =
154+
get_current_checkpoint(&ctx.env, name, &object_backend).await?;
150155
if leaf_index >= checkpoint.size() {
151156
return Response::error("Leaf index is not in log", 422);
152157
}
153158

154-
let seq = if let Some(bytes) = object_backend.fetch(LANDMARK_KEY).await? {
155-
let max_landmarks = params
156-
.max_certificate_lifetime_secs
157-
.div_ceil(params.landmark_interval_secs)
158-
+ 1;
159-
LandmarkSequence::from_bytes(&bytes, max_landmarks)
160-
.map_err(|e| e.to_string())?
161-
} else {
162-
return Err("failed to get landmark sequence".into());
163-
};
159+
let seq = get_landmark_sequence(name, &object_backend).await?;
164160
if leaf_index < seq.first_index() {
165161
return Response::error("Leaf index is before first active landmark", 422);
166162
}
@@ -223,6 +219,9 @@ async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
223219

224220
Response::from_json(&GetCertificateResponse { data, landmark_id })
225221
})
222+
.get_async("/logs/:log/get-landmark-bundle", |_req, ctx| async move {
223+
get_landmark_bundle(&ctx.env, ctx.data).await
224+
})
226225
.get("/logs/:log/metadata", |_req, ctx| {
227226
let name = ctx.data;
228227
let params = &CONFIG.logs[name];
@@ -442,6 +441,93 @@ async fn add_entry(mut req: Request, env: &Env, name: &str) -> Result<Response>
442441
})
443442
}
444443

444+
async fn get_landmark_bundle(env: &Env, name: &str) -> Result<Response> {
445+
let object_backend = ObjectBucket::new(load_public_bucket(env, name)?);
446+
447+
// Fetch the current checkpoint.
448+
let (checkpoint, checkpoint_bytes) = get_current_checkpoint(env, name, &object_backend).await?;
449+
450+
// Fetch the current landmark sequence.
451+
let landmark_sequence = get_landmark_sequence(name, &object_backend).await?;
452+
453+
// Compute the sequence of landmark subtrees and, for each subtree, a proof of consistency with
454+
// the checkpoint. Each signatureless MTC includes an inclusion proof in one of these subtrees.
455+
let mut subtrees = Vec::new();
456+
for landmark_subtree in landmark_sequence.subtrees() {
457+
let consistency_proof = match prove_subtree_consistency(
458+
*checkpoint.hash(),
459+
checkpoint.size(),
460+
landmark_subtree.lo(),
461+
landmark_subtree.hi(),
462+
&object_backend,
463+
)
464+
.await
465+
{
466+
Ok(p) => p,
467+
Err(ProofError::Tlog(s)) => return Response::error(s.to_string(), 422),
468+
Err(ProofError::Other(e)) => return Err(e.to_string().into()),
469+
};
470+
471+
subtrees.push(SubtreeWithConsistencyProof {
472+
start: landmark_subtree.lo(),
473+
end: landmark_subtree.hi(),
474+
consistency_proof: consistency_proof
475+
.iter()
476+
.flat_map(|h| h.0.to_vec())
477+
.collect::<Vec<u8>>(),
478+
});
479+
}
480+
481+
Response::from_json(&GetLandmarkBundleResponse {
482+
checkpoint: checkpoint_bytes,
483+
subtrees,
484+
})
485+
}
486+
487+
async fn get_current_checkpoint(
488+
env: &Env,
489+
name: &str,
490+
object_backend: &ObjectBucket,
491+
) -> Result<(CheckpointText, Vec<u8>)> {
492+
let checkpoint_bytes = object_backend
493+
.fetch(CHECKPOINT_KEY)
494+
.await?
495+
.ok_or("no checkpoint in object storage".to_string())?;
496+
497+
let origin = &load_origin(name);
498+
let verifiers = &VerifierList::new(
499+
load_checkpoint_signers(env, name)
500+
.iter()
501+
.map(|s| s.verifier())
502+
.collect::<Vec<Box<dyn NoteVerifier>>>(),
503+
);
504+
let (checkpoint, _timestamp) =
505+
open_checkpoint(origin.as_str(), verifiers, now_millis(), &checkpoint_bytes)
506+
.map_err(|e| e.to_string())?;
507+
Ok((checkpoint, checkpoint_bytes))
508+
}
509+
510+
async fn get_landmark_sequence(
511+
name: &str,
512+
object_backend: &ObjectBucket,
513+
) -> Result<LandmarkSequence> {
514+
let params = &CONFIG.logs[name];
515+
516+
let Some(landmark_sequence_bytes) = object_backend.fetch(LANDMARK_KEY).await? else {
517+
return Err("failed to get landmark sequence".into());
518+
};
519+
520+
let max_landmarks = params
521+
.max_certificate_lifetime_secs
522+
.div_ceil(params.landmark_interval_secs)
523+
+ 1;
524+
525+
let landmark_sequence = LandmarkSequence::from_bytes(&landmark_sequence_bytes, max_landmarks)
526+
.map_err(|e| e.to_string())?;
527+
528+
Ok(landmark_sequence)
529+
}
530+
445531
fn headers_from_http_metadata(meta: HttpMetadata) -> Headers {
446532
let h = Headers::new();
447533
if let Some(hdr) = meta.cache_control {

0 commit comments

Comments
 (0)