Skip to content

mtpublisher: Replace the stub with a tlog-mirror client - #8973

Open
beautifulentropy wants to merge 1 commit into
paving-a-path-to-a-proper-publisher-07from
paving-a-path-to-a-proper-publisher-08
Open

mtpublisher: Replace the stub with a tlog-mirror client#8973
beautifulentropy wants to merge 1 commit into
paving-a-path-to-a-proper-publisher-07from
paving-a-path-to-a-proper-publisher-08

Conversation

@beautifulentropy

Copy link
Copy Markdown
Member

No description provided.

@beautifulentropy
beautifulentropy marked this pull request as ready for review August 24, 2026 16:18
@beautifulentropy
beautifulentropy requested a review from a team as a code owner August 24, 2026 16:18
@beautifulentropy
beautifulentropy requested a review from jsha August 24, 2026 16:18
@github-actions

Copy link
Copy Markdown
Contributor

@beautifulentropy, this PR appears to contain configuration and/or SQL schema changes. Please ensure that a corresponding deployment ticket has been filed with the new values.

Comment on lines 12 to +36
@@ -25,25 +25,29 @@ type Config struct {

DebugAddr string `validate:"omitempty,hostname_port"`

// PollInterval is how often the stub scans for checkpoints that still
// lack a mirror cosignature.
// PollInterval is how often the publisher scans for checkpoints that
// still lack a mirror cosignature.
PollInterval config.Duration `validate:"required"`

// LogID identifies the issuance log this publisher operates on. It must
// match the mtca's.
LogID issuancelog.ID `validate:"required"`

// MirrorID identifies the cosigner this publisher writes alongside each
// cosignature (e.g. "32473.9").
MirrorID string `validate:"required"`
// MTCAPublicKeyFile holds the PEM-encoded ML-DSA-44 public key the mtca

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we could alternately take the CA certificate file here, which would save us from having to manage a separate artifact containing just the CA public key. Even though our CA certificates are unsigned, Go can parse them - it just can't verify them.

Comment on lines +44 to +46
// MirrorBaseURL is the base URL of the mirror's tlog-mirror submission
// endpoints (e.g. "http://localhost:4700").
MirrorBaseURL string `validate:"required,url"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this belong in cmd.MirrorConfig instead?

Also, I was wondering: does this need to include some CA-specific prefix also, e.g. the CA ID?

Turns out, no: there's just one submission prefix for a given mirror. The relevant submission endpoints (add-checkpoint, add-entries, sign-subtree) carry the issuance log's origin in the request body. https://github.com/C2SP/C2SP/blob/main/tlog-mirror.md#introduction

Comment thread mtpublisher/mirror.go
}
var proof []tlog.Hash
if oldSize > 0 && oldSize < tree.N {
treeProof, err := m.src.consistencyProof(ctx, tree, oldSize)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means we post with an empty consistency proof for two cases:

  1. empty issuance log.
  2. pushing a checkpoint at the same size the mirror already has.

For (1) - I think we'll never have that, right? We always initialize our trees with a null_entry and sign a tree size of 1.

For (2): I had to think about this a bit, but I think it makes sense. It's possible for all mirrors to be fully up-to-date with our latest checkpoint, but have no MirrorSignature in the database, for instance if we previously sync'ed those mirrors but failed to write to the database. Of course we don't want to wedge in such a case, so we push the current checkpoint even though it's "old news", then send an empty add-entries request, and get the mirror's note signature line in the response.

Perhaps that's worth a comment in the addCheckpoint and Cosign doccomments? For instance "Makes requests even when the mirror is up-to-date, in order to get a mirror cosignature."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also possible for a mirror to become unexpectedly up-to-date because someone else is mirroring faster than we are.

Comment thread mtpublisher/mirror.go
Comment on lines +153 to +157
if retried {
return errors.New("mirror rejected the old size twice")
}
retried = true
oldSize, err = mirror.ParseSizeResponse(respBody)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move the error return below ParseSizeResponse so we can include the parsed size in the error message. E.g.:

return errors.New("add-checkpoint at tree size %d got 409 with mirror tree size %d (after retry)",
   ...)

Comment thread mtpublisher/mirror.go
return err
}
default:
return fmt.Errorf("mirror returned status %d: %s", status, strings.TrimSpace(string(respBody)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's trim the size of the response body even more aggressively than maxMirrorResponseSize. I think we probably don't want more than, say, 400 bytes of the response for debugging purposes.

Comment thread mtpublisher/mirror.go
if info.TreeSize != tree.N {
return nil, fmt.Errorf("mirror wants upload_end %d, checkpoint size is %d", info.TreeSize, tree.N)
}
start = info.NextEntry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do some bounds checking on the NextEntry the mirror sent us. It should not be greater than or equal to the issuance log's tree size. It should not be less than zero.

Comment thread mtpublisher/mirror.go
return nil, err
}
if info.TreeSize != tree.N {
return nil, fmt.Errorf("mirror wants upload_end %d, checkpoint size is %d", info.TreeSize, tree.N)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/C2SP/C2SP/blob/main/tlog-mirror.md#add-entries

When sending a "409 Conflict" or "202 Accepted" response, the response body MUST have a Content-Type of text/x.tlog.mirror-info and consist of three lines, each followed by a newline (U+000A):

The tree size of a valid pending checkpoint, in decimal
...
If the client's upload_end value was valid, the first line SHOULD contain upload_end. This allows the client to resume an interrupted upload without recomputing subtree consistency proofs. Otherwise, the first line SHOULD be the tree size of the current pending checkpoint.

After receiving a "409 Conflict" or "202 Accepted" response, the client SHOULD retry setting upload_end to the tree size, upload_start to the advertised next entry value, and the ticket to the received ticket.

The spec wants us to retry with an updated upload_end.

However, if we were to do that, the eventual "200 Success" response would give us note signature lines over a checkpoint at the wrong tree size (the new upload_end), not the one we asked for.

Since we're violating a SHOULD, let's comment. E.g. "Don't set upload_start to the tree size from the response, because we need upload_end to be the tree size of the checkpoint we are seeking a cosignature for. A cosignature on a different tree size won't do."

But I think we don't have to error out in this case, and probably shouldn't. The log's obligations with regards to this value are also only "SHOULD."

Perhaps in a future revision we want to implement "retry with updated upload_end to try to advance the log, but then return an error instead of a cosignature."

Comment thread mtpublisher/source.go
}
proof, err := subtree.ConsistencyProof(p.SubtreeStart, p.End, tree.N, s.hashReaderForTree(ctx, tree))
if err != nil {
return nil, fmt.Errorf("proving subtree [%d, %d): %s", p.SubtreeStart, p.End, err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, fmt.Errorf("proving subtree [%d, %d): %s", p.SubtreeStart, p.End, err)
return nil, fmt.Errorf("fetching subtree consistency proof [%d, %d), tree size %d: %s", p.SubtreeStart, p.End, tree.N, err)

Rationale: make it clear that it was a fetch error, not a verification error. Also, a subtree consistency proof is a function of start, end, and tree size, so include the tree size in the error.

`SELECT id, checkpoints.mtcLogID, mtcaSignature, mirrorID,
mirrorSignature, treeSize, rootHash
`SELECT id, checkpoints.mtcLogID, mtcaSignature,
COALESCE(mirrorID, '') AS mirrorID,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is a nice trick, and makes #8968 slightly redundant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants