Compare commits

...

25 Commits

Author SHA1 Message Date
DragonX Developers
5644dbc554 version: 0.1.3 for the zrpc transport change
Rebased onto master (v0.1.2, the deployed crash fix), so this branch now
carries only the RPC transport work. Bump so the deployed version stays
readable from off-box via GetLightdInfo -- the same check that verified
the 0.1.2 rollout node by node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-26 11:45:38 -05:00
DragonX Developers
68c24b5701 zrpc: bound concurrency, and close idle connections before the node does
Review follow-ups to 73a93f1.

Bound in-flight calls. rpcclient's single sendPostHandler goroutine
imposed an accidental ceiling of one concurrent RPC; removing it without
putting anything in its place left no bound at all. grpc-go supplies
none either -- this server sets no MaxConcurrentStreams, so the default
is math.MaxUint32 -- and dragonxd answers RPC with 8 worker threads
behind a 4096-deep queue, shared on the pool node with getblocktemplate.
Overload would therefore surface as mining latency rather than as an
error we could back off on. MaxConnsPerHost blocks the caller at the
limit instead of dialling more, which is the backpressure wanted;
MaxIdleConnsPerHost alone would only cap reuse and let us exceed the
limit while churning connections. Default 8, matching the node's
DEFAULT_HTTP_THREADS, tunable with -rpc-max-concurrent. Even 8 removes
all of the head-of-line blocking this work set out to fix.

IdleConnTimeout 90s -> 20s. dragonxd closes idle connections at 30s
(DEFAULT_HTTP_SERVER_TIMEOUT, applied via evhttp_set_timeout and not
overridden in DRAGONX.conf). At 90s we were always the second to close,
so a request could be written into a connection the server had already
sent a FIN for, and Go will not retry a POST once bytes are on the wire.
Closing first removes the race.

Reject a negative -rpc-timeout, which silently meant "unbounded", the
same as the documented 0. The check has to run after flag.Parse(); it
was initially placed before it and never fired.

Also correct the coinsupply note: hush_coinsupply walks the block index
back to genesis, loading each block from disk and memoising newcoins and
zfunds into the CBlockIndex, so the first call pays for the whole chain
and later ones are nearly free. It is not a UTXO-set scan, as the
earlier comment claimed. The measured 48s/3s figures are unchanged.

Verified: five concurrent GetLightdInfo calls all return grpc-status 0
with no errors, 46 blocks ingested, and the daemon holds 2 sockets to
the node rather than one per request; a negative timeout exits 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-26 11:44:52 -05:00
DragonX Developers
4e7a1c0f9b rpc: bound every dragonxd call with a timeout, and stop serialising them
lightwalletd used github.com/btcsuite/btcd/rpcclient for exactly one
method, RawRequest, and that package has two defects that together
produce the failure seen on the primary node:

  1. It has no timeout and no way to set one. The http.Client is built
     in an unexported newHTTPClient() and ConnConfig exposes no Timeout
     field, so a call can hang forever. Calls were observed running past
     five minutes against a healthy node that answered the same query
     from dragonx-cli in 2ms.

  2. In HTTP POST mode it runs ONE sendPostHandler goroutine which calls
     handleSendPostMessage synchronously, so the whole process has at
     most one RPC in flight. One stuck call therefore blocks the block
     ingestor, the mempool monitor and every user-facing gRPC handler at
     once.

These compound: a timeout alone would not have been enough, because
bounding the caller's wait still leaves the shared goroutine stuck
inside http.Client.Do with everything queued behind it. Only a timeout
on the HTTP client aborts the in-flight request, and only dropping the
shared goroutine lets independent callers proceed.

Patching the vendored copy is not an option here: go.mod declares
go 1.12, so automatic vendor mode (go >= 1.14) is off and the committed
vendor/ tree is silently ignored in favour of the module cache. It is
also stale -- vendor/modules.txt disagrees with go.mod on btcd,
protobuf, logrus, sqlite3 and six other modules, so -mod=vendor cannot
build at all, and `go mod vendor` would discard any patch.

Replace it with package zrpc: a ~150-line JSON-RPC client that is safe
for concurrent use and takes a timeout. The request envelope, ID
sequence, basic auth and error semantics are deliberately identical.
RPCError.Error() still renders as "<code>: <message>" because callers
recover the numeric code from the string -- common.GetSaplingInfo
checks for -8 via strings.SplitN(err.Error(), ":", 2) -- and the
non-JSON body path still reports `status code: %d, response: %q`.

Default timeout 120s, tunable with -rpc-timeout (0 disables). The bound
is set by the slowest legitimate call: coinsupply measured 48s against
a cold UTXO set, 3s once cached, so a tighter timeout would turn a
slow-but-working call into a hard failure.

Also drops rpcclient's Close=true, which opened a fresh TCP connection
per request and left hundreds of sockets in TIME_WAIT on a busy node;
idle connections are now reused and capped.

Verified against a live dragonxd: getblockchaininfo returns chain=main;
a bad height yields exactly "-8: Block height out of range" and parses
back to -8; a 1ns timeout aborts in 54us instead of hanging; and the
built daemon serves GetLightdInfo at the current height with no errors.
The pre-existing parser TestCompactBlocks failure is unrelated and
reproduces on the base commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-26 11:44:52 -05:00
DragonX Developers
17ca2b0e69 monitor: commit the running supervisor, which existed only in memory
/home/dev/lightwalletd/monitor_lwd.sh was carrying two uncommitted
production fixes, and the copy on disk had been reverted to the broken
committed version. The monitor that is actually running was executing a
deleted inode, so the fixes survived only as long as that process did --
any restart would have picked up the broken file.

The two fixes that were nearly lost:

  * `-cache-size 5000` on the launch line. Without it a relaunch warms
    the block cache from tip-400000 instead of tip-5000, which is
    several minutes of getblock storm against the local node and several
    minutes during which every wallet errors "Server's latest block is
    behind ours".

  * `EXIT_CODE=0; wait "$LWD_PID" || EXIT_CODE=$?` instead of
    `wait "$LWD_PID" || true; EXIT_CODE=$?`. The latter reads the status
    of `|| true` and is therefore always 0, so the monitor logged
    "exited cleanly. Not restarting." and broke its loop on every exit
    including crashes. That bug produced an 11h48m outage on 2026-08-21.

Recovered byte-identical from the running monitor via /proc/<pid>/fd/255
(md5 1823440d0af509c92583796af075b657) and committed so a checkout
cannot discard it again. An out-of-repo copy is kept at
/home/dev/monitor_lwd.sh.good.

Note the other branches still carry the broken blob; checking one out in
this working tree will clobber this file again. This working tree is a
live operational directory, not just a source checkout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-26 11:12:48 -05:00
DragonX Developers
7f5474ef82 version: 0.1.2, from a single constant
Bump for the GetTransaction crash fix, and make the version one value
instead of two literals that could drift.

It was duplicated: cmd/server/main.go had `var version = "0.1.1"` for
--version, and frontend/service.go had "0.1.1-dragonxlightd" inline in
the LightdInfo reply. The gRPC one is the load-bearing copy -- it is
walletrpc/service.proto:48, so every client reads it, and it is the only
way to tell from off-box which build a node is running.

That property is the point of bumping now rather than later. With it, a
rollout can be verified by probing each endpoint over TLS and reading
the advertised version, instead of shelling in to compare binary
checksums, and instead of the only alternative positive test -- calling
GetTransaction on a mempool txid, which proves the fix by crashing any
node that does not have it.

Verified: --version prints 0.1.2, and a GetLightdInfo probe against a
test instance returns 0.1.2-dragonxlightd where production still returns
0.1.1-dragonxlightd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-26 10:33:51 -05:00
DragonX Developers
62358df198 frontend: stop an unconfirmed transaction from killing the daemon
GetTransaction asserted the height out of getrawtransaction's reply
without checking it:

    txHeight = txinfo.(map[string]interface{})["height"].(float64)

dragonxd emits "height" only for a transaction that is in a block --
rawtransaction.cpp puts it inside `if (!hashBlock.IsNull())` -- so every
mempool transaction comes back without the key. The assertion then runs
nil.(float64) and panics. Nothing recovers it: grpc-go v1.24.0 installs
no recovery interceptor (there is no recover() in its server.go) and
this daemon adds none, so the panic takes down the whole process and
every wallet connected to that endpoint with it.

Any client can trigger it deliberately: broadcast a transaction, then
ask for it before it is mined. GetMempoolStream, added in b1b0d45,
hands out unconfirmed txids by design, so ordinary 0-conf use walks
straight into it.

Verified against a live mempool transaction on this node, whose reply
has neither "height" nor "blockhash": the old expression panics with
"interface conversion: interface {} is nil, not float64"; the new one
returns cleanly.

An unconfirmed transaction is now reported as tip+1, which is what
GetMempoolStream already advertises for the same transactions
(mempool.go:109).

Also harden GetSaplingInfo, which had six more unchecked assertions on
the getblockchaininfo reply. Those run on the block-ingestor goroutine,
where a panic is equally fatal. The top-level object is asserted once
and every field is read with the comma-ok form; a missing field now
degrades instead of crashing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-26 10:27:12 -05:00
2bab58c6d2 lightwalletd: adaptive reorg-lag tip + non-blocking startup coinsupply
Adaptive-lag: advertise a tip that trails the real tip by a reorg-rate-driven
lag (GetLatestBlock/GetLightdInfo) so wallets anchor shielded spends at a
settled height during reorg churn. Configurable via
-adaptive-lag/-lag-min/-lag-max/-lag-window; monitor_lwd.sh runs
-lag-min 4 -lag-max 12 -lag-window 30.

Startup coinsupply: the informational startup coinsupply RPC (result used only
for a log line) blocked the gRPC bind for minutes on a node whose supply index
is cold, keeping the lite endpoint down on every restart while it also starved
the block-cache ingestor. Run it in a goroutine so the bind is never blocked;
clients still get coinsupply on demand via the GetCoinsupply RPC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 01:05:10 -05:00
b1b0d4559b feat(frontend): implement GetMempoolStream for 0-conf lightwallet txs
The CompactTxStreamer service did not implement any mempool RPC, so
lightwallets (SilentDragonXLite / ObsidianDragonLite) received UNIMPLEMENTED
from get_mempool_stream and permanently stopped their mempool monitor. As a
result the lite wallet could not see 0-confirmation transactions and only
surfaced incoming shielded chat messages after they were mined (~1 block).

Add GetMempoolStream(Empty) returns (stream RawTransaction). A single
process-wide monitor (frontend/mempool.go) polls the node's getrawmempool and
fetches each new tx via getrawtransaction (full serialized bytes, so shielded
memos are preserved -- CompactTx's 52-byte prefix would not carry a memo), then
fans the tx out to every subscribed stream. This avoids polling the node once
per connected wallet: N clients share one poll loop. The monitor starts lazily
on the first subscription, idles without touching the node when no clients are
connected, and closes all subscriber streams when a new block is mined (the
semantics the client's monitor loop expects; the client re-syncs and reconnects).

Purely additive and wire-compatible: RawTransaction field numbers and the
cash.z.wallet.sdk.rpc package match the deployed client; existing clients that
never call GetMempoolStream are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 20:10:26 -05:00
ec1c479156 Increase block cache size to 400k for faster wallet sync
- Increase default cache-size from 40,000 to 400,000 blocks
- Add Python gRPC protobuf bindings for testing
2026-03-21 03:50:55 -05:00
aae94a4f3c changes 2026-03-20 06:55:27 -05:00
3355c1ea85 stop server script 2026-03-20 06:26:15 -05:00
ebc38c9369 Fix lightwalletd sync loop for DragonX wallet
- Handle missing Sapling upgrade in getblockchaininfo response by
  defaulting activation height to 1 (DragonX has Sapling active from
  genesis but uses NO_ACTIVATION_HEIGHT in chainparams)
- Override consensus branch ID to Sapling (76b809bb) when node reports
  Sprout (00000000) due to disabled activation heights
- Include full serialized block header in CompactBlock so the SDK can
  validate hashFinalSaplingRoot against the Sapling commitment tree
- Set CompactBlock ProtoVersion to 1
2026-03-20 06:24:54 -05:00
f4ef38c42f fix sapling activation height and consensus branch id 2026-03-20 06:11:38 -05:00
dan-s
daab197a01 update for dragonx binary changes, added monitor script 2026-03-04 15:28:02 -06:00
dan-s
ef9b241cfd cleanup 2026-02-28 00:37:44 -06:00
Duke Leto
5bab463245 Use non-legacy conf file location 2022-11-20 17:25:33 +00:00
Duke Leto
24640b178e Update 'README.md' 2022-11-20 17:20:06 +00:00
Duke Leto
5b5533c890 Update 'README.md' 2022-10-31 13:07:18 +00:00
Duke Leto
c9f6e7d7f3 Update 'README.md' 2022-10-31 13:06:25 +00:00
Duke Leto
6cc055b07f Merge pull request 'Add myself to authors' (#21) from dev into master
Reviewed-on: https://git.hush.is/hush/lightwalletd/pulls/21
2021-10-29 02:43:10 +00:00
Duke Leto
8937561710 Add myself to authors 2021-10-28 22:42:37 -04:00
jahway603
0735c58b4c Merge pull request 'added AUTHORS file' (#20) from jahway into master
Reviewed-on: https://git.hush.is/hush/lightwalletd/pulls/20
2021-10-28 03:34:44 +00:00
jahway603
1217bade73 Merge branch 'master' into jahway 2021-10-28 03:34:21 +00:00
jahway603
8881b6f1d6 added AUTHORS file 2021-10-27 23:33:19 -04:00
jahway603
3ef370621c Merge pull request 'new build scripts for ARM' (#19) from jahway into master
Reviewed-on: https://git.hush.is/hush/lightwalletd/pulls/19
2021-10-25 22:22:51 +00:00
28 changed files with 1708 additions and 137 deletions

32
.gitignore vendored
View File

@@ -1,5 +1,33 @@
# Compiled binaries
main
grpcfrontend
cert.pem
key.pem
lightwalletd
# TLS certificates and keys
*.pem
# Database files
*.db
*.sqlite
# Logs
*.log
# Go test binaries and output
*.test
*.out
*.prof
# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
*~
# Temporary files
/tmp/
# Build output
/dist/
/build/

4
AUTHORS Normal file
View File

@@ -0,0 +1,4 @@
# The Hush Developers
Jahway603 https://git.hush.is/jahway603 https://github.com/jahway603
Duke Leto https://git.hush.is/duke

View File

@@ -1,7 +1,8 @@
# Copyright (c) 2021 Jahway603 & The Hush Developers
# Copyright (c) 2024-2026 The DragonX Developers
# Released under the GPLv3
#
# Hush Lightwalletd Makefile
# DragonX Lightwalletd Makefile
# author: jahway603
#
PROJECT_NAME := "lightwalletd"
@@ -19,9 +20,9 @@ build-arm:
# Build binary for ARM architecture (aarch64)
./util/build_arm.sh
# Stop the hushd process in the hushdlwd container
#docker_img_stop_hushd:
# docker exec -i hushdlwd hush-cli stop
# Stop the dragonxd process in the container
#docker_img_stop_dragonxd:
# docker exec -i dragonxdlwd dragonx-cli stop
# Remove and delete ALL images and containers in Docker; assumes containers are stopped
#docker_remove_all:

View File

@@ -1,21 +1,20 @@
# Overview
Hush Lightwalletd is a fork of [lightwalletd](https://github.com/adityapk00/lightwalletd) original from Zcash (ZEC).
DragonX Lightwalletd is a fork of [Hush lightwalletd](https://git.hush.is/hush/lightwalletd) which is itself a fork of [lightwalletd](https://github.com/adityapk00/lightwalletd) originally from Zcash (ZEC).
It is a backend service that provides a bandwidth-efficient interface to the Hush blockchain for [SilentDragonLite cli](https://git.hush.is/hush/silentdragonlite-light-cli) and [SilentDragonLite](https://git.hush.is/hush/SilentDragonLite).
It is a backend service that provides a bandwidth-efficient interface to the DragonX blockchain for light wallet clients.
## Changes from upstream lightwalletd
This version of lightwalletd extends lightwalletd and:
## Features
* Adds support for HUSH
* Adds support for transparent addresses
* Adds several new RPC calls for lightclients
* Support for DragonX (standalone chain with `dragonxd`)
* Support for transparent addresses
* Several RPC calls for light clients
* Lots of perf improvements
* Replaces SQLite with in-memory cache for Compact Blocks
* Replace local Txstore, delegating Tx lookups to hushd
* Remove the need for a separate ingestor
* In-memory cache for Compact Blocks (replaces SQLite)
* Tx lookups delegated to dragonxd
* No separate ingestor needed
## Running your own SDL lightwalletd
## Running your own DragonX lightwalletd
#### 0. First, install Go
You will need Go >= 1.13 which you can download from the official [download page](https://golang.org/dl/) or install via your OS package manager.
@@ -28,15 +27,15 @@ If you're using Ubuntu or Debian, try:
$ sudo apt install golang
```
#### 1. Run a Hush node.
Either compile or build the [Hush Daemon (hushd)](https://git.hush.is/hush/hush3).
#### 1. Run a DragonX node.
Install the DragonX daemon (`dragonxd`) and CLI (`dragonx-cli`).
Next, change your HUSH3.conf file to something like the following:
Next, ensure your DRAGONX.conf file (at `~/.hush/DRAGONX/DRAGONX.conf`) has something like the following:
```
rpcuser=user-CHANGETHIS
rpcpassword=pass-CHANGETHIS
rpcport=18031
rpcport=21769
server=1
txindex=1
rpcworkqueue=256
@@ -44,7 +43,7 @@ rpcallowip=127.0.0.1
rpcbind=127.0.0.1
```
Then start `hushd` in your command window. You might need to run with `-reindex` the first time if you are enabling the `txindex` or `insightexplorer` options for the first time. The reindex might take a while.
Then start `dragonxd`. You might need to run with `-reindex` the first time if you are enabling the `txindex` option for the first time. The reindex might take a while.
#### 2. Compile lightwalletd
Run the build script.
@@ -77,8 +76,8 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location / {
# Replace localhost:9067 with the address and port of your gRPC server if using a custom port
grpc_pass grpc://your_host.net:9067;
# Replace localhost:9069 with the address and port of your gRPC server if using a custom port
grpc_pass grpc://your_host.net:9069;
}
}
```
@@ -86,28 +85,18 @@ server {
Then run the lightwalletd frontend with the following (Note: we use the "-no-tls" option as we are using NGINX as a reverse proxy and letting it handle the TLS authentication for us instead):
```
./lightwalletd -bind-addr your_host.net:9067 -conf-file ~/.komodo/HUSH3/HUSH3.conf -no-tls
./lightwalletd -bind-addr your_host.net:9069 -conf-file ~/.hush/DRAGONX/DRAGONX.conf -no-tls
```
##### Option B: "Let's Encrypt" certificate just using lightwalletd without NGINX
The other option is to configure lightwalletd to handle its own TLS authentication. Once you have a certificate that you want to use (from a certificate authority), pass the certificate to the frontend as follows:
```
./lightwalletd -bind-addr 127.0.0.1:9067 -conf-file ~/.komodo/HUSH3/HUSH3.conf -tls-cert /etc/letsencrypt/live/YOURWEBSITE/fullchain.pem -tls-key /etc/letsencrypt/live/YOURWEBSITE/privkey.pem
./lightwalletd -bind-addr 127.0.0.1:9069 -conf-file ~/.hush/DRAGONX/DRAGONX.conf -tls-cert /etc/letsencrypt/live/YOURWEBSITE/fullchain.pem -tls-key /etc/letsencrypt/live/YOURWEBSITE/privkey.pem
```
#### 4. Point the `silentdragonlite-cli` to this server
You should start seeing the frontend ingest and cache the Hush blocks after ~15 seconds.
Now, connect to your server! (Substitute with your own below)
```
git clone https://git.hush.is/hush/silentdragonlite-cli
cd silentdragonlite-cli
cargo build --release
./target/release/silentdragonlite-cli --server https://lite.example.org
```
* If you have trouble compiling silentdragonlite-cli, then [please refer to it's separate documentation here](https://git.hush.is/hush/silentdragonlite-cli) on how to build it and what pre-requisites need to be installed.
#### 4. Point a light wallet client to this server
You should start seeing the frontend ingest and cache the DragonX blocks after ~15 seconds.
## Lightwalletd Command-line Options
@@ -115,7 +104,7 @@ These are the current different command line options for lightwalletd:
| CLI option | Default | What it does |
|------------|:--------------:|------------------------------:|
| -bind-addr | 127.0.0.1:9067 | address and port to listen on |
| -bind-addr | 127.0.0.1:9069 | address and port to listen on |
| -tls-cert | blank | the path to a TLS certificate |
| -tls-key | blank | the path to a TLS key file |
| -no-tls | false | Disable TLS, serve un-encrypted traffic |
@@ -125,7 +114,12 @@ These are the current different command line options for lightwalletd:
| -cache-size| 40000 | number of blocks to hold in the cache |
## Support
For support or other questions, join us on [Telegram](https://hush.is/telegram), or tweet at [@MyHushTeam](https://twitter.com/MyHushTeam), or toot at our [Mastodon](https://fosstodon.org/@myhushteam) or join [Telegram Support](https://hush.is/telegram_support).
For support or other questions, join us on [Telegram](https://hush.is/telegram) or join [Telegram Support](https://hush.is/telegram_support).
## License
GPLv3 or later
# Copyright
2016-2022 The Hush Developers
2024-2026 The DragonX Developers

View File

@@ -77,32 +77,46 @@ func loggerFromContext(ctx context.Context) *logrus.Entry {
}
type Options struct {
bindAddr string `json:"bind_address,omitempty"`
tlsCertPath string `json:"tls_cert_path,omitempty"`
tlsKeyPath string `json:"tls_cert_key,omitempty"`
noTLS bool `json:no_tls,omitempty`
logLevel uint64 `json:"log_level,omitempty"`
logPath string `json:"log_file,omitempty"`
hush3ConfPath string `json:"hush3_conf,omitempty"`
cacheSize int `json:"hush3_conf,omitempty"`
bindAddr string `json:"bind_address,omitempty"`
tlsCertPath string `json:"tls_cert_path,omitempty"`
tlsKeyPath string `json:"tls_cert_key,omitempty"`
noTLS bool `json:no_tls,omitempty`
logLevel uint64 `json:"log_level,omitempty"`
logPath string `json:"log_file,omitempty"`
confPath string `json:"conf_file,omitempty"`
cacheSize int `json:"cache_size,omitempty"`
adaptiveLag bool `json:"adaptive_lag,omitempty"`
lagMin int `json:"lag_min,omitempty"`
lagMax int `json:"lag_max,omitempty"`
lagWindowMin int `json:"lag_window_min,omitempty"`
rpcTimeout time.Duration `json:"rpc_timeout,omitempty"`
rpcMaxConcurrent int `json:"rpc_max_concurrent,omitempty"`
}
func main() {
var version = "0.1.1" // set version number
var version = common.Version
opts := &Options{}
flag.StringVar(&opts.bindAddr, "bind-addr", "127.0.0.1:9067", "the address to listen on")
flag.StringVar(&opts.bindAddr, "bind-addr", "127.0.0.1:9069", "the address to listen on")
flag.StringVar(&opts.tlsCertPath, "tls-cert", "", "the path to a TLS certificate (optional)")
flag.StringVar(&opts.tlsKeyPath, "tls-key", "", "the path to a TLS key file (optional)")
flag.BoolVar(&opts.noTLS, "no-tls", false, "Disable TLS, serve un-encrypted traffic.")
flag.Uint64Var(&opts.logLevel, "log-level", uint64(logrus.InfoLevel), "log level (logrus 1-7)")
flag.StringVar(&opts.logPath, "log-file", "", "log file to write to")
flag.StringVar(&opts.hush3ConfPath, "conf-file", "", "conf file to pull RPC creds from")
flag.IntVar(&opts.cacheSize, "cache-size", 40000, "number of blocks to hold in the cache")
flag.StringVar(&opts.confPath, "conf-file", "", "conf file to pull RPC creds from")
flag.IntVar(&opts.cacheSize, "cache-size", 400000, "number of blocks to hold in the cache")
flag.BoolVar(&opts.adaptiveLag, "adaptive-lag", true, "advertise a tip that trails the real tip by a reorg-rate-driven lag, so wallets anchor shielded spends at a settled height")
flag.IntVar(&opts.lagMin, "lag-min", 1, "minimum confirmation lag in blocks (applied when the chain is stable)")
flag.IntVar(&opts.lagMax, "lag-max", 12, "maximum confirmation lag in blocks (cap during heavy reorgs)")
flag.IntVar(&opts.lagWindowMin, "lag-window", 30, "minutes of recent reorg history used to size the adaptive lag")
flag.DurationVar(&opts.rpcTimeout, "rpc-timeout", frontend.DefaultRPCTimeout, "bound a single dragonxd JSON-RPC call; 0 disables. Without it one stuck call blocks every other caller")
flag.IntVar(&opts.rpcMaxConcurrent, "rpc-max-concurrent", frontend.DefaultRPCMaxConcurrent, "maximum dragonxd JSON-RPC calls in flight at once; matches the node's RPC worker threads")
// creating --version as a requirement of help2man
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
fmt.Printf("Hush lightwalletd version " + version + "\n")
fmt.Printf("DragonX lightwalletd version " + version + "\n")
os.Exit(0)
}
@@ -110,14 +124,21 @@ func main() {
// TODO support config from file and env vars
flag.Parse()
if opts.hush3ConfPath == "" {
// A negative duration silently means "no timeout", the same as the
// documented 0, so reject it rather than quietly running unbounded.
if opts.rpcTimeout < 0 {
fmt.Fprintln(os.Stderr, "-rpc-timeout must not be negative; use 0 to disable the timeout")
os.Exit(1)
}
if opts.confPath == "" {
flag.Usage()
os.Exit(1)
}
if !opts.noTLS && (opts.tlsCertPath == "" || opts.tlsKeyPath == "") {
println("Please specify a TLS certificate/key to use. You can use a self-signed certificate.")
println("See https://git.hush.is/hush/lightwalletd/src/branch/master/README.md#running-your-own-sdl-lightwalletd")
println("See https://git.hush.is/hush/lightwalletd/src/branch/master/README.md")
os.Exit(1)
}
@@ -159,17 +180,17 @@ func main() {
reflection.Register(server)
}
// Initialize Hush RPC client. Right now (Jan 2018) this is only for
// Initialize DragonX RPC client. Right now this is only for
// sending transactions, but in the future it could back a different type
// of block streamer.
rpcClient, err := frontend.NewZRPCFromConf(opts.hush3ConfPath)
rpcClient, err := frontend.NewZRPCFromConf(opts.confPath, opts.rpcTimeout, opts.rpcMaxConcurrent)
if err != nil {
log.WithFields(logrus.Fields{
"error": err,
}).Warn("HUSH3.conf failed, will try empty credentials for rpc")
}).Warn("DRAGONX.conf failed, will try empty credentials for rpc")
rpcClient, err = frontend.NewZRPCFromCreds("127.0.0.1:18031", "", "")
rpcClient, err = frontend.NewZRPCFromCreds("127.0.0.1:21769", "", "", opts.rpcTimeout, opts.rpcMaxConcurrent)
if err != nil {
log.WithFields(logrus.Fields{
@@ -188,18 +209,25 @@ func main() {
log.Info("Got sapling height ", saplingHeight, " chain ", chainName, " branchID ", branchID, " difficulty ", difficulty, longestchain, " longestchain ", notarized, " notarized ")
// Get the Coinsupply from the RPC
result, coin, height, supply, zfunds, total, err := common.GetCoinsupply(rpcClient)
if err != nil {
log.WithFields(logrus.Fields{
"error": err,
}).Warn("Unable to get coinsupply")
}
log.Info(" result ", result, " coin ", coin, " height", height, "supply", supply, "zfunds", zfunds, "total", total)
// Fetch coinsupply for an informational startup log line only (the result is not
// used elsewhere). On a node whose supply index is cold this RPC can take minutes,
// so run it in the background rather than blocking the gRPC bind on it — otherwise
// the lite endpoint stays down for the entire duration on every restart. Clients
// still get coinsupply on demand via the GetCoinsupply RPC.
go func() {
result, coin, height, supply, zfunds, total, err := common.GetCoinsupply(rpcClient)
if err != nil {
log.WithFields(logrus.Fields{
"error": err,
}).Warn("Unable to get coinsupply")
return
}
log.Info(" result ", result, " coin ", coin, " height", height, "supply", supply, "zfunds", zfunds, "total", total)
}()
// Initialize the cache
cache := common.NewBlockCache(opts.cacheSize)
cache.ConfigureLag(opts.adaptiveLag, opts.lagWindowMin, opts.lagMin, opts.lagMax)
stopChan := make(chan bool, 1)

View File

@@ -3,6 +3,7 @@ package common
import (
"bytes"
"sync"
"time"
"git.hush.is/hush/lightwalletd/walletrpc"
"github.com/golang/protobuf/proto"
@@ -22,14 +23,34 @@ type BlockCache struct {
m map[int]*BlockCacheEntry
mutex sync.RWMutex
// Adaptive confirmation lag. lightwalletd advertises a tip that trails the
// real tip by a number of blocks that scales with the recent reorg rate, so
// wallets anchor shielded spends at a settled height while the chain is
// churning, and at (near) the real tip when it is stable. reorgTimes holds
// the times of recent reorg detections, pruned to lagWindow.
reorgTimes []time.Time
lagWindow time.Duration
lagMin int
lagMax int
adaptiveLag bool
// advTip is the cached advertised (lag-adjusted) tip, kept fresh by Add so
// the per-block serving cap (HeightAllowed) is a cheap read. -1 until ready.
advTip int
}
func NewBlockCache(maxEntries int) *BlockCache {
return &BlockCache{
MaxEntries: maxEntries,
FirstBlock: -1,
LastBlock: -1,
m: make(map[int]*BlockCacheEntry),
MaxEntries: maxEntries,
FirstBlock: -1,
LastBlock: -1,
m: make(map[int]*BlockCacheEntry),
adaptiveLag: true,
lagWindow: 30 * time.Minute,
lagMin: 1,
lagMax: 12,
advTip: -1,
}
}
@@ -57,6 +78,8 @@ func (c *BlockCache) Add(height int, block *walletrpc.CompactBlock) (error, bool
// Don't allow out-of-order blocks. This is more of a sanity check than anything
// If there is a reorg, then the ingestor needs to handle it.
if c.m[height-1] != nil && !bytes.Equal(block.PrevHash, c.m[height-1].hash) {
// Record the reorg so the adaptive confirmation lag can react to it.
c.reorgTimes = append(c.reorgTimes, time.Now())
return nil, true
}
@@ -81,6 +104,12 @@ func (c *BlockCache) Add(height int, block *walletrpc.CompactBlock) (error, bool
c.FirstBlock = c.FirstBlock + 1
}
// Keep the advertised (lag-adjusted) tip fresh for the block-serving cap.
c.advTip = c.LastBlock - c.computeLagLocked()
if c.advTip < c.FirstBlock {
c.advTip = c.FirstBlock
}
//println("Cache size is ", len(c.m))
return nil, false
}
@@ -116,3 +145,78 @@ func (c *BlockCache) GetLatestBlock() int {
return c.LastBlock
}
// ConfigureLag sets the adaptive-confirmation-lag parameters. Called once at
// startup from the command-line flags.
func (c *BlockCache) ConfigureLag(adaptive bool, windowMinutes, min, max int) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.adaptiveLag = adaptive
c.lagWindow = time.Duration(windowMinutes) * time.Minute
c.lagMin = min
c.lagMax = max
}
// computeLagLocked returns the confirmation lag (in blocks) to apply right now
// and prunes reorg events that have aged out of the window. The lag is the
// configured floor plus the number of reorgs seen within lagWindow, capped at
// lagMax. Caller must hold c.mutex.
func (c *BlockCache) computeLagLocked() int {
if !c.adaptiveLag {
return c.lagMin
}
cutoff := time.Now().Add(-c.lagWindow)
kept := c.reorgTimes[:0]
for _, t := range c.reorgTimes {
if t.After(cutoff) {
kept = append(kept, t)
}
}
c.reorgTimes = kept
lag := c.lagMin + len(kept)
if lag > c.lagMax {
lag = c.lagMax
}
if lag < c.lagMin {
lag = c.lagMin
}
return lag
}
// CurrentLag returns the confirmation lag currently being applied.
func (c *BlockCache) CurrentLag() int {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.computeLagLocked()
}
// AdvertisedLatestBlock is the tip height lightwalletd reports to wallets: the
// real cache tip minus the adaptive confirmation lag. Wallets sync to and
// anchor shielded spends at this settled height, immune to tip reorgs. The
// internal cache and ingestor continue to track the real tip.
func (c *BlockCache) AdvertisedLatestBlock() int {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.LastBlock < 0 {
return c.LastBlock
}
adv := c.LastBlock - c.computeLagLocked()
if adv < c.FirstBlock {
adv = c.FirstBlock
}
c.advTip = adv
return adv
}
// HeightAllowed reports whether height is at or below the advertised
// (lag-adjusted) tip. lightwalletd refuses to serve blocks above it so wallets
// cannot sync or anchor shielded spends into the unstable reorg zone. Cheap
// (read lock, no recompute); advTip is kept fresh by Add.
func (c *BlockCache) HeightAllowed(height int) bool {
c.mutex.RLock()
defer c.mutex.RUnlock()
if c.advTip < 0 {
return true
}
return height <= c.advTip
}

View File

@@ -10,12 +10,12 @@ import (
"git.hush.is/hush/lightwalletd/parser"
"git.hush.is/hush/lightwalletd/walletrpc"
"github.com/btcsuite/btcd/rpcclient"
"git.hush.is/hush/lightwalletd/zrpc"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func GetSaplingInfo(rpcClient *rpcclient.Client) (int, int, string, string, int, int, int, error) {
func GetSaplingInfo(rpcClient *zrpc.Client) (int, int, string, string, int, int, int, error) {
result, rpcErr := rpcClient.RawRequest("getblockchaininfo", make([]json.RawMessage, 0))
var err error
@@ -25,7 +25,7 @@ func GetSaplingInfo(rpcClient *rpcclient.Client) (int, int, string, string, int,
if rpcErr != nil {
errParts := strings.SplitN(rpcErr.Error(), ":", 2)
errCode, err = strconv.ParseInt(errParts[0], 10, 32)
//Check to see if we are requesting a height the hushd doesn't have yet
//Check to see if we are requesting a height the dragonxd doesn't have yet
if err == nil && errCode == -8 {
return -1, -1, "", "", -1, -1, -1, nil
}
@@ -38,24 +38,48 @@ func GetSaplingInfo(rpcClient *rpcclient.Client) (int, int, string, string, int,
return -1, -1, "", "", -1, -1, -1, errors.Wrap(err, "error reading JSON response")
}
chainName := f.(map[string]interface{})["chain"].(string)
// Assert the top-level object once, then read every field with the comma-ok
// form. These run on the block-ingestor goroutine, and nothing in this
// daemon or in grpc-go v1.24.0 recovers a panic, so one unexpected response
// shape would take the whole process down rather than fail one call.
fmap, ok := f.(map[string]interface{})
if !ok {
return -1, -1, "", "", -1, -1, -1, errors.New("getblockchaininfo: unexpected response shape")
}
chainName, _ := fmap["chain"].(string)
upgradeJSON := f.(map[string]interface{})["upgrades"]
saplingJSON := upgradeJSON.(map[string]interface{})["76b809bb"] // Sapling ID
saplingHeight := saplingJSON.(map[string]interface{})["activationheight"].(float64)
// DragonX has Sapling active from block 1 but sets NO_ACTIVATION_HEIGHT in
// chainparams, so dragonxd omits it from the upgrades map. Fall back to
// height 1 when the key is absent.
saplingHeight := float64(1)
if upgradesMap, ok := fmap["upgrades"].(map[string]interface{}); ok {
if saplingJSON, ok := upgradesMap["76b809bb"].(map[string]interface{}); ok {
if h, ok := saplingJSON["activationheight"].(float64); ok {
saplingHeight = h
}
}
}
blockHeight := f.(map[string]interface{})["headers"].(float64)
difficulty := f.(map[string]interface{})["difficulty"].(float64)
longestchain := f.(map[string]interface{})["longestchain"].(float64)
notarized := f.(map[string]interface{})["notarized"].(float64)
blockHeight, _ := fmap["headers"].(float64)
difficulty, _ := fmap["difficulty"].(float64)
longestchain, _ := fmap["longestchain"].(float64)
notarized, _ := fmap["notarized"].(float64)
consensus := f.(map[string]interface{})["consensus"]
branchID := consensus.(map[string]interface{})["nextblock"].(string)
// DragonX always uses Sapling consensus rules but CurrentEpochBranchId()
// returns Sprout (0) for full nodes because the activation heights are
// set to NO_ACTIVATION_HEIGHT. Override to the correct Sapling branch ID.
branchID := "76b809bb"
consensus, ok := f.(map[string]interface{})["consensus"]
if ok {
if nextblock, ok := consensus.(map[string]interface{})["nextblock"].(string); ok && nextblock != "00000000" {
branchID = nextblock
}
}
return int(saplingHeight), int(blockHeight), chainName, branchID, int(difficulty), int(longestchain), int(notarized), nil
}
func GetCoinsupply(rpcClient *rpcclient.Client) (string, string, int, int, int, int, error) {
func GetCoinsupply(rpcClient *zrpc.Client) (string, string, int, int, int, int, error) {
result1, rpcErr := rpcClient.RawRequest("coinsupply", make([]json.RawMessage, 0))
var err error
@@ -65,7 +89,7 @@ func GetCoinsupply(rpcClient *rpcclient.Client) (string, string, int, int, int,
if rpcErr != nil {
errParts := strings.SplitN(rpcErr.Error(), ":", 2)
errCode, err = strconv.ParseInt(errParts[0], 10, 32)
//Check to see if we are requesting a height the hushd doesn't have yet
//Check to see if we are requesting a height the dragonxd doesn't have yet
if err == nil && errCode == -8 {
return "", "", -1, -1, -1, -1, nil
}
@@ -78,17 +102,49 @@ func GetCoinsupply(rpcClient *rpcclient.Client) (string, string, int, int, int,
return "", "", -1, -1, -1, -1, errors.Wrap(err, "error reading JSON response")
}
result := f.(map[string]interface{})["result"].(string)
coin := f.(map[string]interface{})["coin"].(string)
height := f.(map[string]interface{})["height"].(float64)
supply := f.(map[string]interface{})["supply"].(float64)
zfunds := f.(map[string]interface{})["zfunds"].(float64)
total := f.(map[string]interface{})["total"].(float64)
coinsupply, ok := f.(map[string]interface{})
if !ok {
return "", "", -1, -1, -1, -1, errors.New("unexpected coinsupply response format")
}
return result, coin, int(height), int(supply), int(zfunds), int(total), nil
getStringField := func(key string) string {
value, ok := coinsupply[key]
if !ok || value == nil {
return ""
}
if strValue, ok := value.(string); ok {
return strValue
}
return fmt.Sprintf("%v", value)
}
getNumberField := func(key string) int {
value, ok := coinsupply[key]
if !ok || value == nil {
return 0
}
number, ok := value.(float64)
if !ok {
return 0
}
return int(number)
}
result := getStringField("result")
coin := getStringField("coin")
height := getNumberField("height")
supply := getNumberField("supply")
zfunds := getNumberField("zfunds")
total := getNumberField("total")
return result, coin, height, supply, zfunds, total, nil
}
func getBlockFromRPC(rpcClient *rpcclient.Client, height int) (*walletrpc.CompactBlock, error) {
func getBlockFromRPC(rpcClient *zrpc.Client, height int) (*walletrpc.CompactBlock, error) {
params := make([]json.RawMessage, 2)
params[0] = json.RawMessage("\"" + strconv.Itoa(height) + "\"")
params[1] = json.RawMessage("0")
@@ -101,7 +157,7 @@ func getBlockFromRPC(rpcClient *rpcclient.Client, height int) (*walletrpc.Compac
if rpcErr != nil {
errParts := strings.SplitN(rpcErr.Error(), ":", 2)
errCode, err = strconv.ParseInt(errParts[0], 10, 32)
//Check to see if we are requesting a height the hushd doesn't have yet
//Check to see if we are requesting a height the dragonxd doesn't have yet
if err == nil && errCode == -8 {
return nil, nil
}
@@ -131,7 +187,7 @@ func getBlockFromRPC(rpcClient *rpcclient.Client, height int) (*walletrpc.Compac
return block.ToCompact(), nil
}
func BlockIngestor(rpcClient *rpcclient.Client, cache *BlockCache, log *logrus.Entry,
func BlockIngestor(rpcClient *zrpc.Client, cache *BlockCache, log *logrus.Entry,
stopChan chan bool, startHeight int) {
reorgCount := 0
height := startHeight
@@ -166,7 +222,7 @@ func BlockIngestor(rpcClient *rpcclient.Client, cache *BlockCache, log *logrus.E
if timeoutCount == 3 {
log.WithFields(logrus.Fields{
"timeouts": timeoutCount,
}).Warn("unable to issue RPC call to hushd node 3 times")
}).Warn("unable to issue RPC call to dragonxd node 3 times")
break
}
}
@@ -207,7 +263,14 @@ func BlockIngestor(rpcClient *rpcclient.Client, cache *BlockCache, log *logrus.E
}
}
func GetBlock(rpcClient *rpcclient.Client, cache *BlockCache, height int) (*walletrpc.CompactBlock, error) {
func GetBlock(rpcClient *zrpc.Client, cache *BlockCache, height int) (*walletrpc.CompactBlock, error) {
// Don't serve blocks above the advertised (lag-adjusted) tip, so wallets can
// neither sync nor anchor shielded spends into the unstable reorg zone.
if !cache.HeightAllowed(height) {
return nil, errors.New(
fmt.Sprintf("Block %d is above the advertised tip (reorg-lag protection)", height))
}
// First, check the cache to see if we have the block
block := cache.Get(height)
if block != nil {
@@ -230,7 +293,7 @@ func GetBlock(rpcClient *rpcclient.Client, cache *BlockCache, height int) (*wall
return block, nil
}
func GetBlockRange(rpcClient *rpcclient.Client, cache *BlockCache,
func GetBlockRange(rpcClient *zrpc.Client, cache *BlockCache,
blockOut chan<- walletrpc.CompactBlock, errOut chan<- error, start, end int) {
// Go over [start, end] inclusive
@@ -241,6 +304,12 @@ func GetBlockRange(rpcClient *rpcclient.Client, cache *BlockCache,
return
}
if block == nil {
errOut <- errors.New(
fmt.Sprintf("Block %d was nil without error", i))
return
}
blockOut <- *block
}

14
common/version.go Normal file
View File

@@ -0,0 +1,14 @@
package common
// Version is the single source of truth for this daemon's version.
//
// It was previously duplicated as a literal in two places that could drift:
// cmd/server/main.go's --version output and the Version field of the LightdInfo
// gRPC reply in frontend/service.go. The gRPC one is the load-bearing copy --
// it is walletrpc/service.proto:48, so every client and every operator probe
// reads it, and it is the only way to tell from off-box which build a node is
// running.
const Version = "0.1.3"
// VersionString is what GetLightdInfo advertises to clients.
const VersionString = Version + "-dragonxlightd"

173
frontend/mempool.go Normal file
View File

@@ -0,0 +1,173 @@
package frontend
import (
"encoding/hex"
"encoding/json"
"sync"
"time"
"git.hush.is/hush/lightwalletd/zrpc"
"github.com/sirupsen/logrus"
"git.hush.is/hush/lightwalletd/common"
"git.hush.is/hush/lightwalletd/walletrpc"
)
// mempoolMonitor polls the node's mempool ONCE (regardless of how many wallets are streaming) and
// fans each new transaction out to every subscribed GetMempoolStream handler. This replaces
// per-connection polling: N connected wallets no longer each hit getrawmempool/getrawtransaction.
//
// Lifecycle matches the semantics the client's monitor loop expects: while a block is current the
// monitor emits each mempool tx to subscribers exactly once; when a new block is mined it resets
// and closes all subscriber channels, so each handler returns (closing its stream) and the client
// reconnects after re-syncing the block. The poller starts lazily on the first subscription and,
// when no clients are connected, idles without touching the node.
type mempoolMonitor struct {
mu sync.Mutex
subs map[int]chan *walletrpc.RawTransaction
nextID int
seen map[string]bool // txids already emitted for the current block
ordered []*walletrpc.RawTransaction // emitted txs in arrival order, replayed to late subscribers
}
var (
sharedMempoolMonitor *mempoolMonitor
mempoolMonitorOnce sync.Once
)
// getMempoolMonitor returns the process-wide mempool monitor, starting its poller on first use.
// The client/cache/log are bound once (they are process singletons on the SqlStreamer).
func getMempoolMonitor(client *zrpc.Client, cache *common.BlockCache, log *logrus.Entry) *mempoolMonitor {
mempoolMonitorOnce.Do(func() {
sharedMempoolMonitor = &mempoolMonitor{
subs: make(map[int]chan *walletrpc.RawTransaction),
seen: make(map[string]bool),
}
go sharedMempoolMonitor.run(client, cache, log)
})
return sharedMempoolMonitor
}
// subscribe registers a stream. It returns the subscriber id, a channel of subsequent mempool txs,
// and a snapshot of txs already emitted this block (to be sent first). Registration and snapshot
// are taken atomically, so every tx reaches a subscriber exactly once (snapshot xor channel).
func (m *mempoolMonitor) subscribe() (int, chan *walletrpc.RawTransaction, []*walletrpc.RawTransaction) {
m.mu.Lock()
defer m.mu.Unlock()
id := m.nextID
m.nextID++
ch := make(chan *walletrpc.RawTransaction, 256)
m.subs[id] = ch
snapshot := make([]*walletrpc.RawTransaction, len(m.ordered))
copy(snapshot, m.ordered)
return id, ch, snapshot
}
// unsubscribe removes a stream. Safe to call after a new-block reset already dropped it (the map
// lookup guards against a double close).
func (m *mempoolMonitor) unsubscribe(id int) {
m.mu.Lock()
defer m.mu.Unlock()
if ch, ok := m.subs[id]; ok {
delete(m.subs, id)
close(ch)
}
}
// reset is called when a new block is mined: forget this block's txs and close every subscriber
// stream so clients re-sync the block and reconnect.
func (m *mempoolMonitor) reset() {
m.mu.Lock()
defer m.mu.Unlock()
m.seen = make(map[string]bool)
m.ordered = nil
for id, ch := range m.subs {
delete(m.subs, id)
close(ch)
}
}
func (m *mempoolMonitor) run(client *zrpc.Client, cache *common.BlockCache, log *logrus.Entry) {
lastHeight := cache.GetLatestBlock()
for {
// Track block height even while idle, so a new subscriber isn't immediately closed by a
// height that advanced during the idle period.
if h := cache.GetLatestBlock(); h != lastHeight {
lastHeight = h
m.reset()
}
m.mu.Lock()
idle := len(m.subs) == 0
m.mu.Unlock()
if idle {
// No one is listening; don't touch the node until a client subscribes.
time.Sleep(2 * time.Second)
continue
}
height := uint64(lastHeight + 1)
// List the current mempool txids: getrawmempool(false) -> ["txid", ...].
mpParams := []json.RawMessage{json.RawMessage("false")}
result, rpcErr := client.RawRequest("getrawmempool", mpParams)
if rpcErr != nil {
log.Warningf("mempool monitor: getrawmempool failed: %s", rpcErr.Error())
time.Sleep(2 * time.Second)
continue
}
var txids []string
if err := json.Unmarshal(result, &txids); err != nil {
log.Warningf("mempool monitor: cannot parse getrawmempool: %s", err.Error())
time.Sleep(2 * time.Second)
continue
}
for _, txid := range txids {
m.mu.Lock()
already := m.seen[txid]
m.mu.Unlock()
if already {
continue
}
// Fetch the full serialized tx: getrawtransaction("txid") -> hex. Full bytes are
// required so shielded memos survive (a CompactTx's 52-byte prefix would not carry a
// memo). The mempool txid is already in RPC display byte order, so (unlike
// GetTransaction) no reversal is needed.
txParams := []json.RawMessage{json.RawMessage("\"" + txid + "\"")}
txResult, txErr := client.RawRequest("getrawtransaction", txParams)
if txErr != nil {
// The tx may have been mined or evicted between listing and fetch; skip it.
continue
}
var txhex string
if err := json.Unmarshal(txResult, &txhex); err != nil {
continue
}
txBytes, err := hex.DecodeString(txhex)
if err != nil {
continue
}
rtx := &walletrpc.RawTransaction{Data: txBytes, Height: height}
// Record + fan out under the lock. The send is non-blocking (default case), so a slow
// subscriber never stalls the poller or the other subscribers.
m.mu.Lock()
if !m.seen[txid] {
m.seen[txid] = true
m.ordered = append(m.ordered, rtx)
for _, ch := range m.subs {
select {
case ch <- rtx:
default:
// Slow subscriber; drop. Block sync will deliver the confirmed tx later.
}
}
}
m.mu.Unlock()
}
time.Sleep(2 * time.Second)
}
}

View File

@@ -2,13 +2,30 @@ package frontend
import (
"net"
"time"
"github.com/btcsuite/btcd/rpcclient"
"git.hush.is/hush/lightwalletd/zrpc"
"github.com/pkg/errors"
ini "gopkg.in/ini.v1"
)
func NewZRPCFromConf(confPath string) (*rpcclient.Client, error) {
// DefaultRPCTimeout bounds a single JSON-RPC round trip to dragonxd.
//
// Why 120s and not something tighter: the slowest legitimate call this daemon
// makes is `coinsupply`, measured at 48s on first call and 3s afterwards.
// hush_coinsupply walks the block index back to genesis loading each block from
// disk, memoising newcoins/zfunds into the CBlockIndex as it goes, so the first
// call pays for the whole chain and later ones are nearly free. A timeout below
// that first-call cost would turn a slow-but-working call into a hard failure.
// 120s leaves ~2.5x headroom while still bounding a hang that is otherwise
// unbounded -- calls were seen running past five minutes.
const DefaultRPCTimeout = 120 * time.Second
// DefaultRPCMaxConcurrent matches dragonxd's DEFAULT_HTTP_THREADS. Asking for
// more in-flight calls than the node has worker threads only adds queueing.
const DefaultRPCMaxConcurrent = 8
func NewZRPCFromConf(confPath string, timeout time.Duration, maxConcurrent int) (*zrpc.Client, error) {
cfg, err := ini.Load(confPath)
if err != nil {
return nil, errors.Wrap(err, "failed to read config file")
@@ -19,19 +36,10 @@ func NewZRPCFromConf(confPath string) (*rpcclient.Client, error) {
username := cfg.Section("").Key("rpcuser").String()
password := cfg.Section("").Key("rpcpassword").String()
return NewZRPCFromCreds(net.JoinHostPort(rpcaddr, rpcport), username, password)
return NewZRPCFromCreds(net.JoinHostPort(rpcaddr, rpcport), username, password, timeout, maxConcurrent)
}
func NewZRPCFromCreds(addr, username, password string) (*rpcclient.Client, error) {
// Connect to local hush RPC server using HTTP POST mode.
connCfg := &rpcclient.ConnConfig{
Host: addr,
User: username,
Pass: password,
HTTPPostMode: true, // Hush only supports HTTP POST mode
DisableTLS: true, // Hush does not provide TLS by default
}
// Notice the notification parameter is nil since notifications are
// not supported in HTTP POST mode.
return rpcclient.New(connCfg, nil)
func NewZRPCFromCreds(addr, username, password string, timeout time.Duration, maxConcurrent int) (*zrpc.Client, error) {
// DragonX only supports HTTP POST mode and does not provide TLS by default.
return zrpc.New(addr, username, password, timeout, maxConcurrent), nil
}

View File

@@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/btcsuite/btcd/rpcclient"
"git.hush.is/hush/lightwalletd/zrpc"
"github.com/sirupsen/logrus"
"git.hush.is/hush/lightwalletd/common"
@@ -24,11 +24,11 @@ var (
// the service type
type SqlStreamer struct {
cache *common.BlockCache
client *rpcclient.Client
client *zrpc.Client
log *logrus.Entry
}
func NewSQLiteStreamer(client *rpcclient.Client, cache *common.BlockCache, log *logrus.Entry) (walletrpc.CompactTxStreamerServer, error) {
func NewSQLiteStreamer(client *zrpc.Client, cache *common.BlockCache, log *logrus.Entry) (walletrpc.CompactTxStreamerServer, error) {
return &SqlStreamer{cache, client, log}, nil
}
@@ -41,7 +41,13 @@ func (s *SqlStreamer) GetCache() *common.BlockCache {
}
func (s *SqlStreamer) GetLatestBlock(ctx context.Context, placeholder *walletrpc.ChainSpec) (*walletrpc.BlockID, error) {
latestBlock := s.cache.GetLatestBlock()
// Advertise a tip that trails the real tip by the adaptive confirmation lag,
// so wallets anchor shielded spends at a settled height during reorg churn.
latestBlock := s.cache.AdvertisedLatestBlock()
s.log.WithFields(logrus.Fields{
"latestBlock": latestBlock,
"adaptiveLag": s.cache.CurrentLag(),
}).Info("GetLatestBlock called")
if latestBlock == -1 {
return nil, errors.New("Cache is empty. Server is probably not yet ready.")
@@ -76,7 +82,7 @@ func (s *SqlStreamer) GetAddressTxids(addressBlockFilter *walletrpc.TransparentA
s.log.Errorf("Got error: %s", rpcErr.Error())
errParts := strings.SplitN(rpcErr.Error(), ":", 2)
errCode, err = strconv.ParseInt(errParts[0], 10, 32)
//Check to see if we are requesting a height the hushd doesn't have yet
//Check to see if we are requesting a height the dragonxd doesn't have yet
if err == nil && errCode == -8 {
return nil
}
@@ -113,6 +119,45 @@ func (s *SqlStreamer) GetAddressTxids(addressBlockFilter *walletrpc.TransparentA
return nil
}
// GetMempoolStream streams currently-unconfirmed (mempool) transactions to the client as full
// RawTransactions, so lightwallets can detect 0-confirmation transactions -- including reading
// shielded memos (e.g. incoming chat messages) -- without waiting for a block. Each mempool tx is
// emitted once; the stream stays open until a new block is mined, at which point it returns
// (closing the stream) so the client re-syncs the block and reconnects. This mirrors the
// zecwallet/Hush lightwalletd semantics the wallets already expect.
//
// The node's mempool is polled by a single process-wide monitor that fans out to all subscribers
// (see mempool.go), so the node isn't polled once per connected wallet.
func (s *SqlStreamer) GetMempoolStream(_ *walletrpc.Empty, resp walletrpc.CompactTxStreamer_GetMempoolStreamServer) error {
monitor := getMempoolMonitor(s.client, s.cache, s.log)
id, ch, snapshot := monitor.subscribe()
defer monitor.unsubscribe(id)
// First send the txs already in this block's mempool, so a wallet that connects mid-block still
// sees them.
for _, rtx := range snapshot {
if err := resp.Send(rtx); err != nil {
return err
}
}
// Then stream subsequent txs until the monitor closes the channel (a new block was mined) or
// the client disconnects.
for {
select {
case <-resp.Context().Done():
return nil
case rtx, ok := <-ch:
if !ok {
return nil
}
if err := resp.Send(rtx); err != nil {
return err
}
}
}
}
func (s *SqlStreamer) GetBlock(ctx context.Context, id *walletrpc.BlockID) (*walletrpc.CompactBlock, error) {
if id.Height == 0 && id.Hash == nil {
return nil, ErrUnspecified
@@ -136,21 +181,47 @@ func (s *SqlStreamer) GetBlock(ctx context.Context, id *walletrpc.BlockID) (*wal
}
func (s *SqlStreamer) GetBlockRange(span *walletrpc.BlockRange, resp walletrpc.CompactTxStreamer_GetBlockRangeServer) error {
s.log.WithFields(logrus.Fields{
"start": span.Start.Height,
"end": span.End.Height,
}).Info("GetBlockRange called")
blockChan := make(chan walletrpc.CompactBlock)
errChan := make(chan error)
go common.GetBlockRange(s.client, s.cache, blockChan, errChan, int(span.Start.Height), int(span.End.Height))
blockCount := 0
for {
select {
case err := <-errChan:
// this will also catch context.DeadlineExceeded from the timeout
if err != nil {
s.log.WithFields(logrus.Fields{
"start": span.Start.Height,
"end": span.End.Height,
"blocksSent": blockCount,
"error": err,
}).Error("GetBlockRange error")
} else {
s.log.WithFields(logrus.Fields{
"start": span.Start.Height,
"end": span.End.Height,
"blocksSent": blockCount,
}).Info("GetBlockRange completed")
}
return err
case cBlock := <-blockChan:
err := resp.Send(&cBlock)
if err != nil {
s.log.WithFields(logrus.Fields{
"start": span.Start.Height,
"end": span.End.Height,
"blocksSent": blockCount,
"error": err,
}).Error("GetBlockRange send error")
return err
}
blockCount++
}
}
@@ -181,7 +252,7 @@ func (s *SqlStreamer) GetTransaction(ctx context.Context, txf *walletrpc.TxFilte
s.log.Errorf("Got error: %s", rpcErr.Error())
errParts := strings.SplitN(rpcErr.Error(), ":", 2)
errCode, err = strconv.ParseInt(errParts[0], 10, 32)
//Check to see if we are requesting a height the hushd doesn't have yet
//Check to see if we are requesting a height the dragonxd doesn't have yet
if err == nil && errCode == -8 {
return nil, err
}
@@ -211,7 +282,7 @@ func (s *SqlStreamer) GetTransaction(ctx context.Context, txf *walletrpc.TxFilte
s.log.Errorf("Got error: %s", rpcErr.Error())
errParts := strings.SplitN(rpcErr.Error(), ":", 2)
errCode, err = strconv.ParseInt(errParts[0], 10, 32)
//Check to see if we are requesting a height the hushd doesn't have yet
//Check to see if we are requesting a height the dragonxd doesn't have yet
if err == nil && errCode == -8 {
return nil, err
}
@@ -222,7 +293,26 @@ func (s *SqlStreamer) GetTransaction(ctx context.Context, txf *walletrpc.TxFilte
if err != nil {
return nil, err
}
txHeight = txinfo.(map[string]interface{})["height"].(float64)
// dragonxd emits "height" only for a transaction that is in a block:
// rawtransaction.cpp puts it inside `if (!hashBlock.IsNull())`. Every
// mempool transaction therefore comes back WITHOUT the key, and an
// unchecked assertion on the missing value panics -- which, with no
// recover() anywhere in grpc-go v1.24.0 or in this daemon, kills the
// whole process and every wallet connected to it. Any client can reach
// this by broadcasting a transaction and immediately asking for it, and
// GetMempoolStream hands out unconfirmed txids by design.
//
// Report an unconfirmed transaction as tip+1, matching what
// GetMempoolStream already advertises (mempool.go:109).
txmap, ok := txinfo.(map[string]interface{})
if !ok {
return nil, errors.New("getrawtransaction: unexpected response shape")
}
if h, ok := txmap["height"].(float64); ok {
txHeight = h
} else {
txHeight = float64(s.cache.GetLatestBlock() + 1)
}
return &walletrpc.RawTransaction{Data: txBytes, Height: uint64(txHeight)}, nil
}
@@ -239,6 +329,13 @@ func (s *SqlStreamer) GetTransaction(ctx context.Context, txf *walletrpc.TxFilte
func (s *SqlStreamer) GetLightdInfo(ctx context.Context, in *walletrpc.Empty) (*walletrpc.LightdInfo, error) {
saplingHeight, blockHeight, chainName, consensusBranchId, difficulty, longestchain, notarized, err := common.GetSaplingInfo(s.client)
s.log.WithFields(logrus.Fields{
"saplingHeight": saplingHeight,
"blockHeight": blockHeight,
"chainName": chainName,
"consensusBranchId": consensusBranchId,
}).Info("GetLightdInfo called")
if err != nil {
s.log.WithFields(logrus.Fields{
"error": err,
@@ -246,16 +343,23 @@ func (s *SqlStreamer) GetLightdInfo(ctx context.Context, in *walletrpc.Empty) (*
return nil, err
}
// Report the advertised (lag-adjusted) tip so wallets consider themselves
// synced at the height they are actually served, not perpetually N behind.
advHeight := s.cache.AdvertisedLatestBlock()
if advHeight < 0 {
advHeight = blockHeight
}
// TODO these are called Error but they aren't at the moment.
// A success will return code 0 and message txhash.
return &walletrpc.LightdInfo{
Version: "0.1.1-hushlightd",
Vendor: "Silentdragonlite LightWalletD",
Version: common.VersionString,
Vendor: "DragonX LightWalletD",
TaddrSupport: true,
ChainName: chainName,
SaplingActivationHeight: uint64(saplingHeight),
ConsensusBranchId: consensusBranchId,
BlockHeight: uint64(blockHeight),
BlockHeight: uint64(advHeight),
Difficulty: uint64(difficulty),
Longestchain: uint64(longestchain),
Notarized: uint64(notarized),
@@ -285,7 +389,7 @@ func (s *SqlStreamer) GetCoinsupply(ctx context.Context, in *walletrpc.Empty) (*
}, nil
}
// SendTransaction forwards raw transaction bytes to a hushd instance over JSON-RPC
// SendTransaction forwards raw transaction bytes to a dragonxd instance over JSON-RPC
func (s *SqlStreamer) SendTransaction(ctx context.Context, rawtx *walletrpc.RawTransaction) (*walletrpc.SendResponse, error) {
// sendrawtransaction "hexstring" ( allowhighfees )
//

126
monitor_lwd.sh Executable file
View File

@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# Copyright 2024-2026 The DragonX Developers
# Released under GPLv3
#
# Monitors lightwalletd and restarts it automatically if it crashes.
# Usage: ./monitor_lwd.sh &
# or: nohup ./monitor_lwd.sh >> /tmp/lwd-monitor.log 2>&1 &
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LWD_BIN="$SCRIPT_DIR/lightwalletd"
LWD_ARGS="-bind-addr lite.dragonx.is:9069 -conf-file $HOME/.hush/DRAGONX/DRAGONX.conf -no-tls -lag-min 4 -lag-max 12 -lag-window 30 -cache-size 5000"
LOGFILE="$SCRIPT_DIR/lwd-monitor.log"
PIDFILE="/tmp/lwd-monitor.pid"
STOPPING=0
RESTART_DELAY=5 # seconds to wait before restarting after a crash
MAX_RAPID_RESTARTS=5 # max restarts within the rapid window before backing off
RAPID_WINDOW=120 # seconds — if this many restarts happen within this window, back off
BACKOFF_DELAY=60 # seconds to wait when backing off
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() {
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE"
}
cleanup() {
STOPPING=1
log "${YELLOW}Monitor shutting down...${NC}"
if [[ -n "${LWD_PID:-}" ]] && kill -0 "$LWD_PID" 2>/dev/null; then
log "Stopping lightwalletd (PID $LWD_PID)..."
kill "$LWD_PID" 2>/dev/null || true
wait "$LWD_PID" 2>/dev/null || true
fi
rm -f "$PIDFILE"
log "Monitor stopped."
exit 0
}
trap cleanup SIGINT SIGTERM
# Prevent duplicate monitors
if [[ -f "$PIDFILE" ]]; then
OLD_PID=$(cat "$PIDFILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
echo "Monitor already running (PID $OLD_PID). Exiting."
exit 1
fi
rm -f "$PIDFILE"
fi
echo $$ > "$PIDFILE"
# Check binary exists
if [[ ! -x "$LWD_BIN" ]]; then
log "${RED}ERROR: lightwalletd binary not found at $LWD_BIN${NC}"
log "Build it first with: make build"
rm -f "$PIDFILE"
exit 1
fi
# Check conf file exists
CONF_FILE="$HOME/.hush/DRAGONX/DRAGONX.conf"
if [[ ! -f "$CONF_FILE" ]]; then
log "${RED}ERROR: DRAGONX.conf not found at $CONF_FILE${NC}"
rm -f "$PIDFILE"
exit 1
fi
log "${GREEN}DragonX lightwalletd monitor started (PID $$)${NC}"
log "Binary: $LWD_BIN"
log "Args: $LWD_ARGS"
restart_times=()
LWD_PID=""
STOPPING=0
while true; do
# Start lightwalletd
log "${GREEN}Starting lightwalletd...${NC}"
$LWD_BIN $LWD_ARGS >> "$LOGFILE" 2>&1 &
LWD_PID=$!
log "lightwalletd started with PID $LWD_PID"
# Wait for it to exit
EXIT_CODE=0
wait "$LWD_PID" || EXIT_CODE=$?
LWD_PID=""
if [[ $STOPPING -eq 1 ]]; then
log "${YELLOW}lightwalletd stopped on request (code $EXIT_CODE). Not restarting.${NC}"
break
fi
log "${RED}lightwalletd crashed with exit code $EXIT_CODE${NC}"
# Track restart frequency for backoff
NOW=$(date +%s)
restart_times+=("$NOW")
# Trim old entries outside the rapid window
CUTOFF=$((NOW - RAPID_WINDOW))
filtered=()
for t in "${restart_times[@]}"; do
if (( t >= CUTOFF )); then
filtered+=("$t")
fi
done
restart_times=("${filtered[@]}")
if (( ${#restart_times[@]} >= MAX_RAPID_RESTARTS )); then
log "${YELLOW}Too many restarts (${#restart_times[@]} in ${RAPID_WINDOW}s). Backing off for ${BACKOFF_DELAY}s...${NC}"
sleep "$BACKOFF_DELAY"
restart_times=()
else
log "Restarting in ${RESTART_DELAY}s..."
sleep "$RESTART_DELAY"
fi
done
rm -f "$PIDFILE"
log "Monitor exiting."

View File

@@ -100,11 +100,11 @@ func (b *Block) GetPrevHash() []byte {
func (b *Block) ToCompact() *walletrpc.CompactBlock {
compactBlock := &walletrpc.CompactBlock{
//TODO ProtoVersion: 1,
Height: uint64(b.GetHeight()),
PrevHash: b.hdr.HashPrevBlock,
Hash: b.GetEncodableHash(),
Time: b.hdr.Time,
ProtoVersion: 1,
Height: uint64(b.GetHeight()),
PrevHash: b.hdr.HashPrevBlock,
Hash: b.GetEncodableHash(),
Time: b.hdr.Time,
}
// Only Sapling transactions have a meaningful compact encoding

View File

@@ -6,7 +6,9 @@
# you can choose either IPv4 or IPv6
# using ipv4 localhost
#./lightwalletd -bind-addr localhost:9067 -conf-file ~/.komodo/HUSH3/HUSH3.conf -no-tls
#./lightwalletd -bind-addr localhost:9067 -conf-file ~/.hush/HUSH3/HUSH3.conf -no-tls
# using ipv6 localhost
./lightwalletd -bind-addr ip6-localhost:9067 -conf-file ~/.komodo/HUSH3/HUSH3.conf -no-tls
#./lightwalletd -bind-addr ip6-localhost:9067 -conf-file ~/.hush/HUSH3/HUSH3.conf -no-tls
./lightwalletd -bind-addr lite.dragonx.is:9069 -conf-file ~/.hush/DRAGONX/DRAGONX.conf -no-tls &

6
start_server.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# set -e
./lightwalletd -bind-addr lite.dragonx.is:9069 -conf-file ~/.hush/DRAGONX/DRAGONX.conf -no-tls

45
stop.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Copyright 2024-2026 The DragonX Developers
# Released under GPLv3
#
# Stops lightwalletd and its monitor process.
set -euo pipefail
PIDFILE="/tmp/lwd-monitor.pid"
# Stop the monitor first (if running), which will also stop lightwalletd
if [[ -f "$PIDFILE" ]]; then
MON_PID=$(cat "$PIDFILE")
if kill -0 "$MON_PID" 2>/dev/null; then
echo "Stopping monitor (PID $MON_PID)..."
kill "$MON_PID"
# Wait briefly for cleanup
for i in $(seq 1 10); do
kill -0 "$MON_PID" 2>/dev/null || break
sleep 0.5
done
echo "Monitor stopped."
else
echo "Stale monitor pidfile found, removing."
rm -f "$PIDFILE"
fi
fi
# Kill any remaining lightwalletd processes
LWDPIDS=$(pgrep -f 'lightwalletd.*-conf-file' 2>/dev/null || true)
if [[ -n "$LWDPIDS" ]]; then
echo "Stopping lightwalletd (PID $LWDPIDS)..."
kill $LWDPIDS 2>/dev/null || true
sleep 1
# Force kill if still running
for pid in $LWDPIDS; do
if kill -0 "$pid" 2>/dev/null; then
echo "Force killing PID $pid..."
kill -9 "$pid" 2>/dev/null || true
fi
done
echo "lightwalletd stopped."
else
echo "No lightwalletd process found."
fi

View File

@@ -23,10 +23,10 @@ echo "+------'+------'+------'+------'+------'+------'+------'+------'+------'+-
# now to compiling...
echo ""
echo "You have go installed, so starting to compile Hush lightwalletd for you..."
echo "You have go installed, so starting to compile DragonX lightwalletd for you..."
cd `pwd`/cmd/server
go build -o lightwalletd main.go
mv lightwalletd `pwd`/../../lightwalletd
echo ""
echo "Hush lightwalletd is now compiled for you. Enjoy and reach out if you need support."
echo "DragonX lightwalletd is now compiled for you. Enjoy and reach out if you need support."
echo "For options, run ./lightwalletd --help"

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: compact_formats.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
31,
1,
'',
'compact_formats.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63ompact_formats.proto\x12\x15\x63\x61sh.z.wallet.sdk.rpc\"\xa1\x01\n\x0c\x43ompactBlock\x12\x14\n\x0cprotoVersion\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\x04\x12\x0c\n\x04hash\x18\x03 \x01(\x0c\x12\x10\n\x08prevHash\x18\x04 \x01(\x0c\x12\x0c\n\x04time\x18\x05 \x01(\r\x12\x0e\n\x06header\x18\x06 \x01(\x0c\x12-\n\x03vtx\x18\x07 \x03(\x0b\x32 .cash.z.wallet.sdk.rpc.CompactTx\"\xa1\x01\n\tCompactTx\x12\r\n\x05index\x18\x01 \x01(\x04\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\x12\x0b\n\x03\x66\x65\x65\x18\x03 \x01(\r\x12\x33\n\x06spends\x18\x04 \x03(\x0b\x32#.cash.z.wallet.sdk.rpc.CompactSpend\x12\x35\n\x07outputs\x18\x05 \x03(\x0b\x32$.cash.z.wallet.sdk.rpc.CompactOutput\"\x1a\n\x0c\x43ompactSpend\x12\n\n\x02nf\x18\x01 \x01(\x0c\"=\n\rCompactOutput\x12\x0b\n\x03\x63mu\x18\x01 \x01(\x0c\x12\x0b\n\x03\x65pk\x18\x02 \x01(\x0c\x12\x12\n\nciphertext\x18\x03 \x01(\x0c\x42\x0bZ\twalletrpcb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'compact_formats_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
_globals['DESCRIPTOR']._loaded_options = None
_globals['DESCRIPTOR']._serialized_options = b'Z\twalletrpc'
_globals['_COMPACTBLOCK']._serialized_start=49
_globals['_COMPACTBLOCK']._serialized_end=210
_globals['_COMPACTTX']._serialized_start=213
_globals['_COMPACTTX']._serialized_end=374
_globals['_COMPACTSPEND']._serialized_start=376
_globals['_COMPACTSPEND']._serialized_end=402
_globals['_COMPACTOUTPUT']._serialized_start=404
_globals['_COMPACTOUTPUT']._serialized_end=465
# @@protoc_insertion_point(module_scope)

View File

@@ -0,0 +1,24 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
GRPC_GENERATED_VERSION = '1.78.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in compact_formats_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)

View File

@@ -694,6 +694,8 @@ type CompactTxStreamerClient interface {
SendTransaction(ctx context.Context, in *RawTransaction, opts ...grpc.CallOption) (*SendResponse, error)
// t-Address support
GetAddressTxids(ctx context.Context, in *TransparentAddressBlockFilter, opts ...grpc.CallOption) (CompactTxStreamer_GetAddressTxidsClient, error)
// Mempool
GetMempoolStream(ctx context.Context, in *Empty, opts ...grpc.CallOption) (CompactTxStreamer_GetMempoolStreamClient, error)
// Misc
GetLightdInfo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*LightdInfo, error)
GetCoinsupply(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Coinsupply, error)
@@ -807,6 +809,38 @@ func (x *compactTxStreamerGetAddressTxidsClient) Recv() (*RawTransaction, error)
return m, nil
}
func (c *compactTxStreamerClient) GetMempoolStream(ctx context.Context, in *Empty, opts ...grpc.CallOption) (CompactTxStreamer_GetMempoolStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &_CompactTxStreamer_serviceDesc.Streams[2], "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolStream", opts...)
if err != nil {
return nil, err
}
x := &compactTxStreamerGetMempoolStreamClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type CompactTxStreamer_GetMempoolStreamClient interface {
Recv() (*RawTransaction, error)
grpc.ClientStream
}
type compactTxStreamerGetMempoolStreamClient struct {
grpc.ClientStream
}
func (x *compactTxStreamerGetMempoolStreamClient) Recv() (*RawTransaction, error) {
m := new(RawTransaction)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *compactTxStreamerClient) GetLightdInfo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*LightdInfo, error) {
out := new(LightdInfo)
err := c.cc.Invoke(ctx, "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo", in, out, opts...)
@@ -835,6 +869,8 @@ type CompactTxStreamerServer interface {
SendTransaction(context.Context, *RawTransaction) (*SendResponse, error)
// t-Address support
GetAddressTxids(*TransparentAddressBlockFilter, CompactTxStreamer_GetAddressTxidsServer) error
// Mempool
GetMempoolStream(*Empty, CompactTxStreamer_GetMempoolStreamServer) error
// Misc
GetLightdInfo(context.Context, *Empty) (*LightdInfo, error)
GetCoinsupply(context.Context, *Empty) (*Coinsupply, error)
@@ -862,6 +898,9 @@ func (*UnimplementedCompactTxStreamerServer) SendTransaction(ctx context.Context
func (*UnimplementedCompactTxStreamerServer) GetAddressTxids(req *TransparentAddressBlockFilter, srv CompactTxStreamer_GetAddressTxidsServer) error {
return status.Errorf(codes.Unimplemented, "method GetAddressTxids not implemented")
}
func (*UnimplementedCompactTxStreamerServer) GetMempoolStream(req *Empty, srv CompactTxStreamer_GetMempoolStreamServer) error {
return status.Errorf(codes.Unimplemented, "method GetMempoolStream not implemented")
}
func (*UnimplementedCompactTxStreamerServer) GetLightdInfo(ctx context.Context, req *Empty) (*LightdInfo, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetLightdInfo not implemented")
}
@@ -987,6 +1026,27 @@ func (x *compactTxStreamerGetAddressTxidsServer) Send(m *RawTransaction) error {
return x.ServerStream.SendMsg(m)
}
func _CompactTxStreamer_GetMempoolStream_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(CompactTxStreamerServer).GetMempoolStream(m, &compactTxStreamerGetMempoolStreamServer{stream})
}
type CompactTxStreamer_GetMempoolStreamServer interface {
Send(*RawTransaction) error
grpc.ServerStream
}
type compactTxStreamerGetMempoolStreamServer struct {
grpc.ServerStream
}
func (x *compactTxStreamerGetMempoolStreamServer) Send(m *RawTransaction) error {
return x.ServerStream.SendMsg(m)
}
func _CompactTxStreamer_GetLightdInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
@@ -1063,6 +1123,11 @@ var _CompactTxStreamer_serviceDesc = grpc.ServiceDesc{
Handler: _CompactTxStreamer_GetAddressTxids_Handler,
ServerStreams: true,
},
{
StreamName: "GetMempoolStream",
Handler: _CompactTxStreamer_GetMempoolStream_Handler,
ServerStreams: true,
},
},
Metadata: "service.proto",
}

View File

@@ -87,6 +87,13 @@ service CompactTxStreamer {
// t-Address support
rpc GetAddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
// Mempool
// Return a stream of current mempool transactions as full RawTransactions. The stream stays
// open while there are mempool transactions and is closed when a new block is mined, at which
// point the client re-syncs the block and reconnects. Full RawTransactions (not CompactTx) are
// required so wallets can read shielded memos of 0-confirmation transactions (e.g. chat).
rpc GetMempoolStream(Empty) returns (stream RawTransaction) {}
// Misc
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
rpc GetCoinsupply(Empty) returns (Coinsupply) {}

60
walletrpc/service_pb2.py Normal file
View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: service.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
31,
1,
'',
'service.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
import compact_formats_pb2 as compact__formats__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rservice.proto\x12\x15\x63\x61sh.z.wallet.sdk.rpc\x1a\x15\x63ompact_formats.proto\"\'\n\x07\x42lockID\x12\x0e\n\x06height\x18\x01 \x01(\x04\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\"h\n\nBlockRange\x12-\n\x05start\x18\x01 \x01(\x0b\x32\x1e.cash.z.wallet.sdk.rpc.BlockID\x12+\n\x03\x65nd\x18\x02 \x01(\x0b\x32\x1e.cash.z.wallet.sdk.rpc.BlockID\"V\n\x08TxFilter\x12-\n\x05\x62lock\x18\x01 \x01(\x0b\x32\x1e.cash.z.wallet.sdk.rpc.BlockID\x12\r\n\x05index\x18\x02 \x01(\x04\x12\x0c\n\x04hash\x18\x03 \x01(\x0c\".\n\x0eRawTransaction\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0e\n\x06height\x18\x02 \x01(\x04\"7\n\x0cSendResponse\x12\x11\n\terrorCode\x18\x01 \x01(\x05\x12\x14\n\x0c\x65rrorMessage\x18\x02 \x01(\t\"\x0b\n\tChainSpec\"\x07\n\x05\x45mpty\"\xe4\x01\n\nLightdInfo\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x0e\n\x06vendor\x18\x02 \x01(\t\x12\x14\n\x0ctaddrSupport\x18\x03 \x01(\x08\x12\x11\n\tchainName\x18\x04 \x01(\t\x12\x1f\n\x17saplingActivationHeight\x18\x05 \x01(\x04\x12\x19\n\x11\x63onsensusBranchId\x18\x06 \x01(\t\x12\x13\n\x0b\x62lockHeight\x18\x07 \x01(\x04\x12\x12\n\ndifficulty\x18\x08 \x01(\x04\x12\x14\n\x0clongestchain\x18\t \x01(\x04\x12\x11\n\tnotarized\x18\n \x01(\x04\"i\n\nCoinsupply\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\x0c\n\x04\x63oin\x18\x02 \x01(\t\x12\x0e\n\x06height\x18\x03 \x01(\x04\x12\x0e\n\x06supply\x18\x04 \x01(\x04\x12\x0e\n\x06zfunds\x18\x05 \x01(\x04\x12\r\n\x05total\x18\x06 \x01(\x04\"%\n\x12TransparentAddress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\"b\n\x1dTransparentAddressBlockFilter\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x30\n\x05range\x18\x02 \x01(\x0b\x32!.cash.z.wallet.sdk.rpc.BlockRange2\xf2\x05\n\x11\x43ompactTxStreamer\x12T\n\x0eGetLatestBlock\x12 .cash.z.wallet.sdk.rpc.ChainSpec\x1a\x1e.cash.z.wallet.sdk.rpc.BlockID\"\x00\x12Q\n\x08GetBlock\x12\x1e.cash.z.wallet.sdk.rpc.BlockID\x1a#.cash.z.wallet.sdk.rpc.CompactBlock\"\x00\x12[\n\rGetBlockRange\x12!.cash.z.wallet.sdk.rpc.BlockRange\x1a#.cash.z.wallet.sdk.rpc.CompactBlock\"\x00\x30\x01\x12Z\n\x0eGetTransaction\x12\x1f.cash.z.wallet.sdk.rpc.TxFilter\x1a%.cash.z.wallet.sdk.rpc.RawTransaction\"\x00\x12_\n\x0fSendTransaction\x12%.cash.z.wallet.sdk.rpc.RawTransaction\x1a#.cash.z.wallet.sdk.rpc.SendResponse\"\x00\x12r\n\x0fGetAddressTxids\x12\x34.cash.z.wallet.sdk.rpc.TransparentAddressBlockFilter\x1a%.cash.z.wallet.sdk.rpc.RawTransaction\"\x00\x30\x01\x12R\n\rGetLightdInfo\x12\x1c.cash.z.wallet.sdk.rpc.Empty\x1a!.cash.z.wallet.sdk.rpc.LightdInfo\"\x00\x12R\n\rGetCoinsupply\x12\x1c.cash.z.wallet.sdk.rpc.Empty\x1a!.cash.z.wallet.sdk.rpc.Coinsupply\"\x00\x42\x0bZ\twalletrpcb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'service_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
_globals['DESCRIPTOR']._loaded_options = None
_globals['DESCRIPTOR']._serialized_options = b'Z\twalletrpc'
_globals['_BLOCKID']._serialized_start=63
_globals['_BLOCKID']._serialized_end=102
_globals['_BLOCKRANGE']._serialized_start=104
_globals['_BLOCKRANGE']._serialized_end=208
_globals['_TXFILTER']._serialized_start=210
_globals['_TXFILTER']._serialized_end=296
_globals['_RAWTRANSACTION']._serialized_start=298
_globals['_RAWTRANSACTION']._serialized_end=344
_globals['_SENDRESPONSE']._serialized_start=346
_globals['_SENDRESPONSE']._serialized_end=401
_globals['_CHAINSPEC']._serialized_start=403
_globals['_CHAINSPEC']._serialized_end=414
_globals['_EMPTY']._serialized_start=416
_globals['_EMPTY']._serialized_end=423
_globals['_LIGHTDINFO']._serialized_start=426
_globals['_LIGHTDINFO']._serialized_end=654
_globals['_COINSUPPLY']._serialized_start=656
_globals['_COINSUPPLY']._serialized_end=761
_globals['_TRANSPARENTADDRESS']._serialized_start=763
_globals['_TRANSPARENTADDRESS']._serialized_end=800
_globals['_TRANSPARENTADDRESSBLOCKFILTER']._serialized_start=802
_globals['_TRANSPARENTADDRESSBLOCKFILTER']._serialized_end=900
_globals['_COMPACTTXSTREAMER']._serialized_start=903
_globals['_COMPACTTXSTREAMER']._serialized_end=1657
# @@protoc_insertion_point(module_scope)

View File

@@ -0,0 +1,403 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
import compact_formats_pb2 as compact__formats__pb2
import service_pb2 as service__pb2
GRPC_GENERATED_VERSION = '1.78.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in service_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)
class CompactTxStreamerStub(object):
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.GetLatestBlock = channel.unary_unary(
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock',
request_serializer=service__pb2.ChainSpec.SerializeToString,
response_deserializer=service__pb2.BlockID.FromString,
_registered_method=True)
self.GetBlock = channel.unary_unary(
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock',
request_serializer=service__pb2.BlockID.SerializeToString,
response_deserializer=compact__formats__pb2.CompactBlock.FromString,
_registered_method=True)
self.GetBlockRange = channel.unary_stream(
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRange',
request_serializer=service__pb2.BlockRange.SerializeToString,
response_deserializer=compact__formats__pb2.CompactBlock.FromString,
_registered_method=True)
self.GetTransaction = channel.unary_unary(
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTransaction',
request_serializer=service__pb2.TxFilter.SerializeToString,
response_deserializer=service__pb2.RawTransaction.FromString,
_registered_method=True)
self.SendTransaction = channel.unary_unary(
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction',
request_serializer=service__pb2.RawTransaction.SerializeToString,
response_deserializer=service__pb2.SendResponse.FromString,
_registered_method=True)
self.GetAddressTxids = channel.unary_stream(
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressTxids',
request_serializer=service__pb2.TransparentAddressBlockFilter.SerializeToString,
response_deserializer=service__pb2.RawTransaction.FromString,
_registered_method=True)
self.GetLightdInfo = channel.unary_unary(
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo',
request_serializer=service__pb2.Empty.SerializeToString,
response_deserializer=service__pb2.LightdInfo.FromString,
_registered_method=True)
self.GetCoinsupply = channel.unary_unary(
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetCoinsupply',
request_serializer=service__pb2.Empty.SerializeToString,
response_deserializer=service__pb2.Coinsupply.FromString,
_registered_method=True)
class CompactTxStreamerServicer(object):
"""Missing associated documentation comment in .proto file."""
def GetLatestBlock(self, request, context):
"""Compact Blocks
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetBlock(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetBlockRange(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetTransaction(self, request, context):
"""Transactions
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def SendTransaction(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetAddressTxids(self, request, context):
"""t-Address support
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetLightdInfo(self, request, context):
"""Misc
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetCoinsupply(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_CompactTxStreamerServicer_to_server(servicer, server):
rpc_method_handlers = {
'GetLatestBlock': grpc.unary_unary_rpc_method_handler(
servicer.GetLatestBlock,
request_deserializer=service__pb2.ChainSpec.FromString,
response_serializer=service__pb2.BlockID.SerializeToString,
),
'GetBlock': grpc.unary_unary_rpc_method_handler(
servicer.GetBlock,
request_deserializer=service__pb2.BlockID.FromString,
response_serializer=compact__formats__pb2.CompactBlock.SerializeToString,
),
'GetBlockRange': grpc.unary_stream_rpc_method_handler(
servicer.GetBlockRange,
request_deserializer=service__pb2.BlockRange.FromString,
response_serializer=compact__formats__pb2.CompactBlock.SerializeToString,
),
'GetTransaction': grpc.unary_unary_rpc_method_handler(
servicer.GetTransaction,
request_deserializer=service__pb2.TxFilter.FromString,
response_serializer=service__pb2.RawTransaction.SerializeToString,
),
'SendTransaction': grpc.unary_unary_rpc_method_handler(
servicer.SendTransaction,
request_deserializer=service__pb2.RawTransaction.FromString,
response_serializer=service__pb2.SendResponse.SerializeToString,
),
'GetAddressTxids': grpc.unary_stream_rpc_method_handler(
servicer.GetAddressTxids,
request_deserializer=service__pb2.TransparentAddressBlockFilter.FromString,
response_serializer=service__pb2.RawTransaction.SerializeToString,
),
'GetLightdInfo': grpc.unary_unary_rpc_method_handler(
servicer.GetLightdInfo,
request_deserializer=service__pb2.Empty.FromString,
response_serializer=service__pb2.LightdInfo.SerializeToString,
),
'GetCoinsupply': grpc.unary_unary_rpc_method_handler(
servicer.GetCoinsupply,
request_deserializer=service__pb2.Empty.FromString,
response_serializer=service__pb2.Coinsupply.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'cash.z.wallet.sdk.rpc.CompactTxStreamer', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('cash.z.wallet.sdk.rpc.CompactTxStreamer', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
class CompactTxStreamer(object):
"""Missing associated documentation comment in .proto file."""
@staticmethod
def GetLatestBlock(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock',
service__pb2.ChainSpec.SerializeToString,
service__pb2.BlockID.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetBlock(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock',
service__pb2.BlockID.SerializeToString,
compact__formats__pb2.CompactBlock.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetBlockRange(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(
request,
target,
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRange',
service__pb2.BlockRange.SerializeToString,
compact__formats__pb2.CompactBlock.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetTransaction(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTransaction',
service__pb2.TxFilter.SerializeToString,
service__pb2.RawTransaction.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def SendTransaction(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction',
service__pb2.RawTransaction.SerializeToString,
service__pb2.SendResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetAddressTxids(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(
request,
target,
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressTxids',
service__pb2.TransparentAddressBlockFilter.SerializeToString,
service__pb2.RawTransaction.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetLightdInfo(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo',
service__pb2.Empty.SerializeToString,
service__pb2.LightdInfo.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetCoinsupply(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetCoinsupply',
service__pb2.Empty.SerializeToString,
service__pb2.Coinsupply.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)

177
zrpc/client.go Normal file
View File

@@ -0,0 +1,177 @@
// Package zrpc is a minimal JSON-RPC client for talking to dragonxd.
//
// It replaces github.com/btcsuite/btcd/rpcclient, of which lightwalletd used
// exactly one method: RawRequest. That package is unusable here for two
// reasons, both of which this package exists to fix:
//
// 1. NO TIMEOUT, AND NO WAY TO SET ONE. rpcclient builds its http.Client in an
// unexported newHTTPClient() and its ConnConfig exposes no Timeout field, so
// a request can hang forever. Calls were observed hanging for over five
// minutes against a healthy node that answered the same query from the CLI
// in 2ms.
//
// 2. EVERY CALL IN THE PROCESS IS SERIALISED. In HTTP POST mode rpcclient runs
// a single sendPostHandler goroutine which invokes handleSendPostMessage
// SYNCHRONOUSLY, so at most one RPC is ever in flight. Combined with (1),
// one stuck call blocks the block ingestor, the mempool monitor and every
// user-facing gRPC handler indefinitely. A timeout alone would not fix this:
// bounding the caller's wait still leaves the shared goroutine stuck on
// http.Client.Do, so everything queued behind it stays blocked. Only a
// timeout on the HTTP client itself aborts the in-flight request, and only
// dropping the shared goroutine lets independent callers proceed.
//
// The wire format, request envelope, ID sequence and error semantics are
// deliberately byte-identical to rpcclient's RawRequest. In particular
// RPCError.Error() must render as "<code>: <message>", because callers parse
// the code back out of the string (see common.GetSaplingInfo, which checks for
// code -8 via strings.SplitN(err.Error(), ":", 2)).
package zrpc
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"sync/atomic"
"time"
)
// RPCError is a JSON-RPC error object returned by dragonxd.
type RPCError struct {
Code int64 `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
// Error renders as "<code>: <message>". Callers depend on this exact shape to
// recover the numeric code; do not change it.
func (e *RPCError) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}
type request struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
ID int64 `json:"id"`
}
type rawResponse struct {
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
}
// Client is safe for concurrent use by multiple goroutines.
type Client struct {
url string
user string
pass string
http *http.Client
nextID int64
}
// New returns a client for a dragonxd JSON-RPC endpoint.
//
// timeout of 0 means no timeout, which reproduces the old unbounded behaviour
// and should not be used in production.
//
// maxConcurrent bounds how many requests may be in flight at once. This is not
// optional book-keeping: rpcclient's single goroutine imposed an accidental
// ceiling of ONE, and removing it without putting anything in its place would
// let a burst of gRPC handlers fan out arbitrarily wide. grpc-go places no
// limit of its own here -- this server sets no MaxConcurrentStreams, so the
// default is math.MaxUint32. dragonxd serves RPC with 8 worker threads
// (DEFAULT_HTTP_THREADS) behind a 4096-deep work queue, and on the pool node
// those threads are shared with getblocktemplate, so overload shows up as
// queueing latency for mining rather than as an error we could back off on.
// A small number still removes all of the head-of-line blocking.
func New(addr, user, pass string, timeout time.Duration, maxConcurrent int) *Client {
if maxConcurrent < 1 {
maxConcurrent = 1
}
return &Client{
url: "http://" + addr,
user: user,
pass: pass,
http: &http.Client{
// Covers the whole exchange: connect, write, response headers and
// body read. This is the bound that was missing.
Timeout: timeout,
Transport: &http.Transport{
// dragonxd's HTTP server supports keep-alive. rpcclient set
// Close=true and opened a fresh TCP connection per request,
// which left hundreds of sockets in TIME_WAIT on a busy node.
//
// MaxConnsPerHost is the real concurrency bound: it BLOCKS a
// caller once the limit is reached rather than dialling more,
// which is the backpressure we want. MaxIdleConnsPerHost only
// caps reuse, so on its own it would let us exceed the limit
// and go back to churning connections.
MaxConnsPerHost: maxConcurrent,
MaxIdleConns: maxConcurrent,
MaxIdleConnsPerHost: maxConcurrent,
// Must stay BELOW dragonxd's own idle timeout, which is 30s
// (DEFAULT_HTTP_SERVER_TIMEOUT in httpserver.h, applied via
// evhttp_set_timeout and not overridden in DRAGONX.conf).
// Whoever closes second loses a race against a FIN already in
// flight, and Go will not retry a POST once bytes are on the
// wire -- so we close first.
IdleConnTimeout: 20 * time.Second,
},
},
}
}
// RawRequest sends a JSON-RPC request and returns the raw result. A JSON-RPC
// error from the server is returned as *RPCError.
func (c *Client) RawRequest(method string, params []json.RawMessage) (json.RawMessage, error) {
if method == "" {
return nil, errors.New("no method")
}
// Marshal parameters as "[]" instead of "null" when none are passed.
if params == nil {
params = []json.RawMessage{}
}
body, err := json.Marshal(&request{
Jsonrpc: "1.0",
Method: method,
Params: params,
ID: atomic.AddInt64(&c.nextID, 1),
})
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", c.url, bytes.NewReader(body))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.SetBasicAuth(c.user, c.pass)
httpResp, err := c.http.Do(httpReq)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
respBytes, err := ioutil.ReadAll(httpResp.Body)
if err != nil {
return nil, fmt.Errorf("error reading json reply: %v", err)
}
var resp rawResponse
if err := json.Unmarshal(respBytes, &resp); err != nil {
// Not a valid JSON-RPC response: surface the status and raw body, the
// same way rpcclient did. dragonxd returns non-JSON bodies for some
// auth and workqueue failures, and callers log this verbatim.
return nil, fmt.Errorf("status code: %d, response: %q",
httpResp.StatusCode, string(respBytes))
}
if resp.Error != nil {
return nil, resp.Error
}
return resp.Result, nil
}

86
zrpc/live_test.go Normal file
View File

@@ -0,0 +1,86 @@
package zrpc
import (
"encoding/json"
"os"
"strconv"
"strings"
"testing"
"time"
ini "gopkg.in/ini.v1"
)
// Live tests against a local dragonxd. Skipped unless ZRPC_CONF points at a
// DRAGONX.conf, so `go test ./...` stays hermetic.
func liveClient(t *testing.T, timeout time.Duration) *Client {
t.Helper()
conf := os.Getenv("ZRPC_CONF")
if conf == "" {
t.Skip("ZRPC_CONF not set; skipping live RPC test")
}
cfg, err := ini.Load(conf)
if err != nil {
t.Fatalf("load conf: %v", err)
}
k := func(n string) string { return cfg.Section("").Key(n).String() }
return New(k("rpcbind")+":"+k("rpcport"), k("rpcuser"), k("rpcpassword"), timeout, 8)
}
func TestLiveSuccess(t *testing.T) {
c := liveClient(t, 30*time.Second)
res, err := c.RawRequest("getblockchaininfo", nil)
if err != nil {
t.Fatalf("getblockchaininfo: %v", err)
}
var f map[string]interface{}
if err := json.Unmarshal(res, &f); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if f["chain"] != "main" {
t.Fatalf("chain = %v, want main", f["chain"])
}
t.Logf("ok: chain=%v blocks=%v", f["chain"], f["blocks"])
}
// The error string must stay "<code>: <message>" -- common.GetSaplingInfo
// recovers the numeric code with strings.SplitN(err.Error(), ":", 2).
func TestLiveErrorStringShape(t *testing.T) {
c := liveClient(t, 30*time.Second)
p := []json.RawMessage{json.RawMessage(`"99999999"`)}
_, err := c.RawRequest("getblock", p)
if err == nil {
t.Fatal("expected an error for an out-of-range height")
}
parts := strings.SplitN(err.Error(), ":", 2)
code, perr := strconv.ParseInt(parts[0], 10, 32)
if perr != nil {
t.Fatalf("error string %q does not start with a numeric code", err.Error())
}
if code != -8 {
t.Logf("note: code %d (expected -8 for a bad height, but any numeric code proves the shape)", code)
}
t.Logf("ok: %q -> code %d", err.Error(), code)
}
// A timeout must actually abort the call rather than hanging.
func TestLiveTimeoutFires(t *testing.T) {
c := liveClient(t, 1*time.Nanosecond)
start := time.Now()
_, err := c.RawRequest("getblockchaininfo", nil)
elapsed := time.Since(start)
if err == nil {
t.Fatal("expected a timeout error")
}
if elapsed > 5*time.Second {
t.Fatalf("timeout did not fire promptly: %v", elapsed)
}
t.Logf("ok: timed out in %v with %v", elapsed, err)
}
func TestNoMethod(t *testing.T) {
c := New("127.0.0.1:1", "u", "p", time.Second, 8)
if _, err := c.RawRequest("", nil); err == nil || err.Error() != "no method" {
t.Fatalf(`RawRequest("") = %v, want "no method"`, err)
}
}