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
This commit is contained in:
DragonX Developers
2026-08-26 02:11:46 -05:00
committed by DanS
parent 2bab58c6d2
commit 62358df198
2 changed files with 37 additions and 11 deletions

View File

@@ -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

View File

@@ -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
}