Skip to content

Commit 2cc18dd

Browse files
committed
mirror_worker: implement the add-entries submission API
Stream and verify entry packages against the target pending checkpoint, persist them as tlog-tiles entry bundles and hash tiles (see commit.rs), and advance the persisted-entry frontier. A complete upload cosigns the mirror checkpoint and returns 200; a client-truncated upload persists the verified prefix and returns 202 with the advanced next entry so the client can resume (C2SP/C2SP#253). Persist incrementally every `commit_packages` packages (default 32) so a long or interrupted upload advances the frontier as it streams rather than only at the end; each flush commits from the last persisted frontier and is resumable. Includes body-decoding (transparent gzip), a streaming buffer adapter, per-origin prefixed R2 storage, and the ticket-sealer for recovering a past pending checkpoint on resume.
1 parent 7004cca commit 2cc18dd

15 files changed

Lines changed: 2385 additions & 15 deletions

Cargo.lock

Lines changed: 48 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
@@ -121,6 +121,7 @@ der = { version = "0.8", features = ["oid"] }
121121
# (crypto-common 0.2 / signature 3.0), so it remains pinned to a pre-release.
122122
# The rest of the batch is now stable.
123123
ed25519-dalek = { version = "3.0.0", features = ["pem"] }
124+
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
124125
futures-executor = "0.3.31"
125126
futures-util = "0.3.31"
126127
getrandom = { version = "0.4", features = ["wasm_js"] }
@@ -157,6 +158,7 @@ tlog_checkpoint = { path = "crates/tlog_checkpoint", version = "0.2.0" }
157158
tlog_core = { path = "crates/tlog_core", version = "0.2.0" }
158159
tlog_cosignature = { path = "crates/tlog_cosignature", version = "0.2.0" }
159160
tlog_entry = { path = "crates/tlog_entry", version = "0.2.0" }
161+
tlog_mirror = { path = "crates/tlog_mirror", version = "0.2.0" }
160162
tlog_tiles = { path = "crates/tlog_tiles", version = "0.2.0" }
161163
tlog_witness = { path = "crates/tlog_witness", version = "0.2.0" }
162164
tokio = { version = "1", features = ["sync"] }

crates/mirror_worker/.dev.vars

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
MIRROR_SIGNING_KEY="-----BEGIN PRIVATE KEY-----\nMDQCAQAwCwYJYIZIAWUDBAMRBCKAIEJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJC\nQkJCQkJC\n-----END PRIVATE KEY-----\n"
2+
MIRROR_TICKET_KEY="Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc="

crates/mirror_worker/Cargo.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,33 +32,39 @@ serde_json.workspace = true
3232

3333
[dependencies]
3434
axum.workspace = true
35+
base64.workspace = true
3536
config = { path = "./config", package = "mirror_worker_config" }
3637
console_error_panic_hook.workspace = true
3738
console_log.workspace = true
39+
flate2.workspace = true
40+
futures-util.workspace = true
3841
generic_log_worker.workspace = true
3942
getrandom.workspace = true
4043
getrandom_03.workspace = true
4144
hex.workspace = true
45+
length_prefixed.workspace = true
4246
log.workspace = true
4347
ml-dsa.workspace = true
4448
pkcs8.workspace = true
4549
serde.workspace = true
4650
serde_json.workspace = true
4751
serde_with.workspace = true
52+
sha2.workspace = true
4853
signed_note.workspace = true
4954
tlog_checkpoint.workspace = true
5055
tlog_core.workspace = true
5156
tlog_cosignature.workspace = true
57+
tlog_mirror.workspace = true
58+
tlog_tiles.workspace = true
5259
tlog_witness.workspace = true
5360
tower-service.workspace = true
5461
worker = { workspace = true, features = ["http", "axum"] }
5562

5663
[dev-dependencies]
57-
# base64 is used only by the dev-config pin tests; ed25519-dalek only to
58-
# check that a non-ML-DSA-44 MIRROR_SIGNING_KEY is rejected. The shipped
59-
# worker is ML-DSA-44 only.
60-
base64.workspace = true
64+
# Ed25519 is used only by unit tests, to check that a non-ML-DSA-44
65+
# MIRROR_SIGNING_KEY is rejected. The shipped worker is ML-DSA-44 only.
6166
ed25519-dalek.workspace = true
67+
tokio = { workspace = true, features = ["macros", "rt"] }
6268

6369
[lints.rust]
6470
unexpected_cfgs = { level = "warn", check-cfg = [

crates/mirror_worker/config.dev.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"submission_prefix": "http://localhost:8787/",
66
"monitoring_prefix": "http://localhost:8787/",
77
"clean_interval_secs": 5,
8+
"commit_packages": 2,
89
"logs": {
910
"oid/1.3.6.1.4.1.32473.2": {
1011
"description": "Dev-only MTC CA cosigner. Key name is the CA ID; the mirror serves log numbers 1-6 as origins oid/1.3.6.1.4.1.32473.2.0.<N>. log_public_keys holds a dev-only ML-DSA-44 SPKI.",

crates/mirror_worker/config.schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
"default": 3600,
3131
"description": "How often (in seconds) the per-origin partial-tile cleaner wakes to clean orphaned partial tiles from object storage. Defaults to 3600 (one hour) when omitted."
3232
},
33+
"commit_packages": {
34+
"type": "integer",
35+
"minimum": 1,
36+
"maximum": 1024,
37+
"default": 32,
38+
"description": "How many entry packages add-entries verifies before flushing them to storage and advancing the persisted-entry frontier. Bounds in-memory buffering and gives durable mid-request progress on large uploads. Defaults to 32 (the recommended per-request package budget) when omitted; capped at 1024 to bound worst-case buffering."
39+
},
3340
"logs": {
3441
"type": "object",
3542
"description": "CAs this mirror mirrors, keyed by log_key_name: the CA cosigner's note-signature name (the CA ID) on the checkpoints it ingests. Used as a signed-note key name at runtime, so per c2sp.org/signed-note it MUST NOT contain '+', whitespace, or control characters.",

crates/mirror_worker/config/src/lib.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ pub struct AppConfig {
6262
/// back to a one-hour default (see [`Self::clean_interval_secs`]).
6363
/// Consumed by [`mirror_worker`](../mirror_worker/)'s `cleaner_do`.
6464
pub clean_interval_secs: Option<u64>,
65+
/// How many entry packages the `add-entries` handler verifies before
66+
/// flushing them to storage and advancing the persisted-entry
67+
/// frontier. Bounds in-memory buffering and gives durable mid-request
68+
/// progress on large uploads. `None` falls back to a default of 32
69+
/// (see [`Self::commit_packages`]). Consumed by
70+
/// [`mirror_worker`](../mirror_worker/)'s `add_entries`.
71+
pub commit_packages: Option<u64>,
6572
/// CAs this mirror mirrors, keyed by `log_key_name`: the CA
6673
/// cosigner's note-signature name (the CA ID) carried by the
6774
/// checkpoints it ingests.
@@ -118,6 +125,17 @@ impl AppConfig {
118125
self.clean_interval_secs.unwrap_or(3600)
119126
}
120127

128+
/// How many entry packages `add-entries` commits per flush, falling
129+
/// back to 32 when `commit_packages` is unset. 32 matches the
130+
/// per-request package budget clients are recommended to stay within
131+
/// (tlog-mirror "Implementation Considerations"), so a compliant
132+
/// single-request upload still commits once, while larger uploads
133+
/// flush every 32 packages instead of buffering the whole body.
134+
#[must_use]
135+
pub fn commit_packages(&self) -> u64 {
136+
self.commit_packages.unwrap_or(32)
137+
}
138+
121139
/// Validate the configuration beyond what `serde` and the JSON schema
122140
/// can express.
123141
///
@@ -141,6 +159,10 @@ impl AppConfig {
141159
/// signed-note key name length cap, since each origin is itself
142160
/// used as a checkpoint origin.
143161
///
162+
/// Simple single-field bounds (e.g. `commit_packages` and the
163+
/// log-number ranges) are expressed in `config.schema.json` and
164+
/// enforced by the build script, so they are not re-checked here.
165+
///
144166
/// `log_key_name` uniqueness across log entries is not checked here;
145167
/// it is enforced earlier, during deserialization (see
146168
/// [`deserialize_logs`]). A plain `serde_json` object silently keeps
@@ -336,6 +358,7 @@ mod tests {
336358
submission_prefix: "https://mirror.example/".to_owned(),
337359
monitoring_prefix: None,
338360
clean_interval_secs: None,
361+
commit_packages: None,
339362
logs: HashMap::from([(
340363
"example.com/log1".to_owned(),
341364
LogParams {
@@ -460,6 +483,14 @@ mod tests {
460483
.expect("a valid log-number window is accepted");
461484
}
462485

486+
#[test]
487+
fn commit_packages_defaults_to_32() {
488+
let mut cfg = good_app_config();
489+
assert_eq!(cfg.commit_packages(), 32);
490+
cfg.commit_packages = Some(8);
491+
assert_eq!(cfg.commit_packages(), 8);
492+
}
493+
463494
#[test]
464495
fn validate_rejects_inverted_window() {
465496
let cfg = with_log(|log| {
@@ -528,6 +559,7 @@ mod tests {
528559
submission_prefix: "https://mirror.example/".to_owned(),
529560
monitoring_prefix: None,
530561
clean_interval_secs: None,
562+
commit_packages: None,
531563
logs: HashMap::from([(
532564
"a".repeat(250),
533565
LogParams {

0 commit comments

Comments
 (0)