From a020b3b7ceea98590e5efc235b90db5184ac035e Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Thu, 16 Jan 2025 17:31:35 -0600 Subject: [PATCH] Cache checkpoint for inactive shards On service startup, Rekor will sign checkpoints for the inactive shards, since inactive tree lengths do not change. The calls to CreateAndSignCheckpoint that were not updated are because the checkpoint is being signed only for the active shard, e.g. on entry upload and when returning the active shard checkpoint. Signed-off-by: Hayden Blauzvern --- pkg/api/api.go | 29 +++++++++++++++++++++++++++++ pkg/api/entries.go | 27 +++++++++++++++++---------- pkg/api/tlog.go | 12 +++--------- tests/sharding-e2e-test.sh | 22 ++++++++++++++++++++++ 4 files changed, 71 insertions(+), 19 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 5b267e3c6..0b41c0c6a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -25,10 +25,12 @@ import ( "strings" "github.com/google/trillian" + "github.com/google/trillian/types" "github.com/redis/go-redis/v9" "github.com/spf13/viper" "golang.org/x/exp/slices" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" @@ -39,6 +41,7 @@ import ( "github.com/sigstore/rekor/pkg/signer" "github.com/sigstore/rekor/pkg/storage" "github.com/sigstore/rekor/pkg/trillianclient" + "github.com/sigstore/rekor/pkg/util" "github.com/sigstore/rekor/pkg/witness" _ "github.com/sigstore/rekor/pkg/pubsub/gcp" // Load GCP pubsub implementation @@ -95,6 +98,11 @@ type API struct { // Publishes notifications when new entries are added to the log. May be // nil if no publisher is configured. newEntryPublisher pubsub.Publisher + // Stores map of inactive tree IDs to checkpoints + // Inactive shards will always return the same checkpoint, + // so we can fetch the checkpoint on service startup to + // minimize signature generations + cachedCheckpoints map[int64]string } func NewAPI(treeID uint) (*API, error) { @@ -132,6 +140,26 @@ func NewAPI(treeID uint) (*API, error) { return nil, fmt.Errorf("unable get sharding details from sharding config: %w", err) } + cachedCheckpoints := make(map[int64]string) + for _, r := range ranges.GetInactive() { + tc := trillianclient.NewTrillianClient(ctx, logClient, r.TreeID) + resp := tc.GetLatest(0) + if resp.Status != codes.OK { + return nil, fmt.Errorf("error with GetLatest(): resp code is %d", resp.Status) + } + result := resp.GetLatestResult + root := &types.LogRootV1{} + if err := root.UnmarshalBinary(result.SignedLogRoot.LogRoot); err != nil { + return nil, fmt.Errorf("error unmarshalling root: %w", err) + } + + cp, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), r.TreeID, uint64(r.TreeLength), root.RootHash, r.Signer) + if err != nil { + return nil, fmt.Errorf("error signing checkpoint for inactive shard %d: %w", r.TreeID, err) + } + cachedCheckpoints[r.TreeID] = string(cp) + } + var newEntryPublisher pubsub.Publisher if p := viper.GetString("rekor_server.new_entry_publisher"); p != "" { if !viper.GetBool("rekor_server.publish_events_protobuf") && !viper.GetBool("rekor_server.publish_events_json") { @@ -151,6 +179,7 @@ func NewAPI(treeID uint) (*API, error) { logRanges: ranges, // Utility functionality not required for operation of the core service newEntryPublisher: newEntryPublisher, + cachedCheckpoints: cachedCheckpoints, }, nil } diff --git a/pkg/api/entries.go b/pkg/api/entries.go index f96bac0e9..29bfec054 100644 --- a/pkg/api/entries.go +++ b/pkg/api/entries.go @@ -74,8 +74,8 @@ func signEntry(ctx context.Context, signer signature.Signer, entry models.LogEnt } // logEntryFromLeaf creates a signed LogEntry struct from trillian structs -func logEntryFromLeaf(ctx context.Context, _ trillianclient.TrillianClient, leaf *trillian.LogLeaf, - signedLogRoot *trillian.SignedLogRoot, proof *trillian.Proof, tid int64, ranges sharding.LogRanges) (models.LogEntry, error) { +func logEntryFromLeaf(ctx context.Context, leaf *trillian.LogLeaf, signedLogRoot *trillian.SignedLogRoot, + proof *trillian.Proof, tid int64, ranges sharding.LogRanges, cachedCheckpoints map[int64]string) (models.LogEntry, error) { log.ContextLogger(ctx).Debugf("log entry from leaf %d", leaf.GetLeafIndex()) root := &ttypes.LogRootV1{} @@ -105,9 +105,17 @@ func logEntryFromLeaf(ctx context.Context, _ trillianclient.TrillianClient, leaf return nil, fmt.Errorf("signing entry error: %w", err) } - scBytes, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), tid, root.TreeSize, root.RootHash, logRange.Signer) - if err != nil { - return nil, err + // If tree ID is inactive, use cached checkpoint + var sc string + val, ok := cachedCheckpoints[tid] + if ok { + sc = val + } else { + scBytes, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), tid, root.TreeSize, root.RootHash, logRange.Signer) + if err != nil { + return nil, err + } + sc = string(scBytes) } inclusionProof := models.InclusionProof{ @@ -115,7 +123,7 @@ func logEntryFromLeaf(ctx context.Context, _ trillianclient.TrillianClient, leaf RootHash: swag.String(hex.EncodeToString(root.RootHash)), LogIndex: swag.Int64(proof.GetLeafIndex()), Hashes: hashes, - Checkpoint: stringPointer(string(scBytes)), + Checkpoint: stringPointer(sc), } uuid := hex.EncodeToString(leaf.MerkleLeafHash) @@ -515,8 +523,7 @@ func SearchLogQueryHandler(params entries.SearchLogQueryParams) middleware.Respo if leafResp == nil { continue } - tcs := trillianclient.NewTrillianClient(httpReqCtx, api.logClient, shard) - logEntry, err := logEntryFromLeaf(httpReqCtx, tcs, leafResp.Leaf, leafResp.SignedLogRoot, leafResp.Proof, shard, api.logRanges) + logEntry, err := logEntryFromLeaf(httpReqCtx, leafResp.Leaf, leafResp.SignedLogRoot, leafResp.Proof, shard, api.logRanges, api.cachedCheckpoints) if err != nil { return handleRekorAPIError(params, http.StatusInternalServerError, err, err.Error()) } @@ -563,7 +570,7 @@ func retrieveLogEntryByIndex(ctx context.Context, logIndex int) (models.LogEntry return models.LogEntry{}, ErrNotFound } - return logEntryFromLeaf(ctx, tc, leaf, result.SignedLogRoot, result.Proof, tid, api.logRanges) + return logEntryFromLeaf(ctx, leaf, result.SignedLogRoot, result.Proof, tid, api.logRanges, api.cachedCheckpoints) } // Retrieve a Log Entry @@ -628,7 +635,7 @@ func retrieveUUIDFromTree(ctx context.Context, uuid string, tid int64) (models.L return models.LogEntry{}, err } - logEntry, err := logEntryFromLeaf(ctx, tc, result.Leaf, result.SignedLogRoot, result.Proof, tid, api.logRanges) + logEntry, err := logEntryFromLeaf(ctx, result.Leaf, result.SignedLogRoot, result.Proof, tid, api.logRanges, api.cachedCheckpoints) if err != nil { return models.LogEntry{}, fmt.Errorf("could not create log entry from leaf: %w", err) } diff --git a/pkg/api/tlog.go b/pkg/api/tlog.go index 83bb4d434..aeb1d7810 100644 --- a/pkg/api/tlog.go +++ b/pkg/api/tlog.go @@ -33,7 +33,6 @@ import ( "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/trillianclient" "github.com/sigstore/rekor/pkg/util" - "github.com/sigstore/sigstore/pkg/signature" ) // GetLogInfoHandler returns the current size of the tree and the STH @@ -44,7 +43,7 @@ func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder { var inactiveShards []*models.InactiveShardLogInfo for _, shard := range api.logRanges.GetInactive() { // Get details for this inactive shard - is, err := inactiveShardLogInfo(params.HTTPRequest.Context(), shard.TreeID, shard.Signer) + is, err := inactiveShardLogInfo(params.HTTPRequest.Context(), shard.TreeID, api.cachedCheckpoints) if err != nil { return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("inactive shard error: %w", err), unexpectedInactiveShardError) } @@ -168,7 +167,7 @@ func GetLogProofHandler(params tlog.GetLogProofParams) middleware.Responder { return tlog.NewGetLogProofOK().WithPayload(&consistencyProof) } -func inactiveShardLogInfo(ctx context.Context, tid int64, signer signature.Signer) (*models.InactiveShardLogInfo, error) { +func inactiveShardLogInfo(ctx context.Context, tid int64, cachedCheckpoints map[int64]string) (*models.InactiveShardLogInfo, error) { tc := trillianclient.NewTrillianClient(ctx, api.logClient, tid) resp := tc.GetLatest(0) if resp.Status != codes.OK { @@ -184,16 +183,11 @@ func inactiveShardLogInfo(ctx context.Context, tid int64, signer signature.Signe hashString := hex.EncodeToString(root.RootHash) treeSize := int64(root.TreeSize) - scBytes, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), tid, root.TreeSize, root.RootHash, signer) - if err != nil { - return nil, err - } - m := models.InactiveShardLogInfo{ RootHash: &hashString, TreeSize: &treeSize, TreeID: stringPointer(fmt.Sprintf("%d", tid)), - SignedTreeHead: stringPointer(string(scBytes)), + SignedTreeHead: stringPointer(cachedCheckpoints[tid]), } return &m, nil } diff --git a/tests/sharding-e2e-test.sh b/tests/sharding-e2e-test.sh index 71a3d2b46..61865aabd 100755 --- a/tests/sharding-e2e-test.sh +++ b/tests/sharding-e2e-test.sh @@ -63,6 +63,18 @@ function stringsMatch () { fi } +function stringsNotMatch () { + one=$1 + two=$2 + + if [[ "$one" != "$two" ]]; then + echo "Strings do not match" + else + echo "Strings $one match but shouldn't" + exit 1 + fi +} + function waitForRekorServer () { count=0 @@ -278,4 +290,14 @@ echo echo "Testing rekor-cli verification via Entry ID..." DEBUG=1 $REKOR_CLI verify --uuid $ENTRY_ID_1 --rekor_server http://localhost:3000 +# Verify that the checkpoint/SignedTreeHead for inactive shards is cached between calls +ACTIVE_SHARD_CHECKPOINT=$(curl "http://localhost:3000/api/v1/log" | jq .signedTreeHead | base64 -w 0) +INACTIVE_SHARD_CHECKPOINT=$(curl "http://localhost:3000/api/v1/log" | jq .inactiveShards[0].signedTreeHead | base64 -w 0) +ACTIVE_SHARD_CHECKPOINT_NOT_CACHED=$(curl "http://localhost:3000/api/v1/log" | jq .signedTreeHead | base64 -w 0) +INACTIVE_SHARD_CHECKPOINT_CACHED=$(curl "http://localhost:3000/api/v1/log" | jq .inactiveShards[0].signedTreeHead | base64 -w 0) +// inactive shard checkpoint is cached +stringsMatch $INACTIVE_SHARD_CHECKPOINT $INACTIVE_SHARD_CHECKPOINT_CACHED +// active shard checkpoint is not cached +stringsNotMatch $ACTIVE_SHARD_CHECKPOINT $ACTIVE_SHARD_CHECKPOINT_NOT_CACHED + echo "Test passed successfully :)"