Skip to content

Commit

Permalink
fix: Fix panic when resyncing after consensus database is deleted
Browse files Browse the repository at this point in the history
  • Loading branch information
n8maninger committed Dec 13, 2024
1 parent ca3d83d commit 0e8dcab
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 17 deletions.
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ toolchain go1.23.2

require (
github.com/mattn/go-sqlite3 v1.14.24
go.sia.tech/core v0.7.1
go.sia.tech/coreutils v0.7.1-0.20241203172514-7bf95dd18f31
go.sia.tech/core v0.7.4-0.20241212155227-c36fa2aec558
go.sia.tech/coreutils v0.7.1-0.20241212095636-85dd0252d9ad
go.sia.tech/jape v0.12.1
go.sia.tech/web/walletd v0.25.0
go.uber.org/zap v1.27.0
Expand All @@ -24,7 +24,7 @@ require (
go.sia.tech/mux v1.3.0 // indirect
go.sia.tech/web v0.0.0-20240610131903-5611d44a533e // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.sia.tech/core v0.7.1 h1:PrKh19Ql5vJbQbB5YGtTHQ8W3fRF8hhYnR4kPOIOIME=
go.sia.tech/core v0.7.1/go.mod h1:gB8iXFJFSV8XIHRaL00CL6Be+hyykB+SYnvRPHCCc/E=
go.sia.tech/coreutils v0.7.1-0.20241203172514-7bf95dd18f31 h1:Qskaf8d6oDKG5emNvGHZsd9iZRqz2GeouVNKY5paXlE=
go.sia.tech/coreutils v0.7.1-0.20241203172514-7bf95dd18f31/go.mod h1:d6jrawloc02MCXi/EVc8FIN5h3C6XDiMs4fuFMcU0PU=
go.sia.tech/core v0.7.4-0.20241212155227-c36fa2aec558 h1:Waa5bGizNofNlgrrtLYAMK9rJtNea2D/RG/cHRGzNDA=
go.sia.tech/core v0.7.4-0.20241212155227-c36fa2aec558/go.mod h1:nKaUneURNBl2ATp3dWOd1VS7Oe6HiXzUGMIBA83uUGM=
go.sia.tech/coreutils v0.7.1-0.20241212095636-85dd0252d9ad h1:cuMwjdzF6rabxIO6cBNeKGq/NMFiJUAGnLtAqdLkhXA=
go.sia.tech/coreutils v0.7.1-0.20241212095636-85dd0252d9ad/go.mod h1:YCtWzHedl1cnM1iR64U/dfJUg5E2osMo7xAKuZKOCaM=
go.sia.tech/jape v0.12.1 h1:xr+o9V8FO8ScRqbSaqYf9bjj1UJ2eipZuNcI1nYousU=
go.sia.tech/jape v0.12.1/go.mod h1:wU+h6Wh5olDjkPXjF0tbZ1GDgoZ6VTi4naFw91yyWC4=
go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c=
Expand All @@ -28,8 +28,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
Expand Down
48 changes: 47 additions & 1 deletion persist/sqlite/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,52 @@ func (s *Store) SetIndexMode(mode wallet.IndexMode) error {
})
}

// ResetChainState deletes all blockchain state from the database.
func (s *Store) ResetChainState() error {
return s.transaction(func(tx *txn) error {
_, err := tx.Exec(`UPDATE sia_addresses SET siacoin_balance=$1, siafund_balance=0, immature_siacoin_balance=$1`, encode(types.ZeroCurrency))
if err != nil {
return fmt.Errorf("failed to reset sia addresses: %w", err)
}

_, err = tx.Exec(`DELETE FROM siacoin_elements`)
if err != nil {
return fmt.Errorf("failed to delete siacoin elements: %w", err)
}

_, err = tx.Exec(`DELETE FROM siafund_elements`)
if err != nil {
return fmt.Errorf("failed to delete siafund elements: %w", err)
}

_, err = tx.Exec(`DELETE FROM state_tree`)
if err != nil {
return fmt.Errorf("failed to delete state tree: %w", err)
}

_, err = tx.Exec(`DELETE FROM event_addresses`)
if err != nil {
return fmt.Errorf("failed to delete event addresses: %w", err)
}

_, err = tx.Exec(`DELETE FROM events`)
if err != nil {
return fmt.Errorf("failed to delete events: %w", err)
}

_, err = tx.Exec(`DELETE FROM chain_indices`)
if err != nil {
return fmt.Errorf("failed to delete chain indices: %w", err)
}

_, err = tx.Exec(`UPDATE global_settings SET last_indexed_height=0, last_indexed_id=$1, element_num_leaves=0`, encode(types.BlockID{}))
if err != nil {
return fmt.Errorf("failed to reset global settings: %w", err)
}
return nil
})
}

func getSiacoinStateElements(tx *txn) ([]stateElement, error) {
const query = `SELECT id, leaf_index, merkle_proof FROM siacoin_elements`
rows, err := tx.Query(query)
Expand Down Expand Up @@ -1212,7 +1258,7 @@ RETURNING id, address_id, siafund_value`, index.Height, encode(index.ID))
func deleteOrphanedSiafundElements(tx *txn, index types.ChainIndex, log *zap.Logger) (map[int64]uint64, error) {
rows, err := tx.Query(`DELETE FROM siafund_elements WHERE id IN (SELECT se.id FROM siafund_elements se
INNER JOIN chain_indices ci ON (ci.id=se.chain_index_id)
WHERE ci.height=$1 AND ci.block_id<>$2)
WHERE ci.height=$1 AND ci.block_id<>$2)
RETURNING id, address_id, siafund_value, spent_index_id IS NOT NULL`, index.Height, encode(index.ID))
if err != nil {
return nil, fmt.Errorf("failed to query siafund elements: %w", err)
Expand Down
9 changes: 8 additions & 1 deletion wallet/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -54,6 +55,7 @@ type (
// A Store is a persistent store of wallet data.
Store interface {
UpdateChainState(reverted []chain.RevertUpdate, applied []chain.ApplyUpdate) error
ResetChainState() error

WalletUnconfirmedEvents(id ID, index types.ChainIndex, timestamp time.Time, v1 []types.Transaction, v2 []types.V2Transaction) (annotated []Event, err error)
WalletEvents(walletID ID, offset, limit int) ([]Event, error)
Expand Down Expand Up @@ -380,7 +382,12 @@ func NewManager(cm ChainManager, store Store, opts ...Option) (*Manager, error)
m.mu.Lock()
// update the store
lastTip, err := store.LastCommittedIndex()
if err != nil {
if err != nil && strings.Contains(err.Error(), "missing block at index") {
log.Warn("missing block at index, resetting chain state", zap.Uint64("height", lastTip.Height), zap.Stringer("id", lastTip.ID))
if err := store.ResetChainState(); err != nil {
log.Panic("failed to reset chain state", zap.Error(err))
}
} else if err != nil {
log.Panic("failed to get last committed index", zap.Error(err))
} else if err := syncStore(ctx, store, cm, lastTip, m.syncBatchSize); err != nil && !errors.Is(err, context.Canceled) {
log.Panic("failed to sync store", zap.Error(err))
Expand Down
125 changes: 119 additions & 6 deletions wallet/wallet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"go.sia.tech/walletd/wallet"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
"lukechampine.com/frand"
)

func waitForBlock(tb testing.TB, cm *chain.Manager, ws wallet.Store) {
Expand Down Expand Up @@ -577,13 +578,13 @@ func TestWalletAddresses(t *testing.T) {
SpendPolicy: &spendPolicy,
Description: "hello, world",
}
err = db.AddWalletAddress(w.ID, addr)
err = wm.AddAddress(w.ID, addr)
if err != nil {
t.Fatal(err)
}

// Check that the address was added
addresses, err := db.WalletAddresses(w.ID)
addresses, err := wm.Addresses(w.ID)
if err != nil {
t.Fatal(err)
} else if len(addresses) != 1 {
Expand All @@ -600,12 +601,12 @@ func TestWalletAddresses(t *testing.T) {
addr.Description = "goodbye, world"
addr.Metadata = json.RawMessage(`{"foo": "bar"}`)

if err := db.AddWalletAddress(w.ID, addr); err != nil {
if err := wm.AddAddress(w.ID, addr); err != nil {
t.Fatal(err)
}

// Check that the address was added
addresses, err = db.WalletAddresses(w.ID)
addresses, err = wm.Addresses(w.ID)
if err != nil {
t.Fatal(err)
} else if len(addresses) != 1 {
Expand All @@ -621,13 +622,13 @@ func TestWalletAddresses(t *testing.T) {
}

// Remove the address
err = db.RemoveWalletAddress(w.ID, address)
err = wm.RemoveAddress(w.ID, address)
if err != nil {
t.Fatal(err)
}

// Check that the address was removed
addresses, err = db.WalletAddresses(w.ID)
addresses, err = wm.Addresses(w.ID)
if err != nil {
t.Fatal(err)
} else if len(addresses) != 0 {
Expand Down Expand Up @@ -3512,3 +3513,115 @@ func TestEventTypes(t *testing.T) {
assertEvent(t, types.Hash256(types.SiafundOutputID(sfe[0].ID).V2ClaimOutputID()), wallet.EventTypeSiafundClaim, claimValue, types.ZeroCurrency, cm.Tip().Height+144)
})
}

func TestReset(t *testing.T) {
log := zaptest.NewLogger(t)
dir := t.TempDir()
db, err := sqlite.OpenDatabase(filepath.Join(dir, "walletd.sqlite3"), log.Named("sqlite3"))
if err != nil {
t.Fatal(err)
}
defer db.Close()

bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db"))
if err != nil {
t.Fatal(err)
}
defer bdb.Close()

pk := types.GeneratePrivateKey()
addr := types.StandardUnlockHash(pk.PublicKey())

network, genesisBlock := testutil.Network()
// send the siafunds to the owned address
genesisBlock.Transactions[0].SiafundOutputs[0].Address = addr

store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock)
if err != nil {
t.Fatal(err)
}

cm := chain.NewManager(store, genesisState)

wm, err := wallet.NewManager(cm, db, wallet.WithLogger(log.Named("wallet")), wallet.WithIndexMode(wallet.IndexModeFull))
if err != nil {
t.Fatal(err)
}
defer wm.Close()

// helper to mine blocks
mineBlock := func(n int, addr types.Address) {
t.Helper()
for i := 0; i < n; i++ {
b, ok := coreutils.MineBlock(cm, addr, 15*time.Second)
if !ok {
t.Fatal("failed to mine block")
} else if err := cm.AddBlocks([]types.Block{b}); err != nil {
t.Fatal(err)
}
}
waitForBlock(t, cm, db)
}

assertBalance := func(t *testing.T, addr types.Address, siacoin types.Currency, siafund uint64) {
t.Helper()

balance, err := db.AddressBalance(addr)
if err != nil {
t.Fatal(err)
} else if !balance.Siacoins.Equals(siacoin) {
t.Fatalf("expected %v SC, got %v", siacoin, balance.Siacoins)
} else if balance.Siafunds != siafund {
t.Fatalf("expected %v siafunds, got %v", siafund, balance.Siafunds)
}
}

// mine a payout to the original address
mineBlock(1, addr)

assertBalance(t, addr, types.ZeroCurrency, genesisState.SiafundCount())

// mine a bunch of payouts to random addresses
for i := 0; i < 50; i++ {
mineBlock(1, frand.Entropy256())
}

assertBalance(t, addr, genesisState.BlockReward(), genesisState.SiafundCount())

// close the wallet and reset it
if err := wm.Close(); err != nil {
t.Fatal(err)
} else if err := db.ResetChainState(); err != nil {
t.Fatal(err)
}

index, err := db.LastCommittedIndex()
if err != nil {
t.Fatal(err)
} else if index.Height != 0 {
t.Fatalf("expected height 0, got %v", index.Height)
} else if index.ID != (types.BlockID{}) {
t.Fatalf("expected zero ID, got %v", index.ID)
}

// balance should be reset
assertBalance(t, addr, types.ZeroCurrency, 0)
events, err := db.AddressEvents(addr, 0, 100)
if err != nil {
t.Fatal(err)
} else if len(events) != 0 {
t.Fatalf("expected 0 events, got %v", len(events))
}

// reopen the wallet
wm, err = wallet.NewManager(cm, db, wallet.WithLogger(log.Named("wallet")), wallet.WithIndexMode(wallet.IndexModeFull))
if err != nil {
t.Fatal(err)
}
defer wm.Close()

// mine a block to trigger sync
mineBlock(1, types.VoidAddress)

assertBalance(t, addr, genesisState.BlockReward(), genesisState.SiafundCount())
}

0 comments on commit 0e8dcab

Please sign in to comment.