Skip to content

Commit

Permalink
Cache checkpoint for inactive shards
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
haydentherapper committed Jan 16, 2025
1 parent 54e84b0 commit a020b3b
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 19 deletions.
29 changes: 29 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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") {
Expand All @@ -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
}

Expand Down
27 changes: 17 additions & 10 deletions pkg/api/entries.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -105,17 +105,25 @@ 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{
TreeSize: swag.Int64(int64(root.TreeSize)),
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)
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
12 changes: 3 additions & 9 deletions pkg/api/tlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
22 changes: 22 additions & 0 deletions tests/sharding-e2e-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 :)"

0 comments on commit a020b3b

Please sign in to comment.