@@ -50,8 +50,8 @@ use crate::{
5050 frontend_worker:: { ApiResult , AppError } ,
5151 load_mirror_signer, load_ticket_sealer, log_verifiers,
5252 mirror_state_do:: {
53- AdvanceNextEntryRequest , CommitRequest , MirrorStateSnapshot , NextEntry , PendingCheckpoint ,
54- state_stub,
53+ AdvanceNextEntryRequest , CommitRequest , CommittedCheckpoint , MirrorStateSnapshot ,
54+ NextEntry , PendingCheckpoint , state_stub,
5555 } ,
5656 storage:: load_origin_bucket,
5757} ;
@@ -337,7 +337,7 @@ async fn verify_and_persist(
337337 ) ) ;
338338 }
339339
340- cosign_and_serve ( env, header, target) . await
340+ cosign_and_serve ( env, header, target, snapshot ) . await
341341}
342342
343343/// The spec's `excess_entries = min(upload_end, next_entry) -
@@ -387,6 +387,13 @@ fn read_package(cursor: &mut Cursor<&[u8]>, num_entries: u64) -> PackageOutcome
387387/// concurrent commits cannot rewind the served checkpoint. The frontend
388388/// therefore does not write R2 itself.
389389///
390+ /// If the DO reports a committed size ahead of `upload_end`, a concurrent
391+ /// `add-entries` already advanced the mirror checkpoint past this upload.
392+ /// Per the spec's final step ("If `upload_end` was too small, the mirror
393+ /// MUST respond with a 409 Conflict") this returns a 409 with mirror-info
394+ /// so the client resyncs, rather than a 200 that would falsely claim the
395+ /// client's smaller checkpoint is being served.
396+ ///
390397/// # Errors
391398///
392399/// Returns an error if the target note/checkpoint fails to parse, signing
@@ -395,6 +402,7 @@ async fn cosign_and_serve(
395402 env : & Env ,
396403 header : & AddEntriesRequestHeader ,
397404 target : & PendingCheckpoint ,
405+ snapshot : & MirrorStateSnapshot ,
398406) -> ApiResult < axum:: response:: Response > {
399407 // The checkpoint text comes from the log-signed pending note; the
400408 // response is the bare cosignature line(s), identical to a witness's
@@ -415,7 +423,7 @@ async fn cosign_and_serve(
415423 // durable checkpoint under its commit lock.
416424 let mut checkpoint_obj = target. signed_note_bytes . clone ( ) ;
417425 checkpoint_obj. extend_from_slice ( & cosig_body) ;
418- dispatch_commit (
426+ let committed = dispatch_commit (
419427 env,
420428 & header. log_origin ,
421429 & CommitRequest {
@@ -426,6 +434,18 @@ async fn cosign_and_serve(
426434 )
427435 . await ?;
428436
437+ if committed. size != header. upload_end {
438+ // The DO refused to rewind: a concurrent commit already advanced
439+ // the mirror checkpoint past upload_end, so ours was skipped.
440+ log:: info!(
441+ "add-entries: commit skipped, mirror checkpoint {} already past upload_end {}; \
442+ returning 409",
443+ committed. size,
444+ header. upload_end,
445+ ) ;
446+ return Ok ( mirror_info_409 ( env, snapshot, & header. log_origin ) ) ;
447+ }
448+
429449 Ok ( (
430450 StatusCode :: OK ,
431451 [ ( CONTENT_TYPE , "text/plain; charset=utf-8" ) ] ,
@@ -473,10 +493,18 @@ async fn first_package_prefix(
473493}
474494
475495/// POST the [`CommitRequest`] to the per-origin DO, advancing the mirror
476- /// checkpoint. A non-200 status (or a `/commit` beyond pending) is a
477- /// frontend/mirror bug, so it is surfaced as a transport error that the
478- /// handler maps to 500.
479- async fn dispatch_commit ( env : & Env , origin : & str , commit_req : & CommitRequest ) -> Result < ( ) > {
496+ /// checkpoint, and return the DO's resulting [`CommittedCheckpoint`].
497+ ///
498+ /// The returned checkpoint may be ahead of `commit_req` when a concurrent
499+ /// `add-entries` already advanced past it (the DO refuses to rewind); the
500+ /// caller compares sizes to detect that skip. A non-200 status (a
501+ /// `/commit` beyond the persisted frontier) is a frontend/mirror bug,
502+ /// surfaced as a transport error that the handler maps to 500.
503+ async fn dispatch_commit (
504+ env : & Env ,
505+ origin : & str ,
506+ commit_req : & CommitRequest ,
507+ ) -> Result < CommittedCheckpoint > {
480508 let stub = state_stub ( env, origin) ?;
481509 let mut resp = stub
482510 . fetch_with_request ( Request :: new_with_init (
@@ -494,7 +522,7 @@ async fn dispatch_commit(env: &Env, origin: &str, commit_req: &CommitRequest) ->
494522 ) ?)
495523 . await ?;
496524 match resp. status_code ( ) {
497- 200 => Ok ( ( ) ) ,
525+ 200 => Ok ( resp . json ( ) . await ? ) ,
498526 status => {
499527 let msg = resp. text ( ) . await . unwrap_or_default ( ) ;
500528 log:: error!( "add-entries: DO /commit returned {status}: {msg}" ) ;
0 commit comments