github.com/google/trillian-examples@v0.0.0-20240520080811-0d40d35cef0e/clone/cmd/sumdbclone/sumdbclone.go (about)

     1  // Copyright 2021 Google LLC. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // sumdbclone is a one-shot tool for downloading entries from sum.golang.org.
    16  package main
    17  
    18  import (
    19  	"context"
    20  	"flag"
    21  	"fmt"
    22  	"net/http"
    23  	"time"
    24  
    25  	"github.com/cenkalti/backoff/v4"
    26  	"github.com/golang/glog"
    27  	sdbclient "github.com/google/trillian-examples/clone/cmd/sumdbclone/internal/client"
    28  	"github.com/google/trillian-examples/clone/internal/cloner"
    29  	"github.com/google/trillian-examples/clone/logdb"
    30  
    31  	_ "github.com/go-sql-driver/mysql"
    32  )
    33  
    34  var (
    35  	vkey           = flag.String("vkey", "sum.golang.org+033de0ae+Ac4zctda0e5eza+HJyk9SxEdh+s3Ux18htTTAD8OuAn8", "The verification key for the log checkpoints")
    36  	url            = flag.String("url", "https://sum.golang.org", "The base URL for the sumdb HTTP API.")
    37  	mysqlURI       = flag.String("mysql_uri", "", "URL of a MySQL database to clone the log into. The DB should contain only one log.")
    38  	writeBatchSize = flag.Uint("write_batch_size", 1024, "The number of leaves to write in each DB transaction.")
    39  	workers        = flag.Uint("workers", 4, "The number of worker threads to run in parallel to fetch entries.")
    40  	timeout        = flag.Duration("timeout", 10*time.Second, "Maximum time to wait for http connections to complete.")
    41  	pollInterval   = flag.Duration("poll_interval", 0, "How often to poll the log for new checkpoints once all entries have been downloaded. Set to 0 to exit after download.")
    42  )
    43  
    44  const (
    45  	tileHeight = 8
    46  )
    47  
    48  func main() {
    49  	flag.Parse()
    50  
    51  	if len(*mysqlURI) == 0 {
    52  		glog.Exit("Missing required parameter 'mysql_uri'")
    53  	}
    54  
    55  	ctx := context.Background()
    56  	db, err := logdb.NewDatabase(*mysqlURI)
    57  	if err != nil {
    58  		glog.Exitf("Failed to connect to database: %q", err)
    59  	}
    60  
    61  	client := sdbclient.NewSumDB(tileHeight, *vkey, *url, &http.Client{
    62  		Timeout: *timeout,
    63  	})
    64  	cloneAndVerify(ctx, client, db)
    65  	if *pollInterval == 0 {
    66  		return
    67  	}
    68  	ticker := time.NewTicker(*pollInterval)
    69  	for {
    70  		select {
    71  		case <-ticker.C:
    72  			cloneAndVerify(ctx, client, db)
    73  		case <-ctx.Done():
    74  			glog.Exit(ctx.Err())
    75  		}
    76  	}
    77  }
    78  
    79  // cloneAndVerify verifies the downloaded leaves with the target checkpoint, and if it verifies, persists the checkpoint.
    80  func cloneAndVerify(ctx context.Context, client *sdbclient.SumDBClient, db *logdb.Database) {
    81  	targetCp, err := client.LatestCheckpoint()
    82  	if err != nil {
    83  		glog.Exitf("Failed to get latest checkpoint from log: %v", err)
    84  	}
    85  	glog.Infof("Target checkpoint is for tree size %d", targetCp.N)
    86  
    87  	if err := clone(ctx, db, client, targetCp); err != nil {
    88  		glog.Exitf("Failed to clone log: %v", err)
    89  	}
    90  }
    91  
    92  func clone(ctx context.Context, db *logdb.Database, client *sdbclient.SumDBClient, targetCp *sdbclient.Checkpoint) error {
    93  	fullTileSize := 1 << tileHeight
    94  	cl := cloner.New(*workers, uint(fullTileSize), *writeBatchSize, db)
    95  
    96  	next, err := cl.Next()
    97  	if err != nil {
    98  		return fmt.Errorf("couldn't determine first leaf to fetch: %v", err)
    99  	}
   100  	if next >= uint64(targetCp.N) {
   101  		glog.Infof("No work to do. Local tree size = %d, latest log tree size = %d", next, targetCp.N)
   102  		return nil
   103  	}
   104  
   105  	// batchFetch gets full or partial tiles depending on the number of leaves requested.
   106  	// start must always be the first index within a tile or it is being used wrong.
   107  	batchFetch := func(start uint64, leaves [][]byte) (uint64, error) {
   108  		if start%uint64(fullTileSize) > 0 {
   109  			return 0, backoff.Permanent(fmt.Errorf("%d is not the first leaf in a tile", start))
   110  		}
   111  		offset := int(start >> tileHeight)
   112  
   113  		var got [][]byte
   114  		var err error
   115  		if len(leaves) == fullTileSize {
   116  			got, err = client.FullLeavesAtOffset(offset)
   117  		} else {
   118  			got, err = client.PartialLeavesAtOffset(offset, len(leaves))
   119  		}
   120  		if err != nil {
   121  			return 0, fmt.Errorf("failed to get leaves at offset %d: %v", offset, err)
   122  		}
   123  		copy(leaves, got)
   124  		return uint64(len(leaves)), nil
   125  	}
   126  
   127  	// Download any remainder of a previous partial tile before calling Clone,
   128  	// which is optimized for chunks perfectly aligning with tiles.
   129  	if rem := next % uint64(fullTileSize); rem > 0 {
   130  		tileStart := next - rem
   131  		needed := fullTileSize
   132  		if d := targetCp.N - int64(tileStart); int(d) < needed {
   133  			needed = int(d)
   134  		}
   135  		leaves := make([][]byte, needed)
   136  		glog.Infof("Next=%d does not align with tile boundary; prefetching tile [%d, %d)", next, tileStart, tileStart+uint64(len(leaves)))
   137  		if _, err := batchFetch(tileStart, leaves); err != nil {
   138  			return err
   139  		}
   140  		if err := db.WriteLeaves(ctx, next, leaves[rem:]); err != nil {
   141  			return fmt.Errorf("failed to write to DB for batch starting at %d: %q", next, err)
   142  		}
   143  	}
   144  
   145  	cp := cloner.UnwrappedCheckpoint{
   146  		Size: uint64(targetCp.N),
   147  		Hash: targetCp.Hash[:],
   148  		Raw:  targetCp.Raw,
   149  	}
   150  	// The database must now contain only complete tiles, or else be matched with
   151  	// the targetCp. Either way, the preconditions for the cloner configuration are met.
   152  	if err := cl.CloneAndVerify(ctx, batchFetch, cp); err != nil {
   153  		return fmt.Errorf("failed to clone and verify log: %v", err)
   154  	}
   155  	return nil
   156  }