@@ -27,9 +27,11 @@ import (
2727 "errors"
2828 "fmt"
2929 "io"
30+ "log/slog"
3031 "net/http"
3132 "net/url"
3233 "strconv"
34+ "strings"
3335
3436 "github.com/transparency-dev/tessera/api/layout"
3537 "github.com/transparency-dev/tessera/client"
@@ -128,6 +130,11 @@ func (o *Options) validate() error {
128130// TODO(roger2hk): Should multiple mirrors in one client be supported?
129131type Client struct {
130132 opts * Options
133+
134+ // TODO(roger2hk): Add a mutex just in case client is used across multiple goroutines.
135+ // State of the mirror.
136+ oldSize uint64
137+ ticket []byte
131138}
132139
133140// NewClient creates a new Client with the provided options.
@@ -346,7 +353,19 @@ func (c *Client) streamEntries(ctx context.Context, uploadStart, uploadEnd uint6
346353}
347354
348355// pushCheckpoint sends a new checkpoint and its consistency proof to the mirror's /add-checkpoint endpoint.
349- func (c * Client ) pushCheckpoint (ctx context.Context , oldSize uint64 , proof [][]byte , checkpointRaw []byte ) error {
356+ func (c * Client ) pushCheckpoint (ctx context.Context , oldSize , targetSize uint64 , checkpointRaw []byte ) error {
357+ var proof [][]byte
358+ if oldSize > 0 {
359+ pb , err := client .NewProofBuilder (ctx , targetSize , c .opts .tileFetcher )
360+ if err != nil {
361+ return fmt .Errorf ("failed to create ProofBuilder: %v" , err )
362+ }
363+ proof , err = pb .ConsistencyProof (ctx , oldSize , targetSize )
364+ if err != nil {
365+ return fmt .Errorf ("failed to generate consistency proof: %v" , err )
366+ }
367+ }
368+
350369 reqBody := c .buildCheckpointRequestBody (oldSize , proof , checkpointRaw )
351370
352371 u , err := c .opts .mirrorURL .Parse ("add-checkpoint" )
@@ -369,9 +388,32 @@ func (c *Client) pushCheckpoint(ctx context.Context, oldSize uint64, proof [][]b
369388 _ , _ = io .Copy (io .Discard , resp .Body )
370389 _ = resp .Body .Close ()
371390 }()
391+ respBody , err := io .ReadAll (resp .Body )
392+ if err != nil {
393+ return fmt .Errorf ("failed to read body from witness at %q: %v" , u .String (), err )
394+ }
395+
396+ if resp .StatusCode == http .StatusConflict {
397+ // The witness MUST check that the old size matches the size of the latest checkpoint it cosigned
398+ // for the checkpoint's origin (or zero if it never cosigned a checkpoint for that origin).
399+ // If it doesn't match, the witness MUST respond with a "409 Conflict" HTTP status code.
400+ // The response body MUST consist of the tree size of the latest cosigned checkpoint in decimal,
401+ // followed by a newline (U+000A). The response MUST have a Content-Type of text/x.tlog.size
402+ if resp .Header .Get ("Content-Type" ) == "text/x.tlog.size" {
403+ bodyStr := strings .TrimSpace (string (respBody ))
404+ newWitSize , err := strconv .ParseUint (bodyStr , 10 , 64 )
405+ if err != nil {
406+ return fmt .Errorf ("witness at %q replied with x.tlog.size but body %q could not be parsed as decimal" , u .String (), bodyStr )
407+ }
408+ slog .InfoContext (ctx , "Witness replied with x.tlog.size different than our hint" , slog .String ("url" , u .String ()), slog .Uint64 ("reply" , newWitSize ), slog .Uint64 ("hinted" , oldSize ))
409+
410+ return ErrConflict {
411+ PendingSize : newWitSize ,
412+ }
413+ }
414+ }
372415
373416 if resp .StatusCode != http .StatusOK {
374- respBody , _ := io .ReadAll (resp .Body )
375417 return fmt .Errorf ("add-checkpoint failed with status %d: %s" , resp .StatusCode , string (respBody ))
376418 }
377419
@@ -404,47 +446,31 @@ func (c *Client) buildCheckpointRequestBody(oldSize uint64, proof [][]byte, chec
404446// Sync synchronizes all entries and the checkpoint from the source log to the mirror
405447// up to the specified targetSize. It returns the mirror's cosignatures on success.
406448func (c * Client ) Sync (ctx context.Context , targetCheckpointRaw []byte , targetSize uint64 ) ([]byte , error ) {
407- // Get the mirror's current state by querying it with upload_start=0, upload_end=0 (guaranteed to conflict).
408- _ , err := c .pushEntries (ctx , 0 , 0 , nil )
409- if err == nil {
410- return nil , errors .New ("unexpected success when querying mirror status with size 0" )
411- }
412-
413449 var conflict ErrConflict
414- if ! errors .As (err , & conflict ) {
415- return nil , fmt .Errorf ("failed to retrieve mirror status: %v" , err )
416- }
450+ nextEntry := c .oldSize
417451
418- oldSize := conflict .PendingSize
419- nextEntry := conflict .NextEntry
420- ticket := conflict .Ticket
421-
422- // If the mirror's pending checkpoint is smaller than target size, update it first.
423- if oldSize < targetSize {
424- var proof [][]byte
425- if oldSize > 0 {
426- pb , err := client .NewProofBuilder (ctx , targetSize , c .opts .tileFetcher )
427- if err != nil {
428- return nil , fmt .Errorf ("failed to create ProofBuilder: %v" , err )
429- }
430- proof , err = pb .ConsistencyProof (ctx , oldSize , targetSize )
431- if err != nil {
432- return nil , fmt .Errorf ("failed to generate consistency proof: %v" , err )
452+ // Push the checkpoint with the old size (0 if not provided).
453+ for c .oldSize < targetSize {
454+ err := c .pushCheckpoint (ctx , c .oldSize , targetSize , targetCheckpointRaw )
455+ if err != nil {
456+ if ! errors .As (err , & conflict ) {
457+ return nil , fmt .Errorf ("failed to push checkpoint: %v" , err )
433458 }
459+ c .oldSize = max (c .oldSize , conflict .PendingSize )
460+ continue
434461 }
435462
436- if err := c .pushCheckpoint (ctx , oldSize , proof , targetCheckpointRaw ); err != nil {
437- return nil , fmt .Errorf ("failed to push new checkpoint: %v" , err )
438- }
463+ nextEntry = c .oldSize
464+ c .oldSize = targetSize
439465 }
440466
441467 // Push entries up to target size in packages of 256, handling concurrent conflicts and retries.
442- uploadEnd := max (targetSize , oldSize )
468+ uploadEnd := max (targetSize , c . oldSize )
443469
444470 var cosigs []byte
445471 for {
446472 var err error
447- cosigs , err = c .pushEntries (ctx , nextEntry , uploadEnd , ticket )
473+ cosigs , err = c .pushEntries (ctx , nextEntry , uploadEnd , c . ticket )
448474 if err == nil {
449475 break
450476 }
@@ -453,11 +479,14 @@ func (c *Client) Sync(ctx context.Context, targetCheckpointRaw []byte, targetSiz
453479 return nil , fmt .Errorf ("sync failed during entry upload: %v" , err )
454480 }
455481
456- nextEntry = conflict .NextEntry
457- ticket = conflict .Ticket
458482 uploadEnd = conflict .PendingSize
483+ nextEntry = conflict .NextEntry
484+ c .ticket = conflict .Ticket
485+ c .oldSize = conflict .PendingSize
459486
460- // TODO(roger2hk): Handle the case uploadEnd is updated to a value smaller than targetSize.
487+ if uploadEnd < targetSize {
488+ return nil , fmt .Errorf ("mirror size reverted to %d, which is smaller than target %d" , uploadEnd , targetSize )
489+ }
461490 }
462491
463492 return cosigs , nil
0 commit comments