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 }