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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 39 additions & 14 deletions api_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1468,23 +1468,24 @@ func (s *APIServer) handleLoadWallet(w http.ResponseWriter, r *http.Request) {

// Handle chain-ahead-of-wallet (chain was reset while wallet was offline)
chainHeight := s.daemon.Chain().Height()
walletHeight := wl.SyncedHeight()
if walletHeight > chainHeight {
if removed := wl.RewindToHeight(chainHeight); removed > 0 {
originalWalletHeight := wl.SyncedHeight()
if removed := rewindWalletToCanonicalTip(wl, s.daemon.Chain()); removed > 0 {
if err := wl.Save(); err != nil {
writeInternal(w, r, http.StatusInternalServerError, "internal error", err)
return
}
}

walletHeight, walletHash := wl.SyncedBlock()
if originalWalletHeight == chainHeight && walletHeight == chainHeight && chainHeight > 0 && !walletSyncHashKnown(walletHash) {
// Legacy wallets do not know which block hash they scanned at the tip.
// Rewind one block so the next scan records canonical hash metadata.
if removed := wl.RewindToHeight(chainHeight - 1); removed > 0 {
if err := wl.Save(); err != nil {
writeInternal(w, r, http.StatusInternalServerError, "internal error", err)
return
}
}
walletHeight = wl.SyncedHeight()
}

// Conservative reorg recovery: when wallet and chain heights match exactly,
// rewind one block and rescan tip. This clears stale same-height branch data
// even for wallets that predate tip-hash sync metadata.
if walletHeight == chainHeight && chainHeight > 0 {
wl.RewindToHeight(chainHeight - 1)
walletHeight = wl.SyncedHeight()
}

wl.SetInputFilter(func(out *wallet.OwnedOutput) bool {
Expand Down Expand Up @@ -1589,7 +1590,11 @@ func (s *APIServer) handleCreateWallet(w http.ResponseWriter, r *http.Request) {

chainHeight := s.daemon.Chain().Height()
if chainHeight > 0 {
wl.SetSyncedHeight(chainHeight)
if block := s.daemon.Chain().GetBlockByHeight(chainHeight); block != nil {
wl.SetSyncedBlock(chainHeight, block.Hash())
} else {
wl.SetSyncedHeight(chainHeight)
}
if err := wl.Save(); err != nil {
writeInternal(w, r, http.StatusInternalServerError, "internal error", err)
return
Expand Down Expand Up @@ -2347,6 +2352,12 @@ func (s *APIServer) catchUpScan() {

ctx := s.cli.ctx

if removed := rewindWalletToCanonicalTip(w, s.daemon.Chain()); removed > 0 {
if err := w.Save(); err != nil {
log.Printf("Warning: catchUpScan reorg save: %v", err)
}
}

walletHeight := w.SyncedHeight()
chainHeight := s.daemon.Chain().Height()
if walletHeight >= chainHeight {
Expand All @@ -2364,11 +2375,25 @@ func (s *APIServer) catchUpScan() {
if block == nil {
break
}
_, walletHash := w.SyncedBlock()
if walletSyncHashKnown(walletHash) && block.Header.PrevHash != walletHash {
if h <= 1 {
break
}
if removed := w.RewindToHeight(h - 2); removed > 0 {
if err := w.Save(); err != nil {
log.Printf("Warning: catchUpScan reorg save at height %d: %v", h, err)
}
}
h = w.SyncedHeight()
continue
}
sc.ScanBlock(blockToScanData(block))
w.ReconcileUnconfirmedSpends(func(txID [32]byte) bool {
return s.daemon.Mempool().HasTransaction(txID)
})
w.SetSyncedHeight(h)
blockHash := block.Hash()
w.SetSyncedBlock(h, blockHash)

if h%100 == 0 || h == chainHeight {
if err := w.Save(); err != nil {
Expand Down
71 changes: 50 additions & 21 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,18 +490,19 @@ func (c *CLI) recoverWalletAfterChainReset() {
return
}
chainHeight := c.daemon.Chain().Height()
walletHeight := c.wallet.SyncedHeight()
if walletHeight > chainHeight {
removed := c.wallet.RewindToHeight(chainHeight)
if removed > 0 {
fmt.Printf(" Chain reset: removed %d orphaned outputs, rewound to height %d\n", removed, chainHeight)
if err := c.wallet.Save(); err != nil {
fmt.Printf(" Warning: failed to persist rewound wallet: %v\n", err)
}
originalWalletHeight := c.wallet.SyncedHeight()
removed := rewindWalletToCanonicalTip(c.wallet, c.daemon.Chain())
if removed > 0 {
fmt.Printf(" Chain reset: removed %d orphaned outputs, rewound to height %d\n", removed, c.wallet.SyncedHeight())
if err := c.wallet.Save(); err != nil {
fmt.Printf(" Warning: failed to persist rewound wallet: %v\n", err)
}
} else if chainHeight > 0 && walletHeight == chainHeight {
// Conservative reorg recovery: if wallet and chain are at the same height,
// rewind one block to clear potentially stale same-height fork state.
}

walletHeight, walletHash := c.wallet.SyncedBlock()
if chainHeight > 0 && originalWalletHeight == chainHeight && walletHeight == chainHeight && !walletSyncHashKnown(walletHash) {
// Legacy wallets do not know which block hash they scanned at the tip.
// Rewind one block so the next scan records canonical hash metadata.
removed := c.wallet.RewindToHeight(chainHeight - 1)
if removed > 0 {
fmt.Printf(" Chain reset: removed %d orphaned outputs, rewound to height %d\n", removed, chainHeight-1)
Expand Down Expand Up @@ -1116,20 +1117,47 @@ func (c *CLI) autoScanBlocks() {
w := c.wallet
c.mu.RUnlock()

if scanner == nil {
if scanner == nil || w == nil {
continue
}

blockData := blockToScanData(block)
scanner.ScanBlock(blockData)
w.ReconcileUnconfirmedSpends(func(txID [32]byte) bool {
return c.daemon.Mempool().HasTransaction(txID)
})
if removed := rewindWalletToCanonicalTip(w, c.daemon.Chain()); removed > 0 {
unsaved++
}

w.SetSyncedHeight(blockData.Height)
unsaved++
if unsaved >= saveBatchSize {
doSave()
for {
walletHeight, walletHash := w.SyncedBlock()
chainHeight := c.daemon.Chain().Height()
if walletHeight >= chainHeight {
break
}

nextHeight := walletHeight + 1
canonical := c.daemon.Chain().GetBlockByHeight(nextHeight)
if canonical == nil {
break
}
if walletSyncHashKnown(walletHash) && canonical.Header.PrevHash != walletHash {
if walletHeight == 0 {
break
}
if removed := w.RewindToHeight(walletHeight - 1); removed > 0 {
unsaved++
}
continue
}

blockData := blockToScanData(canonical)
scanner.ScanBlock(blockData)
w.ReconcileUnconfirmedSpends(func(txID [32]byte) bool {
return c.daemon.Mempool().HasTransaction(txID)
})

w.SetSyncedBlock(blockData.Height, blockData.Hash)
unsaved++
if unsaved >= saveBatchSize {
doSave()
}
}
}
}
Expand Down Expand Up @@ -1159,6 +1187,7 @@ func (c *CLI) watchMinedBlocks() {
func blockToScanData(block *Block) *wallet.BlockData {
data := &wallet.BlockData{
Height: block.Header.Height,
Hash: block.Hash(),
Transactions: make([]wallet.TxData, len(block.Transactions)),
}

Expand Down
10 changes: 9 additions & 1 deletion cli_cmd_wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,12 @@ func memoTextIfPrintable(b []byte) (string, bool) {
}

func (c *CLI) cmdSync() {
if removed := rewindWalletToCanonicalTip(c.wallet, c.daemon.Chain()); removed > 0 {
fmt.Printf(" Chain reset: removed %d orphaned outputs, rewound to height %d\n", removed, c.wallet.SyncedHeight())
if err := c.wallet.Save(); err != nil {
fmt.Printf(" Warning: failed to persist rewound wallet: %v\n", err)
}
}
chainHeight := c.daemon.Chain().Height()
walletHeight := c.wallet.SyncedHeight()

Expand All @@ -729,6 +735,7 @@ func (c *CLI) cmdSync() {
blocks := c.daemon.Chain().GetBlocksByHeightRange(walletHeight+1, chainHeight)

scannedTo := walletHeight
var scannedHash [32]byte
for _, block := range blocks {
if block == nil {
break
Expand All @@ -739,13 +746,14 @@ func (c *CLI) cmdSync() {

h := block.Header.Height
scannedTo = h
scannedHash = blockData.Hash
if found > 0 || spent > 0 {
fmt.Printf(" Block %d: +%d outputs, %d spent\n", h, found, spent)
}
}

if scannedTo > walletHeight {
c.wallet.SetSyncedHeight(scannedTo)
c.wallet.SetSyncedBlock(scannedTo, scannedHash)
fmt.Printf(" Wallet synced to height %d\n", scannedTo)
}
}
Expand Down
8 changes: 6 additions & 2 deletions daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -1250,8 +1250,12 @@ func (d *Daemon) SubmitBlock(block *Block) error {
}
d.syncMgr.BroadcastBlock(blockData)

// Notify subscribers
d.notifyBlock(block)
// Notify wallet/UI subscribers only for blocks that became main-chain.
// Side-chain blocks may be accepted for fork choice, but scanning them would
// record outputs the wallet cannot spend on the canonical chain.
if isMainChain {
d.notifyBlock(block)
}
d.notifyMinedBlock(block)

return nil
Expand Down
10 changes: 6 additions & 4 deletions wallet/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
// BlockData is the minimal block info needed for scanning
type BlockData struct {
Height uint64
Hash [32]byte
Transactions []TxData
}

Expand Down Expand Up @@ -165,6 +166,7 @@ func (s *Scanner) ScanBlock(block *BlockData) (found int, spent int) {
OneTimePubKey: out.PubKey,
Commitment: out.Commitment,
BlockHeight: block.Height,
BlockHash: block.Hash,
IsCoinbase: tx.IsCoinbase,
Spent: false,
}
Expand Down Expand Up @@ -218,7 +220,7 @@ func (s *Scanner) ScanBlocks(blocks []*BlockData) (totalFound, totalSpent int) {
totalFound += found
totalSpent += spent

s.wallet.SetSyncedHeight(block.Height)
s.wallet.SetSyncedBlock(block.Height, block.Hash)
}
return totalFound, totalSpent
}
Expand Down Expand Up @@ -268,9 +270,9 @@ func BlockToScanData(blockJSON []byte) (*BlockData, error) {
TxID [32]byte `json:"tx_id"`
TxPublicKey [32]byte `json:"tx_public_key"`
Outputs []struct {
PublicKey [32]byte `json:"public_key"`
Commitment [32]byte `json:"commitment"`
EncryptedAmount [8]byte `json:"encrypted_amount"`
PublicKey [32]byte `json:"public_key"`
Commitment [32]byte `json:"commitment"`
EncryptedAmount [8]byte `json:"encrypted_amount"`
EncryptedMemo [MemoSize]byte `json:"encrypted_memo"`
} `json:"outputs"`
Inputs []struct {
Expand Down
35 changes: 35 additions & 0 deletions wallet/sync_reorg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package wallet

import "testing"

func TestWalletRewindRestoresSyncedBlockHash(t *testing.T) {
w := &Wallet{}
hash1 := [32]byte{0x01}
hash2 := [32]byte{0x02}

w.SetSyncedBlock(1, hash1)
w.SetSyncedBlock(2, hash2)

w.RewindToHeight(1)

height, hash := w.SyncedBlock()
if height != 1 {
t.Fatalf("height=%d, want 1", height)
}
if hash != hash1 {
t.Fatalf("hash=%x, want %x", hash[:], hash1[:])
}
}

func TestWalletSetSyncedHeightClearsUnknownHash(t *testing.T) {
w := &Wallet{}
hash := [32]byte{0x01}

w.SetSyncedBlock(1, hash)
w.SetSyncedHeight(1)

_, got := w.SyncedBlock()
if got != ([32]byte{}) {
t.Fatalf("hash=%x, want zero hash", got[:])
}
}
Loading