Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ and this library adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The most recent changes are listed first.

## [Unreleased]

### Changed

- Reduce backend RPC load from polling wallets. `GetLatestBlock` and
`GetLatestTreeState` now read the chain tip from the block cache that the
ingestor already maintains, instead of issuing a `getblockchaininfo` per
call, and `GetLightdInfo` caches its reply for 5 seconds. All three fall
back to the RPC when the cache is disabled (`--nocache`) or still empty
during initial sync.

## [0.5.1] - 2026-07-27

### Added
Expand Down
34 changes: 32 additions & 2 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"slices"
"strconv"
"strings"
"sync"
"time"

"github.com/sirupsen/logrus"
Expand All @@ -21,6 +22,7 @@ import (
"github.com/zcash/lightwalletd/walletrpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)

// 'make build' will overwrite this string with the output of git-describe (tag)
Expand Down Expand Up @@ -79,6 +81,14 @@ var Time struct {
// Log as a global variable simplifies logging
var Log *logrus.Entry

// LightdInfo cache - avoid repeated RPC calls for server info
var (
lightdInfoCache *walletrpc.LightdInfo
lightdInfoCacheTime time.Time
lightdInfoCacheTTL = 5 * time.Second // server status doesn't need real-time updates
lightdInfoCacheMutex sync.RWMutex
)

// The following are JSON zcashd rpc requests and replies.
type (
// zcashd rpc "getblockchaininfo"
Expand Down Expand Up @@ -269,6 +279,17 @@ func GetBlockChainInfo() (*ZcashdRpcReplyGetblockchaininfo, error) {
}

func GetLightdInfo() (*walletrpc.LightdInfo, error) {
// Serve from cache when the entry is still fresh.
lightdInfoCacheMutex.RLock()
if lightdInfoCache != nil && Time.Now().Sub(lightdInfoCacheTime) < lightdInfoCacheTTL {
// Return a clone so callers can't mutate the cached value. proto.Clone
// rather than a struct copy, because LightdInfo embeds a mutex.
info := proto.Clone(lightdInfoCache).(*walletrpc.LightdInfo)
lightdInfoCacheMutex.RUnlock()
return info, nil
}
lightdInfoCacheMutex.RUnlock()

result, rpcErr := RawRequest(context.Background(), "getinfo", []json.RawMessage{})
if rpcErr != nil {
return nil, rpcErr
Expand Down Expand Up @@ -307,7 +328,7 @@ func GetLightdInfo() (*walletrpc.LightdInfo, error) {
if DarksideEnabled {
vendor = "ECC DarksideWalletD"
}
return &walletrpc.LightdInfo{
info := &walletrpc.LightdInfo{
Version: Version,
Vendor: vendor,
TaddrSupport: true,
Expand All @@ -325,7 +346,16 @@ func GetLightdInfo() (*walletrpc.LightdInfo, error) {
DonationAddress: DonationAddress,
UpgradeName: upgrade.Name,
UpgradeHeight: uint64(upgrade.ActivationHeight),
}, nil
}

// Update cache
lightdInfoCacheMutex.Lock()
lightdInfoCache = info
lightdInfoCacheTime = Time.Now()
lightdInfoCacheMutex.Unlock()

// Return a clone so callers can't mutate the cached value.
return proto.Clone(info).(*walletrpc.LightdInfo), nil
}

func getBlockFromRPC(ctx context.Context, height int) (*walletrpc.CompactBlock, error) {
Expand Down
3 changes: 3 additions & 0 deletions common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ func TestGetLightdInfo(t *testing.T) {
RawRequest = getLightdInfoStub
defer resetGlobals()
Time.Sleep = sleepStub
Time.Now = time.Now
// Clear any cached LightdInfo from previous tests
lightdInfoCache = nil
// This calls the getblockchaininfo rpc just to establish connectivity with zcashd
FirstRPC()

Expand Down
61 changes: 43 additions & 18 deletions frontend/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,40 @@ func checkTaddress(taddr string) error {
return nil
}

// GetLatestBlock returns the height and hash of the best chain, according to zcashd.
// GetLatestBlock returns the height and hash of the best chain.
// Uses the locally-cached chain tip rather than querying zcashd for every request.
func (s *lwdStreamer) GetLatestBlock(ctx context.Context, placeholder *walletrpc.ChainSpec) (*walletrpc.BlockID, error) {
common.Log.Debugf("gRPC GetLatestBlock(%+v)\n", placeholder)

blockChainInfo, err := common.GetBlockChainInfo()
if err != nil {
return nil, status.Errorf(codes.Unavailable,
"GetLatestBlock: GetBlockChainInfo failed: %s", err.Error())
// Use the cached chain tip, which BlockIngestor keeps up to date. Fall back
// to the RPC when the cache is disabled (--nocache, so s.cache is nil and no
// ingestor is running) or still empty during initial sync.
latestHeight := -1
if s.cache != nil {
latestHeight = s.cache.GetLatestHeight()
}
bestBlockHashBigEndian, err := hash32.Decode(blockChainInfo.BestBlockHash)
if err != nil {
return nil, status.Errorf(codes.Internal,
"GetLatestBlock: decode block hash %s failed: %s", blockChainInfo.BestBlockHash, err.Error())
if latestHeight < 0 {
blockChainInfo, err := common.GetBlockChainInfo()
if err != nil {
return nil, status.Errorf(codes.Unavailable,
"GetLatestBlock: GetBlockChainInfo failed: %s", err.Error())
}
bestBlockHashBE, err := hash32.Decode(blockChainInfo.BestBlockHash)
if err != nil {
return nil, status.Errorf(codes.Internal,
"GetLatestBlock: decode block hash %s failed: %s", blockChainInfo.BestBlockHash, err.Error())
}
return &walletrpc.BlockID{
Height: uint64(blockChainInfo.Blocks),
Hash: hash32.ToSlice(hash32.Reverse(bestBlockHashBE)),
}, nil
}
// Binary block hash should always be in little-endian format
bestBlockHash := hash32.Reverse(bestBlockHashBigEndian)

latestHash := s.cache.GetLatestHash()
r := &walletrpc.BlockID{
Height: uint64(blockChainInfo.Blocks),
Hash: hash32.ToSlice(bestBlockHash)}
Height: uint64(latestHeight),
Hash: hash32.ToSlice(latestHash),
}
common.Log.Tracef(" return: %+v\n", r)
return r, nil
}
Expand Down Expand Up @@ -443,12 +458,22 @@ func (s *lwdStreamer) GetTreeState(ctx context.Context, id *walletrpc.BlockID) (

func (s *lwdStreamer) GetLatestTreeState(ctx context.Context, in *walletrpc.Empty) (*walletrpc.TreeState, error) {
common.Log.Debugf("gRPC GetLatestTreeState()\n")
blockChainInfo, err := common.GetBlockChainInfo()
if err != nil {
return nil, status.Errorf(codes.Unavailable,
"GetLatestTreeState: getblockchaininfo failed, error: %s", err.Error())

// As in GetLatestBlock: prefer the cached height, fall back to the RPC when
// the cache is disabled or still empty.
latestHeight := -1
if s.cache != nil {
latestHeight = s.cache.GetLatestHeight()
}
latestHeight := blockChainInfo.Blocks
if latestHeight < 0 {
blockChainInfo, err := common.GetBlockChainInfo()
if err != nil {
return nil, status.Errorf(codes.Unavailable,
"GetLatestTreeState: getblockchaininfo failed, error: %s", err.Error())
}
latestHeight = blockChainInfo.Blocks
}

r, err := s.GetTreeState(ctx, &walletrpc.BlockID{Height: uint64(latestHeight)})
if err == nil {
common.Log.Tracef(" return: %+v\n", r)
Expand Down