Skip to content

Add MMD verification in CT Hammer #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6dc37e7
Add MMD verification in CT Hammer
roger2hk Mar 7, 2025
a5a6681
Rearrange the `NewMMDVerifier` arguments
roger2hk Mar 10, 2025
c0b1c95
Update comment wording when leafMMDChan is full
roger2hk Mar 10, 2025
d1c6363
Break if there is any error during MMD verification
roger2hk Mar 10, 2025
e13d3fe
Replace `time.Unix` with `time.UnixMilli`
roger2hk Mar 10, 2025
8736695
Update comment for the use of ProofBuilder in MMD verifier
roger2hk Mar 10, 2025
5e70a73
Specify leafMMDChan channel direction in `NewMMDVerifier`
roger2hk Mar 10, 2025
c8939d3
Update comment for the use of ProofBuilder in MMD verifier
roger2hk Mar 10, 2025
22624bb
Simplify the `MMDVerifier` loop
roger2hk Mar 11, 2025
c6c42a1
Bump the buffer size of `leafMMDChan`
roger2hk Mar 11, 2025
643cbbf
Log when leafMMDChan is full
roger2hk Mar 11, 2025
fd99c44
Stop logging when the number of MMD verifiers is 0
roger2hk Mar 11, 2025
8c46bdd
Add a TODO to exit gracefully when the inclusion proof fails
roger2hk Mar 11, 2025
eaa9975
Move on to the next leaf if inclusion proof fails
roger2hk Mar 11, 2025
7fb3c47
Use the `cap` instead of `len` for `leafMMDChan` check
roger2hk Mar 11, 2025
091816d
Revert the `leafMMDChan` buffer size back to number of writers
roger2hk Mar 11, 2025
3c18e92
Fix nil pointer when there is nothing in the leafMMDChan
roger2hk Mar 11, 2025
304dc0a
Replace `break` with `continue` in `MMDVerifier` as `break` stops the…
roger2hk Mar 11, 2025
02f7de7
Add more comments for MMD verification
roger2hk Mar 11, 2025
8fb6474
Bump the `leafMMDChan` buffer size to `NumWriters*12`
roger2hk Mar 11, 2025
4b8fb44
Lower the `leafMMDChan` buffer size to `NumWriters*2`
roger2hk Mar 11, 2025
593fad1
Remove default case in `MMDVerifier`
roger2hk Mar 11, 2025
b1154cf
Reuse and update checkpoint and proof builder only if needed
roger2hk Mar 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deployment/modules/gcp/cloudbuild/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ resource "google_cloudbuild_trigger" "build_trigger" {
--logtostderr \
--num_writers=256 \
--max_write_ops=256 \
--num_mmd_verifiers=256 \
--leaf_write_goal=10000
EOT
wait_for = ["bearer_token"]
Expand Down
6 changes: 4 additions & 2 deletions internal/hammer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ Example usage to test a deployment of `cmd/gcp`:

```shell
go run ./internal/hammer \
--log_public_key=test-static-ct+59739ea1+BTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGbaLj7T8pSEfEYL6nbF8U1xLjoy+dBkL5pINuSaTZ6DTW2WQ1bdZ4lO8ZuAcGLtSRESI01di5ZskWwgRwphuiY= \
--log_public_key=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZtouPtPylIR8RgvqdsXxTXEuOjL50GQvmkg25JpNnoNNbZZDVt1niU7xm4BwYu1JERIjTV2LlmyRbCBHCmG6Jg== \
--log_url=https://storage.googleapis.com/transparency-dev-playground-test-static-ct-bucket \
--write_log_url=http://localhost:6962/test-static-ct
--max_read_ops=1024 \
--num_readers_random=128 \
--num_readers_full=128 \
--num_writers=256 \
--max_write_ops=42 \
--num_mmd_verifiers=256 \
--bearer_token=$(gcloud auth print-access-token)
```

Expand All @@ -34,12 +35,13 @@ If the timeout of 1 minute is reached first, then it exits with an exit code of

```shell
go run ./internal/hammer \
--log_public_key=test-static-ct+59739ea1+BTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGbaLj7T8pSEfEYL6nbF8U1xLjoy+dBkL5pINuSaTZ6DTW2WQ1bdZ4lO8ZuAcGLtSRESI01di5ZskWwgRwphuiY= \
--log_public_key=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZtouPtPylIR8RgvqdsXxTXEuOjL50GQvmkg25JpNnoNNbZZDVt1niU7xm4BwYu1JERIjTV2LlmyRbCBHCmG6Jg== \
--log_url=https://storage.googleapis.com/transparency-dev-playground-test-static-ct-bucket \
--write_log_url=http://localhost:6962/test-static-ct
--max_read_ops=0 \
--num_writers=512 \
--max_write_ops=512 \
--num_mmd_verifiers=512 \
--max_runtime=1m \
--leaf_write_goal=2500 \
--bearer_token=$(gcloud auth print-access-token) \
Expand Down
4 changes: 4 additions & 0 deletions internal/hammer/hammer.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ var (

maxWriteOpsPerSecond = flag.Int("max_write_ops", 0, "The maximum number of write operations per second")
numWriters = flag.Int("num_writers", 0, "The number of independent write tasks to run")
numMMDVerifiers = flag.Int("num_mmd_verifiers", 0, "The number of MMD verifiers performing inclusion proof for the added leaves")
mmdDuration = flag.Duration("mmd_duration", 10*time.Second, "The Maximum Merge Delay (MMD) duration of the log")

dupChance = flag.Float64("dup_chance", 0.1, "The probability of a generated leaf being a duplicate of a previous value")

Expand Down Expand Up @@ -160,6 +162,8 @@ func main() {
NumReadersRandom: *numReadersRandom,
NumReadersFull: *numReadersFull,
NumWriters: *numWriters,
NumMMDVerifiers: *numMMDVerifiers,
MMDDuration: *mmdDuration,
}
hammer := loadtest.NewHammer(&tracker, f.ReadEntryBundle, w, gen, ha.SeqLeafChan, ha.ErrChan, opts)

Expand Down
42 changes: 21 additions & 21 deletions internal/hammer/loadtest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,42 +155,42 @@ type httpLeafWriter struct {
bearerToken string
}

func (w httpLeafWriter) Write(ctx context.Context, newLeaf []byte) (uint64, error) {
func (w httpLeafWriter) Write(ctx context.Context, newLeaf []byte) (uint64, uint64, error) {
req, err := http.NewRequest(http.MethodPost, w.u.String(), bytes.NewReader(newLeaf))
if err != nil {
return 0, fmt.Errorf("failed to create request: %v", err)
return 0, 0, fmt.Errorf("failed to create request: %v", err)
}
if w.bearerToken != "" {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", w.bearerToken))
}
resp, err := w.hc.Do(req.WithContext(ctx))
if err != nil {
return 0, fmt.Errorf("failed to write leaf: %v", err)
return 0, 0, fmt.Errorf("failed to write leaf: %v", err)
}
body, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
return 0, fmt.Errorf("failed to read body: %v", err)
return 0, 0, fmt.Errorf("failed to read body: %v", err)
}
switch resp.StatusCode {
case http.StatusOK:
if resp.Request.Method != http.MethodPost {
return 0, fmt.Errorf("write leaf was redirected to %s", resp.Request.URL)
return 0, 0, fmt.Errorf("write leaf was redirected to %s", resp.Request.URL)
}
// Continue below
case http.StatusServiceUnavailable, http.StatusBadGateway, http.StatusGatewayTimeout:
// These status codes may indicate a delay before retrying, so handle that here:
time.Sleep(retryDelay(resp.Header.Get("RetryAfter"), time.Second))

return 0, fmt.Errorf("log not available. Status code: %d. Body: %q %w", resp.StatusCode, body, ErrRetry)
return 0, 0, fmt.Errorf("log not available. Status code: %d. Body: %q %w", resp.StatusCode, body, ErrRetry)
default:
return 0, fmt.Errorf("write leaf was not OK. Status code: %d. Body: %q", resp.StatusCode, body)
return 0, 0, fmt.Errorf("write leaf was not OK. Status code: %d. Body: %q", resp.StatusCode, body)
}
index, err := parseAddChainResponse(body)
index, timestamp, err := parseAddChainResponse(body)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe in a followup PR, you could check here that the SCT matches with the request.

if err != nil {
return 0, fmt.Errorf("write leaf failed to parse response: %v", body)
return 0, 0, fmt.Errorf("write leaf failed to parse response: %v", body)
}
return index, nil
return index, timestamp, nil
}

func retryDelay(retryAfter string, defaultDur time.Duration) time.Duration {
Expand All @@ -216,7 +216,7 @@ type roundRobinLeafWriter struct {
ws []httpLeafWriter
}

func (rr *roundRobinLeafWriter) Write(ctx context.Context, newLeaf []byte) (uint64, error) {
func (rr *roundRobinLeafWriter) Write(ctx context.Context, newLeaf []byte) (uint64, uint64, error) {
w := rr.next()
return w(ctx, newLeaf)
}
Expand All @@ -232,39 +232,39 @@ func (rr *roundRobinLeafWriter) next() LeafWriter {
}

// parseAddChainResponse parses the add-chain response and returns the leaf
// index from the extensions.
// index from the extensions and timestamp from the response.
// Code is inspired by https://github.com/FiloSottile/sunlight/blob/main/tile.go.
func parseAddChainResponse(body []byte) (uint64, error) {
func parseAddChainResponse(body []byte) (uint64, uint64, error) {
var resp types.AddChainResponse
if err := json.Unmarshal(body, &resp); err != nil {
return 0, fmt.Errorf("can't parse add-chain response: %v", err)
return 0, 0, fmt.Errorf("can't parse add-chain response: %v", err)
}

extensionBytes, err := base64.StdEncoding.DecodeString(resp.Extensions)
if err != nil {
return 0, fmt.Errorf("can't decode extensions: %v", err)
return 0, 0, fmt.Errorf("can't decode extensions: %v", err)
}
extensions := cryptobyte.String(extensionBytes)
var extensionType uint8
var extensionData cryptobyte.String
var leafIdx int64
if !extensions.ReadUint8(&extensionType) {
return 0, fmt.Errorf("can't read extension type")
return 0, 0, fmt.Errorf("can't read extension type")
}
if extensionType != 0 {
return 0, fmt.Errorf("wrong extension type %d, want 0", extensionType)
return 0, 0, fmt.Errorf("wrong extension type %d, want 0", extensionType)
}
if !extensions.ReadUint16LengthPrefixed(&extensionData) {
return 0, fmt.Errorf("can't read extension data")
return 0, 0, fmt.Errorf("can't read extension data")
}
if !readUint40(&extensionData, &leafIdx) {
return 0, fmt.Errorf("can't read leaf index from extension")
return 0, 0, fmt.Errorf("can't read leaf index from extension")
}
if !extensionData.Empty() ||
!extensions.Empty() {
return 0, fmt.Errorf("invalid data tile extensions: %v", resp.Extensions)
return 0, 0, fmt.Errorf("invalid data tile extensions: %v", resp.Extensions)
}
return uint64(leafIdx), nil
return uint64(leafIdx), resp.Timestamp, nil
}

// readUint40 decodes a big-endian, 40-bit value into out and advances over it.
Expand Down
17 changes: 16 additions & 1 deletion internal/hammer/loadtest/hammer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,38 @@ type HammerOpts struct {
NumReadersRandom int
NumReadersFull int
NumWriters int
NumMMDVerifiers int
MMDDuration time.Duration
}

func NewHammer(tracker *client.LogStateTracker, f client.EntryBundleFetcherFunc, w LeafWriter, gen func() []byte, seqLeafChan chan<- LeafTime, errChan chan<- error, opts HammerOpts) *Hammer {
readThrottle := NewThrottle(opts.MaxReadOpsPerSecond)
writeThrottle := NewThrottle(opts.MaxWriteOpsPerSecond)

var leafMMDChan chan LeafMMD
if opts.NumMMDVerifiers > 0 {
leafMMDChan = make(chan LeafMMD, opts.NumWriters*2)
}

randomReaders := NewWorkerPool(func() Worker {
return NewLeafReader(tracker, f, RandomNextLeaf(), readThrottle.TokenChan, errChan)
})
fullReaders := NewWorkerPool(func() Worker {
return NewLeafReader(tracker, f, MonotonicallyIncreasingNextLeaf(), readThrottle.TokenChan, errChan)
})
writers := NewWorkerPool(func() Worker {
return NewLogWriter(w, gen, writeThrottle.TokenChan, errChan, seqLeafChan)
return NewLogWriter(w, gen, writeThrottle.TokenChan, errChan, seqLeafChan, leafMMDChan)
Copy link
Collaborator

Choose a reason for hiding this comment

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

In a followup PR, you could also add a leafMMDChan to randomReaders. MMDVerifier does two things really:

  1. checks that input matches the hases in the tree
  2. make sure that 1. passes within the MMD

From writer, input is what the hammer has generated - and that's great! After that check, it would still be possible for leaf bundles and L0 hashes not to match.
From reader, input would be leaf bundles read from the tree. Leaf bundles and L0 hashes must match for this test to pass.

})
mmdVerifiers := NewWorkerPool(func() Worker {
return NewMMDVerifier(tracker, opts.MMDDuration, errChan, leafMMDChan)
})

return &Hammer{
opts: opts,
randomReaders: randomReaders,
fullReaders: fullReaders,
writers: writers,
mmdVerifiers: mmdVerifiers,
readThrottle: readThrottle,
writeThrottle: writeThrottle,
tracker: tracker,
Expand All @@ -66,6 +77,7 @@ type Hammer struct {
randomReaders WorkerPool
fullReaders WorkerPool
writers WorkerPool
mmdVerifiers WorkerPool
readThrottle *Throttle
writeThrottle *Throttle
tracker *client.LogStateTracker
Expand All @@ -82,6 +94,9 @@ func (h *Hammer) Run(ctx context.Context) {
for i := 0; i < h.opts.NumWriters; i++ {
h.writers.Grow(ctx)
}
for i := 0; i < h.opts.NumMMDVerifiers; i++ {
h.mmdVerifiers.Grow(ctx)
}

go h.readThrottle.Run(ctx)
go h.writeThrottle.Run(ctx)
Expand Down
2 changes: 2 additions & 0 deletions internal/hammer/loadtest/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,13 @@ func (c *tuiController) Run(ctx context.Context) {
c.hammer.randomReaders.Grow(ctx)
c.hammer.fullReaders.Grow(ctx)
c.hammer.writers.Grow(ctx)
c.hammer.mmdVerifiers.Grow(ctx)
case 'W':
klog.Info("Decreasing the number of workers")
c.hammer.randomReaders.Shrink(ctx)
c.hammer.fullReaders.Shrink(ctx)
c.hammer.writers.Shrink(ctx)
c.hammer.mmdVerifiers.Shrink(ctx)
}
return event
})
Expand Down
Loading
Loading