ref: Persist discharges.

The discharge cache is moved into the blessingStore so that discharges
may be used across processes.

On my machine, the first call of
'namespace glob identity' with a blessing that has a revocation caveat
takes .7s, but subsequent calls take .25 seconds.

MultiPart: 2/2

Change-Id: Ifdc23e618120baef1638b2ebda1f900b772ce727
diff --git a/lib/security/blessingstore.go b/lib/security/blessingstore.go
index 5ff62b7..0b09ee8 100644
--- a/lib/security/blessingstore.go
+++ b/lib/security/blessingstore.go
@@ -9,10 +9,13 @@
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"fmt"
 	"reflect"
 	"sort"
+	"strings"
 	"sync"
+	"time"
 
 	"v.io/v23/security"
 	"v.io/v23/verror"
@@ -27,6 +30,8 @@
 	errDataOrSignerUnspecified = verror.Register(pkgPath+".errDataOrSignerUnspecified", verror.NoRetry, "{1:}{2:} persisted data or signer is not specified{:_}")
 )
 
+const cacheKeyFormat = uint32(1)
+
 // blessingStore implements security.BlessingStore.
 type blessingStore struct {
 	publicKey  security.PublicKey
@@ -116,6 +121,105 @@
 	return m
 }
 
+func (bs *blessingStore) CacheDischarge(discharge security.Discharge, caveat security.Caveat, impetus security.DischargeImpetus) {
+	id := discharge.ID()
+	tp := caveat.ThirdPartyDetails()
+	// Only add to the cache if the caveat did not require arguments.
+	if id == "" || tp == nil || tp.Requirements().ReportArguments {
+		return
+	}
+	key := dcacheKey(tp, impetus)
+	bs.mu.Lock()
+	defer bs.mu.Unlock()
+	old, hadold := bs.state.DischargeCache[key]
+	bs.state.DischargeCache[key] = discharge
+	if err := bs.save(); err != nil {
+		if hadold {
+			bs.state.DischargeCache[key] = old
+		} else {
+			delete(bs.state.DischargeCache, key)
+		}
+	}
+	return
+}
+
+func (bs *blessingStore) ClearDischarges(discharges ...security.Discharge) {
+	bs.mu.Lock()
+	bs.clearDischargesLocked(discharges...)
+	bs.mu.Unlock()
+	return
+}
+
+func (bs *blessingStore) clearDischargesLocked(discharges ...security.Discharge) {
+	for _, d := range discharges {
+		for k, cached := range bs.state.DischargeCache {
+			if cached.Equivalent(d) {
+				delete(bs.state.DischargeCache, k)
+			}
+		}
+	}
+}
+
+func (bs *blessingStore) Discharge(caveat security.Caveat, impetus security.DischargeImpetus) (out security.Discharge) {
+	defer bs.mu.Unlock()
+	bs.mu.Lock()
+	tp := caveat.ThirdPartyDetails()
+	if tp == nil || tp.Requirements().ReportArguments {
+		return
+	}
+	key := dcacheKey(tp, impetus)
+	if cached, exists := bs.state.DischargeCache[key]; exists {
+		out = cached
+		// If the discharge has expired, purge it from the cache.
+		if hasDischargeExpired(out) {
+			out = security.Discharge{}
+			bs.clearDischargesLocked(cached)
+		}
+	}
+	return
+}
+
+func hasDischargeExpired(dis security.Discharge) bool {
+	expiry := dis.Expiry()
+	if expiry.IsZero() {
+		return false
+	}
+	return expiry.Before(time.Now())
+}
+
+func dcacheKey(tp security.ThirdPartyCaveat, impetus security.DischargeImpetus) dischargeCacheKey {
+	// If the algorithm for computing dcacheKey changes, cacheKeyFormat must be changed as well.
+	id := tp.ID()
+	r := tp.Requirements()
+	var method, servers string
+	// We currently do not cache on impetus.Arguments because there it seems there is no
+	// general way to generate a key from them.
+	if r.ReportMethod {
+		method = impetus.Method
+	}
+	if r.ReportServer && len(impetus.Server) > 0 {
+		// Sort the server blessing patterns to increase cache usage.
+		var bps []string
+		for _, bp := range impetus.Server {
+			bps = append(bps, string(bp))
+		}
+		sort.Strings(bps)
+		servers = strings.Join(bps, ",")
+	}
+	h := sha256.New()
+	h.Write(hashString(id))
+	h.Write(hashString(method))
+	h.Write(hashString(servers))
+	var key [sha256.Size]byte
+	copy(key[:], h.Sum(nil))
+	return key
+}
+
+func hashString(d string) []byte {
+	h := sha256.Sum256([]byte(d))
+	return h[:]
+}
+
 // DebugString return a human-readable string encoding of the store
 // in the following format
 // Default Blessings <blessings>
@@ -158,7 +262,10 @@
 func newInMemoryBlessingStore(publicKey security.PublicKey) security.BlessingStore {
 	return &blessingStore{
 		publicKey: publicKey,
-		state:     blessingStoreState{PeerBlessings: make(map[security.BlessingPattern]security.Blessings)},
+		state: blessingStoreState{
+			PeerBlessings:  make(map[security.BlessingPattern]security.Blessings),
+			DischargeCache: make(map[dischargeCacheKey]security.Discharge),
+		},
 	}
 }
 
@@ -185,6 +292,10 @@
 	if err := decodeFromStorage(&bs.state, data, signature, bs.signer.PublicKey()); err != nil {
 		return err
 	}
+	if bs.state.CacheKeyFormat != cacheKeyFormat {
+		bs.state.CacheKeyFormat = cacheKeyFormat
+		bs.state.DischargeCache = make(map[dischargeCacheKey]security.Discharge)
+	}
 	return bs.verifyState()
 }
 
@@ -197,12 +308,17 @@
 	}
 	bs := &blessingStore{
 		publicKey:  signer.PublicKey(),
-		state:      blessingStoreState{PeerBlessings: make(map[security.BlessingPattern]security.Blessings)},
 		serializer: serializer,
 		signer:     signer,
 	}
 	if err := bs.deserialize(); err != nil {
 		return nil, err
 	}
+	if bs.state.PeerBlessings == nil {
+		bs.state.PeerBlessings = make(map[security.BlessingPattern]security.Blessings)
+	}
+	if bs.state.DischargeCache == nil {
+		bs.state.DischargeCache = make(map[dischargeCacheKey]security.Discharge)
+	}
 	return bs, nil
 }
diff --git a/lib/security/blessingstore_test.go b/lib/security/blessingstore_test.go
index 339cd59..01cc3df 100644
--- a/lib/security/blessingstore_test.go
+++ b/lib/security/blessingstore_test.go
@@ -140,6 +140,7 @@
 	if err := tester.testSetDefault(s); err != nil {
 		t.Error(err)
 	}
+	testDischargeCache(t, s)
 }
 
 func TestBlessingStorePersistence(t *testing.T) {
@@ -164,6 +165,7 @@
 	if err := tester.testSetDefault(s); err != nil {
 		t.Error(err)
 	}
+	testDischargeCache(t, s)
 
 	// Recreate the BlessingStore from the directory.
 	p2, err := LoadPersistentPrincipal(dir, nil)
diff --git a/lib/security/discharge_cache_test.go b/lib/security/discharge_cache_test.go
new file mode 100644
index 0000000..f974d1a
--- /dev/null
+++ b/lib/security/discharge_cache_test.go
@@ -0,0 +1,105 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package security
+
+import (
+	"testing"
+	"time"
+
+	"v.io/v23/security"
+	"v.io/v23/vdl"
+)
+
+func testDischargeCache(t *testing.T, s security.BlessingStore) {
+	var (
+		discharger = mkPrincipal()
+		expiredCav = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "moline", security.ThirdPartyRequirements{}, security.UnconstrainedUse()))
+		argsCav    = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "peoria", security.ThirdPartyRequirements{ReportArguments: true}, security.UnconstrainedUse()))
+		methodCav  = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "moline", security.ThirdPartyRequirements{ReportMethod: true}, security.UnconstrainedUse()))
+		serverCav  = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "peoria", security.ThirdPartyRequirements{ReportServer: true}, security.UnconstrainedUse()))
+
+		dEmpty   = security.Discharge{}
+		dExpired = mkDischarge(discharger.MintDischarge(expiredCav, mkCaveat(security.NewExpiryCaveat(time.Now().Add(-1*time.Minute)))))
+		dArgs    = mkDischarge(discharger.MintDischarge(argsCav, security.UnconstrainedUse()))
+		dMethod  = mkDischarge(discharger.MintDischarge(methodCav, security.UnconstrainedUse()))
+		dServer  = mkDischarge(discharger.MintDischarge(serverCav, security.UnconstrainedUse()))
+
+		emptyImp       = security.DischargeImpetus{}
+		argsImp        = security.DischargeImpetus{Arguments: []*vdl.Value{&vdl.Value{}}}
+		methodImp      = security.DischargeImpetus{Method: "foo"}
+		otherMethodImp = security.DischargeImpetus{Method: "bar"}
+		serverImp      = security.DischargeImpetus{Server: []security.BlessingPattern{security.BlessingPattern("fooserver")}}
+		otherServerImp = security.DischargeImpetus{Server: []security.BlessingPattern{security.BlessingPattern("barserver")}}
+	)
+
+	// Discharges for different cavs should not be cached.
+	d := mkDischarge(discharger.MintDischarge(argsCav, security.UnconstrainedUse()))
+	s.CacheDischarge(d, argsCav, emptyImp)
+	if d := s.Discharge(methodCav, emptyImp); d.ID() != "" {
+		t.Errorf("Discharge for different caveat should not have been in cache")
+	}
+	s.ClearDischarges(d)
+
+	// Add some discharges into the cache.
+	s.CacheDischarge(dArgs, argsCav, argsImp)
+	s.CacheDischarge(dMethod, methodCav, methodImp)
+	s.CacheDischarge(dServer, serverCav, serverImp)
+	s.CacheDischarge(dExpired, expiredCav, emptyImp)
+
+	testCases := []struct {
+		caveat          security.Caveat           // caveat that we are fetching discharges for.
+		queryImpetus    security.DischargeImpetus // Impetus used to  query the cache.
+		cachedDischarge security.Discharge        // Discharge that we expect to be returned from the cache, nil if the discharge should not be cached.
+	}{
+		// Expired discharges should not be returned by the cache.
+		{expiredCav, emptyImp, dEmpty},
+
+		// Discharges with Impetuses that have Arguments should not be cached.
+		{argsCav, argsImp, dEmpty},
+
+		{methodCav, methodImp, dMethod},
+		{methodCav, otherMethodImp, dEmpty},
+		{methodCav, emptyImp, dEmpty},
+
+		{serverCav, serverImp, dServer},
+		{serverCav, otherServerImp, dEmpty},
+		{serverCav, emptyImp, dEmpty},
+	}
+
+	for i, test := range testCases {
+		out := s.Discharge(test.caveat, test.queryImpetus)
+		if got := out.ID(); got != test.cachedDischarge.ID() {
+			t.Errorf("#%d: got discharge %v, want %v, queried with %v", i, got, test.cachedDischarge.ID(), test.queryImpetus)
+		}
+	}
+	if t.Failed() {
+		t.Logf("dArgs.ID():    %v", dArgs.ID())
+		t.Logf("dMethod.ID():  %v", dMethod.ID())
+		t.Logf("dServer.ID():  %v", dServer.ID())
+		t.Logf("dExpired.ID(): %v", dExpired.ID())
+	}
+}
+
+func mkPrincipal() security.Principal {
+	p, err := NewPrincipal()
+	if err != nil {
+		panic(err)
+	}
+	return p
+}
+
+func mkDischarge(d security.Discharge, err error) security.Discharge {
+	if err != nil {
+		panic(err)
+	}
+	return d
+}
+
+func mkCaveat(c security.Caveat, err error) security.Caveat {
+	if err != nil {
+		panic(err)
+	}
+	return c
+}
diff --git a/lib/security/type.vdl b/lib/security/type.vdl
index 9d65571..9539700 100644
--- a/lib/security/type.vdl
+++ b/lib/security/type.vdl
@@ -18,4 +18,11 @@
 	// DefaultBlessings is the default Blessings to be shared with peers for which
 	// no other information is available to select blessings.
 	DefaultBlessings security.WireBlessings
+	// DischargeCache is the cache of discharges.
+	DischargeCache map[dischargeCacheKey]security.WireDischarge
+	// CacheKeyFormat is the dischargeCacheKey format version. It should incremented
+	// any time the format of the dischargeCacheKey is changed.
+	CacheKeyFormat uint32
 }
+
+type dischargeCacheKey [32]byte
diff --git a/lib/security/type.vdl.go b/lib/security/type.vdl.go
index 3fabed7..5681451 100644
--- a/lib/security/type.vdl.go
+++ b/lib/security/type.vdl.go
@@ -32,6 +32,11 @@
 	// DefaultBlessings is the default Blessings to be shared with peers for which
 	// no other information is available to select blessings.
 	DefaultBlessings security.Blessings
+	// DischargeCache is the cache of discharges.
+	DischargeCache map[dischargeCacheKey]security.Discharge
+	// CacheKeyFormat is the dischargeCacheKey format version. It should incremented
+	// any time the format of the dischargeCacheKey is changed.
+	CacheKeyFormat uint32
 }
 
 func (blessingStoreState) __VDLReflect(struct {
@@ -39,7 +44,15 @@
 }) {
 }
 
+type dischargeCacheKey [32]byte
+
+func (dischargeCacheKey) __VDLReflect(struct {
+	Name string `vdl:"v.io/x/ref/lib/security.dischargeCacheKey"`
+}) {
+}
+
 func init() {
 	vdl.Register((*blessingRootsState)(nil))
 	vdl.Register((*blessingStoreState)(nil))
+	vdl.Register((*dischargeCacheKey)(nil))
 }
diff --git a/runtime/internal/rpc/client.go b/runtime/internal/rpc/client.go
index d7aa5b8..9c81d74 100644
--- a/runtime/internal/rpc/client.go
+++ b/runtime/internal/rpc/client.go
@@ -1018,7 +1018,7 @@
 			// to detect it, we conservatively flush all discharges we used from the cache.
 			// TODO(ataly,andreser): add verror.BadDischarge and handle it explicitly?
 			vlog.VI(3).Infof("Discarding %d discharges as RPC failed with %v", len(fc.discharges), fc.response.Error)
-			fc.dc.Invalidate(fc.discharges...)
+			fc.dc.Invalidate(fc.ctx, fc.discharges...)
 		}
 		if id == errBadNumInputArgs.ID || id == errBadInputArg.ID {
 			return fc.close(verror.New(verror.ErrBadProtocol, fc.ctx, fc.response.Error))
diff --git a/runtime/internal/rpc/discharges.go b/runtime/internal/rpc/discharges.go
index 46017ee..d69e349 100644
--- a/runtime/internal/rpc/discharges.go
+++ b/runtime/internal/rpc/discharges.go
@@ -5,14 +5,13 @@
 package rpc
 
 import (
-	"sort"
-	"strings"
 	"sync"
 	"time"
 
 	"v.io/x/ref/lib/apilog"
 	"v.io/x/ref/runtime/internal/rpc/stream/vc"
 
+	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/rpc"
 	"v.io/v23/security"
@@ -35,7 +34,6 @@
 type dischargeClient struct {
 	c                     rpc.Client
 	defaultCtx            *context.T
-	cache                 dischargeCache
 	dischargeExpiryBuffer time.Duration
 }
 
@@ -51,12 +49,8 @@
 // Attempts will be made to refresh a discharge DischargeExpiryBuffer before they expire.
 func InternalNewDischargeClient(defaultCtx *context.T, client rpc.Client, dischargeExpiryBuffer time.Duration) vc.DischargeClient {
 	return &dischargeClient{
-		c:          client,
-		defaultCtx: defaultCtx,
-		cache: dischargeCache{
-			cache:    make(map[dischargeCacheKey]security.Discharge),
-			idToKeys: make(map[string][]dischargeCacheKey),
-		},
+		c:                     client,
+		defaultCtx:            defaultCtx,
 		dischargeExpiryBuffer: dischargeExpiryBuffer,
 	}
 }
@@ -85,15 +79,15 @@
 		}
 	}
 
+	if ctx == nil {
+		ctx = d.defaultCtx
+	}
+	bstore := v23.GetPrincipal(ctx).BlessingStore()
 	// Gather discharges from cache.
-	// (Collect a set of pointers, where nil implies a missing discharge)
-	discharges := make([]*security.Discharge, len(caveats))
-	if d.cache.Discharges(caveats, filteredImpetuses, discharges) > 0 {
+	discharges, rem := discharges(bstore, caveats, impetus)
+	if rem > 0 {
 		// Fetch discharges for caveats for which no discharges were
 		// found in the cache.
-		if ctx == nil {
-			ctx = d.defaultCtx
-		}
 		if ctx != nil {
 			var span vtrace.Span
 			ctx, span = vtrace.WithNewSpan(ctx, "Fetching Discharges")
@@ -102,14 +96,30 @@
 		d.fetchDischarges(ctx, caveats, filteredImpetuses, discharges)
 	}
 	for _, d := range discharges {
-		if d != nil {
-			ret = append(ret, *d)
+		if d.ID() != "" {
+			ret = append(ret, d)
 		}
 	}
 	return
 }
-func (d *dischargeClient) Invalidate(discharges ...security.Discharge) {
-	d.cache.invalidate(discharges...)
+
+func discharges(bs security.BlessingStore, caveats []security.Caveat, imp security.DischargeImpetus) (out []security.Discharge, rem int) {
+	out = make([]security.Discharge, len(caveats))
+	for i := range caveats {
+		out[i] = bs.Discharge(caveats[i], imp)
+		if out[i].ID() == "" {
+			rem++
+		}
+	}
+	return
+}
+
+func (d *dischargeClient) Invalidate(ctx *context.T, discharges ...security.Discharge) {
+	if ctx == nil {
+		ctx = d.defaultCtx
+	}
+	bstore := v23.GetPrincipal(ctx).BlessingStore()
+	bstore.ClearDischarges(discharges...)
 }
 
 // fetchDischarges fills out by fetching discharges for caveats from the
@@ -118,12 +128,14 @@
 // fetched or no new discharges are fetched.
 // REQUIRES: len(caveats) == len(out)
 // REQUIRES: caveats[i].ThirdPartyDetails() != nil for 0 <= i < len(caveats)
-func (d *dischargeClient) fetchDischarges(ctx *context.T, caveats []security.Caveat, impetuses []security.DischargeImpetus, out []*security.Discharge) {
+func (d *dischargeClient) fetchDischarges(ctx *context.T, caveats []security.Caveat, impetuses []security.DischargeImpetus, out []security.Discharge) {
+	bstore := v23.GetPrincipal(ctx).BlessingStore()
 	var wg sync.WaitGroup
 	for {
 		type fetched struct {
 			idx       int
-			discharge *security.Discharge
+			discharge security.Discharge
+			caveat    security.Caveat
 			impetus   security.DischargeImpetus
 		}
 		discharges := make(chan fetched, len(caveats))
@@ -143,14 +155,14 @@
 					vlog.VI(3).Infof("Discharge fetch for %v failed: %v", tp, err)
 					return
 				}
-				discharges <- fetched{i, &dis, impetuses[i]}
+				discharges <- fetched{i, dis, caveats[i], impetuses[i]}
 			}(i, ctx, caveats[i])
 		}
 		wg.Wait()
 		close(discharges)
 		var got int
 		for fetched := range discharges {
-			d.cache.Add(*fetched.discharge, fetched.impetus)
+			bstore.CacheDischarge(fetched.discharge, fetched.caveat, fetched.impetus)
 			out[fetched.idx] = fetched.discharge
 			got++
 		}
@@ -163,117 +175,6 @@
 	}
 }
 
-func (d *dischargeClient) shouldFetchDischarge(dis *security.Discharge) bool {
-	if dis == nil {
-		return true
-	}
-	expiry := dis.Expiry()
-	if expiry.IsZero() {
-		return false
-	}
-	return expiry.Before(time.Now().Add(d.dischargeExpiryBuffer))
-}
-
-// dischargeCache is a concurrency-safe cache for third party caveat discharges.
-type dischargeCache struct {
-	mu       sync.RWMutex
-	cache    map[dischargeCacheKey]security.Discharge // GUARDED_BY(mu)
-	idToKeys map[string][]dischargeCacheKey           // GUARDED_BY(mu)
-}
-
-type dischargeCacheKey struct {
-	id, method, serverPatterns string
-}
-
-func (dcc *dischargeCache) cacheKey(id string, impetus security.DischargeImpetus) dischargeCacheKey {
-	// We currently do not cache on impetus.Arguments because there it seems there is no
-	// universal way to generate a key from them.
-	// Add sorted BlessingPatterns to the key.
-	var bps []string
-	for _, bp := range impetus.Server {
-		bps = append(bps, string(bp))
-	}
-	sort.Strings(bps)
-	return dischargeCacheKey{
-		id:             id,
-		method:         impetus.Method,
-		serverPatterns: strings.Join(bps, ","), // "," is restricted in blessingPatterns.
-	}
-}
-
-// Add inserts the argument to the cache, the previous discharge for the same caveat.
-func (dcc *dischargeCache) Add(d security.Discharge, filteredImpetus security.DischargeImpetus) {
-	// Only add to the cache if the caveat did not require arguments.
-	if len(filteredImpetus.Arguments) > 0 {
-		return
-	}
-	id := d.ID()
-	dcc.mu.Lock()
-	dcc.cache[dcc.cacheKey(id, filteredImpetus)] = d
-	if _, ok := dcc.idToKeys[id]; !ok {
-		dcc.idToKeys[id] = []dischargeCacheKey{}
-	}
-	dcc.idToKeys[id] = append(dcc.idToKeys[id], dcc.cacheKey(id, filteredImpetus))
-	dcc.mu.Unlock()
-}
-
-// Discharges takes a slice of caveats, a slice of filtered Discharge impetuses
-// corresponding to the caveats, and a slice of discharges of the same length and
-// fills in nil entries in the discharges slice with pointers to cached discharges
-// (if there are any).
-//
-// REQUIRES: len(caveats) == len(impetuses) == len(out)
-// REQUIRES: caveats[i].ThirdPartyDetails() != nil, for all 0 <= i < len(caveats)
-func (dcc *dischargeCache) Discharges(caveats []security.Caveat, impetuses []security.DischargeImpetus, out []*security.Discharge) (remaining int) {
-	dcc.mu.Lock()
-	for i, d := range out {
-		if d != nil {
-			continue
-		}
-		id := caveats[i].ThirdPartyDetails().ID()
-		key := dcc.cacheKey(id, impetuses[i])
-		if cached, exists := dcc.cache[key]; exists {
-			out[i] = &cached
-			// If the discharge has expired, purge it from the cache.
-			if hasDischargeExpired(out[i]) {
-				out[i] = nil
-				delete(dcc.cache, key)
-				remaining++
-			}
-		} else {
-			remaining++
-		}
-	}
-	dcc.mu.Unlock()
-	return
-}
-
-func hasDischargeExpired(dis *security.Discharge) bool {
-	expiry := dis.Expiry()
-	if expiry.IsZero() {
-		return false
-	}
-	return expiry.Before(time.Now())
-}
-
-func (dcc *dischargeCache) invalidate(discharges ...security.Discharge) {
-	dcc.mu.Lock()
-	for _, d := range discharges {
-		if keys, ok := dcc.idToKeys[d.ID()]; ok {
-			var newKeys []dischargeCacheKey
-			for _, k := range keys {
-				if cached := dcc.cache[k]; cached.Equivalent(d) {
-					delete(dcc.cache, k)
-				} else {
-					newKeys = append(newKeys, k)
-				}
-			}
-			dcc.idToKeys[d.ID()] = newKeys
-		}
-	}
-	dcc.mu.Unlock()
-}
-
 // filteredImpetus returns a copy of 'before' after removing any values that are not required as per 'r'.
 func filteredImpetus(r security.ThirdPartyRequirements, before security.DischargeImpetus) (after security.DischargeImpetus) {
 	if r.ReportServer && len(before.Server) > 0 {
@@ -293,3 +194,14 @@
 	}
 	return
 }
+
+func (d *dischargeClient) shouldFetchDischarge(dis security.Discharge) bool {
+	if dis.ID() == "" {
+		return true
+	}
+	expiry := dis.Expiry()
+	if expiry.IsZero() {
+		return false
+	}
+	return expiry.Before(time.Now().Add(d.dischargeExpiryBuffer))
+}
diff --git a/runtime/internal/rpc/discharges_test.go b/runtime/internal/rpc/discharges_test.go
deleted file mode 100644
index 7b67b1b..0000000
--- a/runtime/internal/rpc/discharges_test.go
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package rpc
-
-import (
-	"testing"
-	"time"
-
-	"v.io/v23/security"
-	"v.io/v23/vdl"
-	"v.io/x/ref/test/testutil"
-)
-
-func TestDischargeClientCache(t *testing.T) {
-	dcc := &dischargeCache{
-		cache:    make(map[dischargeCacheKey]security.Discharge),
-		idToKeys: make(map[string][]dischargeCacheKey),
-	}
-
-	var (
-		discharger = testutil.NewPrincipal("discharger")
-		expiredCav = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "moline", security.ThirdPartyRequirements{}, security.UnconstrainedUse()))
-		argsCav    = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "moline", security.ThirdPartyRequirements{}, security.UnconstrainedUse()))
-		methodCav  = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "moline", security.ThirdPartyRequirements{}, security.UnconstrainedUse()))
-		serverCav  = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "moline", security.ThirdPartyRequirements{}, security.UnconstrainedUse()))
-
-		dExpired = mkDischarge(discharger.MintDischarge(expiredCav, mkCaveat(security.NewExpiryCaveat(time.Now().Add(-1*time.Minute)))))
-		dArgs    = mkDischarge(discharger.MintDischarge(argsCav, security.UnconstrainedUse()))
-		dMethod  = mkDischarge(discharger.MintDischarge(methodCav, security.UnconstrainedUse()))
-		dServer  = mkDischarge(discharger.MintDischarge(serverCav, security.UnconstrainedUse()))
-
-		emptyImp       = security.DischargeImpetus{}
-		argsImp        = security.DischargeImpetus{Arguments: []*vdl.Value{&vdl.Value{}}}
-		methodImp      = security.DischargeImpetus{Method: "foo"}
-		otherMethodImp = security.DischargeImpetus{Method: "bar"}
-		serverImp      = security.DischargeImpetus{Server: []security.BlessingPattern{security.BlessingPattern("fooserver")}}
-		otherServerImp = security.DischargeImpetus{Server: []security.BlessingPattern{security.BlessingPattern("barserver")}}
-	)
-
-	// Discharges for different cavs should not be cached.
-	d := mkDischarge(discharger.MintDischarge(argsCav, security.UnconstrainedUse()))
-	dcc.Add(d, emptyImp)
-	outdis := make([]*security.Discharge, 1)
-	if remaining := dcc.Discharges([]security.Caveat{methodCav}, []security.DischargeImpetus{emptyImp}, outdis); remaining == 0 {
-		t.Errorf("Discharge for different caveat should not have been in cache")
-	}
-	dcc.invalidate(d)
-
-	// Add some discharges into the cache.
-	dcc.Add(dArgs, argsImp)
-	dcc.Add(dMethod, methodImp)
-	dcc.Add(dServer, serverImp)
-	dcc.Add(dExpired, emptyImp)
-
-	testCases := []struct {
-		caveat          security.Caveat           // caveat that we are fetching discharges for.
-		queryImpetus    security.DischargeImpetus // Impetus used to  query the cache.
-		cachedDischarge *security.Discharge       // Discharge that we expect to be returned from the cache, nil if the discharge should not be cached.
-	}{
-		// Expired discharges should not be returned by the cache.
-		{expiredCav, emptyImp, nil},
-
-		// Discharges with Impetuses that have Arguments should not be cached.
-		{argsCav, argsImp, nil},
-
-		{methodCav, methodImp, &dMethod},
-		{methodCav, otherMethodImp, nil},
-		{methodCav, emptyImp, nil},
-
-		{serverCav, serverImp, &dServer},
-		{serverCav, otherServerImp, nil},
-		{serverCav, emptyImp, nil},
-	}
-
-	for i, test := range testCases {
-		out := make([]*security.Discharge, 1)
-		remaining := dcc.Discharges([]security.Caveat{test.caveat}, []security.DischargeImpetus{test.queryImpetus}, out)
-		if test.cachedDischarge != nil {
-			got := "nil"
-			if remaining == 0 {
-				got = out[0].ID()
-			}
-			if got != test.cachedDischarge.ID() {
-				t.Errorf("#%d: got discharge %v, want %v, queried with %v", i, got, test.cachedDischarge.ID(), test.queryImpetus)
-			}
-		} else if remaining == 0 {
-			t.Errorf("#%d: discharge %v should not have been in cache, queried with %v", i, out[0].ID(), test.queryImpetus)
-		}
-	}
-	if t.Failed() {
-		t.Logf("dArgs.ID():    %v", dArgs.ID())
-		t.Logf("dMethod.ID():  %v", dMethod.ID())
-		t.Logf("dServer.ID():  %v", dServer.ID())
-		t.Logf("dExpired.ID(): %v", dExpired.ID())
-	}
-}
-
-func mkDischarge(d security.Discharge, err error) security.Discharge {
-	if err != nil {
-		panic(err)
-	}
-	return d
-}
diff --git a/runtime/internal/rpc/full_test.go b/runtime/internal/rpc/full_test.go
index f60009b..8224776 100644
--- a/runtime/internal/rpc/full_test.go
+++ b/runtime/internal/rpc/full_test.go
@@ -1193,6 +1193,15 @@
 func (*singleBlessingStore) PeerBlessings() map[security.BlessingPattern]security.Blessings {
 	return nil
 }
+func (*singleBlessingStore) CacheDischarge(security.Discharge, security.Caveat, security.DischargeImpetus) {
+	return
+}
+func (*singleBlessingStore) ClearDischarges(...security.Discharge) {
+	return
+}
+func (*singleBlessingStore) Discharge(security.Caveat, security.DischargeImpetus) security.Discharge {
+	return security.Discharge{}
+}
 
 // singleBlessingPrincipal implements security.Principal. It is a wrapper over
 // a security.Principal that intercepts  all invocations on the
diff --git a/runtime/internal/rpc/stream/vc/vc.go b/runtime/internal/rpc/stream/vc/vc.go
index 12c2527..805a07f 100644
--- a/runtime/internal/rpc/stream/vc/vc.go
+++ b/runtime/internal/rpc/stream/vc/vc.go
@@ -199,7 +199,7 @@
 	PrepareDischarges(ctx *context.T, forcaveats []security.Caveat, impetus security.DischargeImpetus) []security.Discharge
 	// Invalidate marks the provided discharges as invalid, and therefore unfit
 	// for being returned by a subsequent PrepareDischarges call.
-	Invalidate(discharges ...security.Discharge)
+	Invalidate(ctx *context.T, discharges ...security.Discharge)
 	RPCStreamListenerOpt()
 }
 
diff --git a/runtime/internal/rpc/stream/vc/vc_test.go b/runtime/internal/rpc/stream/vc/vc_test.go
index e42c385..6e2737f 100644
--- a/runtime/internal/rpc/stream/vc/vc_test.go
+++ b/runtime/internal/rpc/stream/vc/vc_test.go
@@ -170,9 +170,9 @@
 func (m mockDischargeClient) PrepareDischarges(_ *context.T, forcaveats []security.Caveat, impetus security.DischargeImpetus) []security.Discharge {
 	return m
 }
-func (mockDischargeClient) Invalidate(...security.Discharge) {}
-func (mockDischargeClient) RPCStreamListenerOpt()            {}
-func (mockDischargeClient) RPCStreamVCOpt()                  {}
+func (mockDischargeClient) Invalidate(*context.T, ...security.Discharge) {}
+func (mockDischargeClient) RPCStreamListenerOpt()                        {}
+func (mockDischargeClient) RPCStreamVCOpt()                              {}
 
 // Test that mockDischargeClient implements vc.DischargeClient.
 var _ vc.DischargeClient = (mockDischargeClient)(nil)
diff --git a/services/agent/agentlib/client.go b/services/agent/agentlib/client.go
index 7dc2c3c..2efad70 100644
--- a/services/agent/agentlib/client.go
+++ b/services/agent/agentlib/client.go
@@ -252,6 +252,28 @@
 	return
 }
 
+func (b *blessingStore) CacheDischarge(d security.Discharge, c security.Caveat, i security.DischargeImpetus) {
+	err := b.caller.call("BlessingStoreCacheDischarge", results(), d, c, i)
+	if err != nil {
+		vlog.Errorf("error calling BlessingStoreCacheDischarge: %v", err)
+	}
+}
+
+func (b *blessingStore) ClearDischarges(discharges ...security.Discharge) {
+	err := b.caller.call("BlessingStoreClearDischarges", results(), discharges)
+	if err != nil {
+		vlog.Errorf("error calling BlessingStoreClearDischarges: %v", err)
+	}
+}
+
+func (b *blessingStore) Discharge(caveat security.Caveat, impetus security.DischargeImpetus) (out security.Discharge) {
+	err := b.caller.call("BlessingStoreDischarge", results(&out), caveat, impetus)
+	if err != nil {
+		vlog.Errorf("error calling BlessingStoreDischarge: %v", err)
+	}
+	return
+}
+
 type blessingRoots struct {
 	caller caller
 }
diff --git a/services/agent/internal/cache/cache.go b/services/agent/internal/cache/cache.go
index cc6b904..70bca36 100644
--- a/services/agent/internal/cache/cache.go
+++ b/services/agent/internal/cache/cache.go
@@ -312,6 +312,24 @@
 	return fmt.Sprintf("cached[%s]", s.impl)
 }
 
+func (s *cachedStore) CacheDischarge(d security.Discharge, c security.Caveat, i security.DischargeImpetus) {
+	s.mu.Lock()
+	s.impl.CacheDischarge(d, c, i)
+	s.mu.Unlock()
+}
+
+func (s *cachedStore) ClearDischarges(discharges ...security.Discharge) {
+	s.mu.Lock()
+	s.impl.ClearDischarges(discharges...)
+	s.mu.Unlock()
+}
+
+func (s *cachedStore) Discharge(caveat security.Caveat, impetus security.DischargeImpetus) security.Discharge {
+	defer s.mu.Unlock()
+	s.mu.Lock()
+	return s.impl.Discharge(caveat, impetus)
+}
+
 // Must be called while holding mu.
 func (s *cachedStore) flush() {
 	s.hasDef = false
diff --git a/services/agent/internal/server/server.go b/services/agent/internal/server/server.go
index 893d4e7..eab49a2 100644
--- a/services/agent/internal/server/server.go
+++ b/services/agent/internal/server/server.go
@@ -403,6 +403,26 @@
 	return a.principal.BlessingStore().Default(), nil
 }
 
+func (a agentd) BlessingStoreCacheDischarge(_ *context.T, _ rpc.ServerCall, discharge security.Discharge, caveat security.Caveat, impetus security.DischargeImpetus) error {
+	a.w.lock()
+	a.principal.BlessingStore().CacheDischarge(discharge, caveat, impetus)
+	a.w.unlock(a.id)
+	return nil
+}
+
+func (a agentd) BlessingStoreClearDischarges(_ *context.T, _ rpc.ServerCall, discharges []security.Discharge) error {
+	a.w.lock()
+	a.principal.BlessingStore().ClearDischarges(discharges...)
+	a.w.unlock(a.id)
+	return nil
+}
+
+func (a agentd) BlessingStoreDischarge(_ *context.T, _ rpc.ServerCall, caveat security.Caveat, impetus security.DischargeImpetus) (security.Discharge, error) {
+	a.w.lock()
+	defer a.w.unlock(a.id)
+	return a.principal.BlessingStore().Discharge(caveat, impetus), nil
+}
+
 func (a agentd) BlessingRootsAdd(_ *context.T, _ rpc.ServerCall, root []byte, pattern security.BlessingPattern) error {
 	pkey, err := security.UnmarshalPublicKey(root)
 	if err != nil {
diff --git a/services/agent/internal/test_principal/main.go b/services/agent/internal/test_principal/main.go
index 28cc564..7887d67 100644
--- a/services/agent/internal/test_principal/main.go
+++ b/services/agent/internal/test_principal/main.go
@@ -94,7 +94,8 @@
 	if err != nil {
 		errorf("security.NewPublicKeyCaveat: %v", err)
 	}
-	if _, err := p.MintDischarge(tpcav, cav); err != nil {
+	dis, err := p.MintDischarge(tpcav, cav)
+	if err != nil {
 		errorf("MintDischarge: %v", err)
 	}
 	// BlessingRoots
@@ -134,6 +135,14 @@
 	if forpeer := p.BlessingStore().ForPeer("superman/friend"); !reflect.DeepEqual(forpeer, b) {
 		errorf("BlessingStore().ForPeer returned %v and not %v", forpeer, b)
 	}
+	p.BlessingStore().CacheDischarge(dis, tpcav, security.DischargeImpetus{})
+	if got := p.BlessingStore().Discharge(tpcav, security.DischargeImpetus{}); !dis.Equivalent(got) {
+		errorf("BlessingStore().Discharges returned %#v want %#v", got, dis)
+	}
+	p.BlessingStore().ClearDischarges(dis)
+	if got := p.BlessingStore().Discharge(tpcav, security.DischargeImpetus{}); got.ID() != "" {
+		errorf("BlessingStore().Discharges returned %#v want empty", got)
+	}
 
 	if len(errors) > 0 {
 		// Print out all errors and exit with failure.
diff --git a/services/agent/wire.vdl b/services/agent/wire.vdl
index 994aaf5..1d92fcc 100644
--- a/services/agent/wire.vdl
+++ b/services/agent/wire.vdl
@@ -52,6 +52,9 @@
 	BlessingStoreDefault() (security.WireBlessings | error)
 	BlessingStorePeerBlessings() (map[security.BlessingPattern]security.WireBlessings | error)
 	BlessingStoreDebugString() (string | error)
+	BlessingStoreCacheDischarge(discharge security.WireDischarge, caveat security.Caveat, impetus security.DischargeImpetus) error
+	BlessingStoreClearDischarges(discharges []security.WireDischarge) error
+	BlessingStoreDischarge(caveat security.Caveat, impetus security.DischargeImpetus) (wd security.WireDischarge | error)
 
 	BlessingRootsAdd(root []byte, pattern security.BlessingPattern) error
 	BlessingRootsRecognized(root []byte, blessing string) error
diff --git a/services/agent/wire.vdl.go b/services/agent/wire.vdl.go
index 1a0e6a2..da18067 100644
--- a/services/agent/wire.vdl.go
+++ b/services/agent/wire.vdl.go
@@ -63,6 +63,9 @@
 	BlessingStoreDefault(*context.T, ...rpc.CallOpt) (security.Blessings, error)
 	BlessingStorePeerBlessings(*context.T, ...rpc.CallOpt) (map[security.BlessingPattern]security.Blessings, error)
 	BlessingStoreDebugString(*context.T, ...rpc.CallOpt) (string, error)
+	BlessingStoreCacheDischarge(ctx *context.T, discharge security.Discharge, caveat security.Caveat, impetus security.DischargeImpetus, opts ...rpc.CallOpt) error
+	BlessingStoreClearDischarges(ctx *context.T, discharges []security.Discharge, opts ...rpc.CallOpt) error
+	BlessingStoreDischarge(ctx *context.T, caveat security.Caveat, impetus security.DischargeImpetus, opts ...rpc.CallOpt) (wd security.Discharge, err error)
 	BlessingRootsAdd(ctx *context.T, root []byte, pattern security.BlessingPattern, opts ...rpc.CallOpt) error
 	BlessingRootsRecognized(ctx *context.T, root []byte, blessing string, opts ...rpc.CallOpt) error
 	BlessingRootsDump(*context.T, ...rpc.CallOpt) (map[security.BlessingPattern][][]byte, error)
@@ -159,6 +162,21 @@
 	return
 }
 
+func (c implAgentClientStub) BlessingStoreCacheDischarge(ctx *context.T, i0 security.Discharge, i1 security.Caveat, i2 security.DischargeImpetus, opts ...rpc.CallOpt) (err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "BlessingStoreCacheDischarge", []interface{}{i0, i1, i2}, nil, opts...)
+	return
+}
+
+func (c implAgentClientStub) BlessingStoreClearDischarges(ctx *context.T, i0 []security.Discharge, opts ...rpc.CallOpt) (err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "BlessingStoreClearDischarges", []interface{}{i0}, nil, opts...)
+	return
+}
+
+func (c implAgentClientStub) BlessingStoreDischarge(ctx *context.T, i0 security.Caveat, i1 security.DischargeImpetus, opts ...rpc.CallOpt) (o0 security.Discharge, err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "BlessingStoreDischarge", []interface{}{i0, i1}, []interface{}{&o0}, opts...)
+	return
+}
+
 func (c implAgentClientStub) BlessingRootsAdd(ctx *context.T, i0 []byte, i1 security.BlessingPattern, opts ...rpc.CallOpt) (err error) {
 	err = v23.GetClient(ctx).Call(ctx, c.name, "BlessingRootsAdd", []interface{}{i0, i1}, nil, opts...)
 	return
@@ -273,6 +291,9 @@
 	BlessingStoreDefault(*context.T, rpc.ServerCall) (security.Blessings, error)
 	BlessingStorePeerBlessings(*context.T, rpc.ServerCall) (map[security.BlessingPattern]security.Blessings, error)
 	BlessingStoreDebugString(*context.T, rpc.ServerCall) (string, error)
+	BlessingStoreCacheDischarge(ctx *context.T, call rpc.ServerCall, discharge security.Discharge, caveat security.Caveat, impetus security.DischargeImpetus) error
+	BlessingStoreClearDischarges(ctx *context.T, call rpc.ServerCall, discharges []security.Discharge) error
+	BlessingStoreDischarge(ctx *context.T, call rpc.ServerCall, caveat security.Caveat, impetus security.DischargeImpetus) (wd security.Discharge, err error)
 	BlessingRootsAdd(ctx *context.T, call rpc.ServerCall, root []byte, pattern security.BlessingPattern) error
 	BlessingRootsRecognized(ctx *context.T, call rpc.ServerCall, root []byte, blessing string) error
 	BlessingRootsDump(*context.T, rpc.ServerCall) (map[security.BlessingPattern][][]byte, error)
@@ -303,6 +324,9 @@
 	BlessingStoreDefault(*context.T, rpc.ServerCall) (security.Blessings, error)
 	BlessingStorePeerBlessings(*context.T, rpc.ServerCall) (map[security.BlessingPattern]security.Blessings, error)
 	BlessingStoreDebugString(*context.T, rpc.ServerCall) (string, error)
+	BlessingStoreCacheDischarge(ctx *context.T, call rpc.ServerCall, discharge security.Discharge, caveat security.Caveat, impetus security.DischargeImpetus) error
+	BlessingStoreClearDischarges(ctx *context.T, call rpc.ServerCall, discharges []security.Discharge) error
+	BlessingStoreDischarge(ctx *context.T, call rpc.ServerCall, caveat security.Caveat, impetus security.DischargeImpetus) (wd security.Discharge, err error)
 	BlessingRootsAdd(ctx *context.T, call rpc.ServerCall, root []byte, pattern security.BlessingPattern) error
 	BlessingRootsRecognized(ctx *context.T, call rpc.ServerCall, root []byte, blessing string) error
 	BlessingRootsDump(*context.T, rpc.ServerCall) (map[security.BlessingPattern][][]byte, error)
@@ -399,6 +423,18 @@
 	return s.impl.BlessingStoreDebugString(ctx, call)
 }
 
+func (s implAgentServerStub) BlessingStoreCacheDischarge(ctx *context.T, call rpc.ServerCall, i0 security.Discharge, i1 security.Caveat, i2 security.DischargeImpetus) error {
+	return s.impl.BlessingStoreCacheDischarge(ctx, call, i0, i1, i2)
+}
+
+func (s implAgentServerStub) BlessingStoreClearDischarges(ctx *context.T, call rpc.ServerCall, i0 []security.Discharge) error {
+	return s.impl.BlessingStoreClearDischarges(ctx, call, i0)
+}
+
+func (s implAgentServerStub) BlessingStoreDischarge(ctx *context.T, call rpc.ServerCall, i0 security.Caveat, i1 security.DischargeImpetus) (security.Discharge, error) {
+	return s.impl.BlessingStoreDischarge(ctx, call, i0, i1)
+}
+
 func (s implAgentServerStub) BlessingRootsAdd(ctx *context.T, call rpc.ServerCall, i0 []byte, i1 security.BlessingPattern) error {
 	return s.impl.BlessingRootsAdd(ctx, call, i0, i1)
 }
@@ -552,6 +588,30 @@
 			},
 		},
 		{
+			Name: "BlessingStoreCacheDischarge",
+			InArgs: []rpc.ArgDesc{
+				{"discharge", ``}, // security.Discharge
+				{"caveat", ``},    // security.Caveat
+				{"impetus", ``},   // security.DischargeImpetus
+			},
+		},
+		{
+			Name: "BlessingStoreClearDischarges",
+			InArgs: []rpc.ArgDesc{
+				{"discharges", ``}, // []security.Discharge
+			},
+		},
+		{
+			Name: "BlessingStoreDischarge",
+			InArgs: []rpc.ArgDesc{
+				{"caveat", ``},  // security.Caveat
+				{"impetus", ``}, // security.DischargeImpetus
+			},
+			OutArgs: []rpc.ArgDesc{
+				{"wd", ``}, // security.Discharge
+			},
+		},
+		{
 			Name: "BlessingRootsAdd",
 			InArgs: []rpc.ArgDesc{
 				{"root", ``},    // []byte