Compare commits
4 Commits
2bab58c6d2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b5a5e5a67 | ||
|
|
17ca2b0e69 | ||
|
|
7f5474ef82 | ||
|
|
62358df198 |
@@ -93,7 +93,7 @@ type Options struct {
|
||||
}
|
||||
|
||||
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:9069", "the address to listen on")
|
||||
|
||||
@@ -38,25 +38,32 @@ 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)
|
||||
|
||||
// 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)
|
||||
upgradeJSON, ok := f.(map[string]interface{})["upgrades"]
|
||||
if ok {
|
||||
if upgradesMap, ok := upgradeJSON.(map[string]interface{}); ok {
|
||||
if saplingJSON, ok := upgradesMap["76b809bb"]; ok {
|
||||
saplingHeight = saplingJSON.(map[string]interface{})["activationheight"].(float64)
|
||||
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)
|
||||
|
||||
// DragonX always uses Sapling consensus rules but CurrentEpochBranchId()
|
||||
// returns Sprout (0) for full nodes because the activation heights are
|
||||
|
||||
14
common/version.go
Normal file
14
common/version.go
Normal 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.2"
|
||||
|
||||
// VersionString is what GetLightdInfo advertises to clients.
|
||||
const VersionString = Version + "-dragonxlightd"
|
||||
40
deploy/README.md
Normal file
40
deploy/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# deploy/
|
||||
|
||||
Operational scripts, versioned here but **executed from outside this repository**.
|
||||
|
||||
A node's runtime directory (`/home/dev/lightwalletd`) holds only the binary and
|
||||
its logs. The supervisor scripts live in `/home/dev/`:
|
||||
|
||||
| repo (source of truth) | deployed to | invoked by |
|
||||
|------------------------|--------------------|---------------------------|
|
||||
| `deploy/monitor_lwd.sh`| `/home/dev/monitor_lwd.sh` | `/home/dev/lwd_watchdog.sh` (cron, primary) |
|
||||
|
||||
## Why they are not run from here
|
||||
|
||||
This repository's working tree used to *be* the runtime directory, and
|
||||
`monitor_lwd.sh` — the primary's live supervisor — was a tracked file inside it.
|
||||
On 2026-08-26 a routine `git reset --hard` during an unrelated cherry-pick
|
||||
reverted it to an older committed revision that (a) dropped `-cache-size 5000`,
|
||||
making every relaunch warm the block cache from tip-400000, and (b) reintroduced
|
||||
`wait "$LWD_PID" || true; EXIT_CODE=$?`, which reads the exit 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 — the bug behind an 11h48m
|
||||
outage on 2026-08-21.
|
||||
|
||||
Nothing noticed at the time because the running monitor was executing an
|
||||
already-deleted inode: the working copy was broken while the live process was
|
||||
fine. It was recovered from `/proc/<pid>/fd/255`.
|
||||
|
||||
Deploying these from outside the working tree means no checkout, reset, rebase or
|
||||
branch switch can reach a running supervisor.
|
||||
|
||||
## Changing one
|
||||
|
||||
Edit it here, commit, then copy to the node and let the next relaunch pick it up:
|
||||
|
||||
cp deploy/monitor_lwd.sh /home/dev/monitor_lwd.sh.stage
|
||||
chmod 755 /home/dev/monitor_lwd.sh.stage
|
||||
mv -f /home/dev/monitor_lwd.sh.stage /home/dev/monitor_lwd.sh
|
||||
|
||||
`mv`, not `cp`: a rename cannot disturb a running process, and the currently
|
||||
running monitor keeps its own inode until it next restarts.
|
||||
@@ -8,11 +8,19 @@
|
||||
|
||||
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"
|
||||
LOGFILE="$SCRIPT_DIR/lwd-monitor.log"
|
||||
# RUNTIME DIR IS EXPLICIT, not derived from this script's own location.
|
||||
# This script used to live inside /home/dev/lightwalletd, which is a git
|
||||
# working tree as well as the runtime directory -- so a routine `git reset
|
||||
# --hard` or branch checkout there silently reverted the live supervisor to an
|
||||
# older committed version. That happened on 2026-08-26; the running monitor
|
||||
# survived only because it was executing an already-deleted inode. The script
|
||||
# now lives outside the repo and names the runtime dir directly.
|
||||
LWD_DIR="${LWD_DIR:-/home/dev/lightwalletd}"
|
||||
LWD_BIN="$LWD_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="$LWD_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
|
||||
@@ -29,6 +37,7 @@ log() {
|
||||
}
|
||||
|
||||
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)..."
|
||||
@@ -75,6 +84,7 @@ log "Args: $LWD_ARGS"
|
||||
|
||||
restart_times=()
|
||||
LWD_PID=""
|
||||
STOPPING=0
|
||||
|
||||
while true; do
|
||||
# Start lightwalletd
|
||||
@@ -84,12 +94,12 @@ while true; do
|
||||
log "lightwalletd started with PID $LWD_PID"
|
||||
|
||||
# Wait for it to exit
|
||||
wait "$LWD_PID" || true
|
||||
EXIT_CODE=$?
|
||||
EXIT_CODE=0
|
||||
wait "$LWD_PID" || EXIT_CODE=$?
|
||||
LWD_PID=""
|
||||
|
||||
if [[ $EXIT_CODE -eq 0 ]]; then
|
||||
log "${YELLOW}lightwalletd exited cleanly (code 0). Not restarting.${NC}"
|
||||
if [[ $STOPPING -eq 1 ]]; then
|
||||
log "${YELLOW}lightwalletd stopped on request (code $EXIT_CODE). Not restarting.${NC}"
|
||||
break
|
||||
fi
|
||||
|
||||
@@ -293,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
|
||||
}
|
||||
@@ -334,7 +353,7 @@ func (s *SqlStreamer) GetLightdInfo(ctx context.Context, in *walletrpc.Empty) (*
|
||||
// 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-dragonxlightd",
|
||||
Version: common.VersionString,
|
||||
Vendor: "DragonX LightWalletD",
|
||||
TaddrSupport: true,
|
||||
ChainName: chainName,
|
||||
|
||||
Reference in New Issue
Block a user