diff --git a/backend/coins/btc/account.go b/backend/coins/btc/account.go index 8efc6cc346..d80de29791 100644 --- a/backend/coins/btc/account.go +++ b/backend/coins/btc/account.go @@ -282,7 +282,22 @@ func (account *Account) Initialize() error { if account.initialized { return nil } - account.initialized = true + defer func() { + if account.initialized { + return + } + if account.transactions != nil { + account.transactions.Close() + account.transactions = nil + } + if account.db != nil { + if closeErr := account.db.Close(); closeErr != nil { + account.log.WithError(closeErr).Error("couldn't close db") + } + account.db = nil + } + account.subaccounts = nil + }() signingConfigurations := account.Config().Config.SigningConfigurations if len(signingConfigurations) == 0 { @@ -318,9 +333,10 @@ func (account *Account) Initialize() error { account.log.Debug("Connection to blockchain backend established") } } - account.coin.Initialize() + if err := account.coin.Initialize(); err != nil { + return err + } account.SetOffline(account.coin.Blockchain().ConnectionError()) - account.coin.Blockchain().RegisterOnConnectionErrorChangedEvent(onConnectionStatusChanged) theHeaders := account.coin.Headers() account.transactions = transactions.NewTransactions( account.coin.Net(), account.db, theHeaders, account.Synchronizer, @@ -344,9 +360,13 @@ func (account *Account) Initialize() error { account.subaccounts = append(account.subaccounts, subacc) } + if err := account.BaseAccount.Initialize(accountIdentifier); err != nil { + return err + } + account.coin.Blockchain().RegisterOnConnectionErrorChangedEvent(onConnectionStatusChanged) + account.initialized = true go account.ensureAddresses() - - return account.BaseAccount.Initialize(accountIdentifier) + return nil } // XPubVersionForScriptType returns the xpub version bytes for the given coin and script type. @@ -672,7 +692,10 @@ func (account *Account) onAddressStatus(address *addresses.AccountAddress, statu return } - account.transactions.UpdateAddressHistory(address.PubkeyScriptHashHex(), history) + if err := account.transactions.UpdateAddressHistory(address.PubkeyScriptHashHex(), history); err != nil { + account.reportFatalSyncError(err, "UpdateAddressHistory failed") + return + } account.incAndEmitSyncCounter() account.ensureAddresses() } diff --git a/backend/coins/btc/coin.go b/backend/coins/btc/coin.go index 62f5e2c0c5..8063228e0a 100644 --- a/backend/coins/btc/coin.go +++ b/backend/coins/btc/coin.go @@ -30,9 +30,10 @@ import ( // Coin models a Bitcoin-related coin. type Coin struct { - initOnce sync.Once - code coinpkg.Code - name string + initLock sync.Mutex + initialized bool + code coinpkg.Code + name string // unit is the main unit of the coin, e.g. 'BTC' unit string // formatUnit keeps track of the unit used, e.g. 'BTC' or 'sat' depening on if sat mode is enabled @@ -93,63 +94,78 @@ func (coin *Coin) TstSetMakeBlockchain(f func() blockchain.Interface) { } // Initialize implements coinpkg.Coin. -func (coin *Coin) Initialize() { - coin.initOnce.Do(func() { - // Init blockchain - coin.blockchain = coin.makeBlockchain() +func (coin *Coin) Initialize() error { + coin.initLock.Lock() + defer coin.initLock.Unlock() + if coin.initialized { + return nil + } - // Init Headers + // Init blockchain + blockchain := coin.makeBlockchain() - // delete old db version (up to v4.10.0, bbolt was used): - oldDBFilename := path.Join(coin.dbFolder, fmt.Sprintf("headers-%s.db", coin.code)) - if _, err := os.Stat(oldDBFilename); err == nil { - _ = os.Remove(oldDBFilename) - } + // Init Headers - db, err := headersdb.NewDB( - path.Join(coin.dbFolder, fmt.Sprintf("headers-%s.bin", coin.code)), - coin.log) - if err != nil { - coin.log.WithError(err).Panic("Could not open headers DB") + // delete old db version (up to v4.10.0, bbolt was used): + oldDBFilename := path.Join(coin.dbFolder, fmt.Sprintf("headers-%s.db", coin.code)) + if _, err := os.Stat(oldDBFilename); err == nil { + _ = os.Remove(oldDBFilename) + } + + db, err := headersdb.NewDB( + path.Join(coin.dbFolder, fmt.Sprintf("headers-%s.bin", coin.code)), + coin.log) + if err != nil { + blockchain.Close() + return errp.WithMessage(err, "could not open headers DB") + } + theHeaders := headers.NewHeaders( + coin.net, + db, + blockchain, + coin.log) + if err := theHeaders.Initialize(); err != nil { + if closeErr := db.Close(); closeErr != nil { + coin.log.WithError(closeErr).Error("could not close headers DB") } - coin.headers = headers.NewHeaders( - coin.net, - db, - coin.blockchain, - coin.log) - coin.headers.Initialize() - coin.headers.SubscribeEvent(func(event headers.Event) { - if event == headers.EventSyncing || event == headers.EventSynced { - status, err := coin.headers.Status() - if err != nil { - coin.log.WithError(err).Error("Could not get headers status") - coin.Notify(observable.Event{ - Subject: fmt.Sprintf("coins/%s/headers/status", coin.code), - Action: action.Replace, - Object: struct { - Success bool `json:"success"` - ErrorMessage string `json:"errorMessage,omitempty"` - }{ - Success: false, - ErrorMessage: err.Error(), - }, - }) - return - } + blockchain.Close() + return errp.WithMessage(err, "could not initialize headers") + } + coin.blockchain = blockchain + coin.headers = theHeaders + coin.headers.SubscribeEvent(func(event headers.Event) { + if event == headers.EventSyncing || event == headers.EventSynced { + status, err := coin.headers.Status() + if err != nil { + coin.log.WithError(err).Error("Could not get headers status") coin.Notify(observable.Event{ Subject: fmt.Sprintf("coins/%s/headers/status", coin.code), Action: action.Replace, Object: struct { - Success bool `json:"success"` - Status *headers.Status `json:"status"` + Success bool `json:"success"` + ErrorMessage string `json:"errorMessage,omitempty"` }{ - Success: true, - Status: status, + Success: false, + ErrorMessage: err.Error(), }, }) + return } - }) + coin.Notify(observable.Event{ + Subject: fmt.Sprintf("coins/%s/headers/status", coin.code), + Action: action.Replace, + Object: struct { + Success bool `json:"success"` + Status *headers.Status `json:"status"` + }{ + Success: true, + Status: status, + }, + }) + } }) + coin.initialized = true + return nil } // Name implements coinpkg.Coin. diff --git a/backend/coins/btc/coin_test.go b/backend/coins/btc/coin_test.go index d394252a11..905d18978b 100644 --- a/backend/coins/btc/coin_test.go +++ b/backend/coins/btc/coin_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "math/big" "os" + "path" "testing" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts/errors" @@ -53,7 +54,7 @@ func (s *testSuite) SetupTest() { } s.coin.TstSetMakeBlockchain(func() blockchain.Interface { return blockchainMock }) - s.coin.Initialize() + s.Require().NoError(s.coin.Initialize()) } func (s *testSuite) TearDownTest() { @@ -67,6 +68,28 @@ func TestSuite(t *testing.T) { suite.Run(t, &testSuite{code: coin.CodeLTC, unit: "LTC", net: <c.MainNetParams}) } +func (s *testSuite) TestInitializeRetriesAfterError() { + dbFolder := path.Join(s.dbFolder, "missing") + btcCoin := NewCoin(s.code, "Some coin", s.unit, coin.BtcUnitDefault, s.net, dbFolder, nil, + explorer, addressExplorer, socksproxy.NewSocksProxy(false, "")) + closeCount := 0 + btcCoin.TstSetMakeBlockchain(func() blockchain.Interface { + return &blockchainMock.BlockchainMock{ + MockClose: func() { + closeCount++ + }, + MockHeadersSubscribe: func(func(*types.Header)) {}, + } + }) + + s.Require().Error(btcCoin.Initialize()) + s.Require().Equal(1, closeCount) + + s.Require().NoError(os.MkdirAll(dbFolder, 0700)) + s.Require().NoError(btcCoin.Initialize()) + s.Require().Equal(1, closeCount) +} + func (s *testSuite) TestCoin() { s.Require().Equal(s.net, s.coin.Net()) s.Require().Equal(s.code, s.coin.Code()) diff --git a/backend/coins/btc/db/transactionsdb/transactionsdb.go b/backend/coins/btc/db/transactionsdb/transactionsdb.go index 5d67a86f01..b2837127d9 100644 --- a/backend/coins/btc/db/transactionsdb/transactionsdb.go +++ b/backend/coins/btc/db/transactionsdb/transactionsdb.go @@ -155,16 +155,16 @@ func (tx *Tx) PutTx(txHash chainhash.Hash, msgTx *wire.MsgTx, height int, header return nil } -// DeleteTx implements transactions.DBTxInterface. It panics if called from a read-only db -// transaction. -func (tx *Tx) DeleteTx(txHash chainhash.Hash) { +// DeleteTx implements transactions.DBTxInterface. +func (tx *Tx) DeleteTx(txHash chainhash.Hash) error { bucketTransactions, err := tx.tx.CreateBucketIfNotExists([]byte(bucketTransactionsKey)) if err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } if err := bucketTransactions.Delete(txHash[:]); err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } + return nil } // AddAddressToTx implements transactions.DBTxInterface. @@ -215,10 +215,10 @@ func (tx *Tx) UnverifiedTransactions() ([]chainhash.Hash, error) { func (tx *Tx) MarkTxVerified(txHash chainhash.Hash, headerTimestamp time.Time) error { bucketUnverifiedTransactions, err := tx.tx.CreateBucketIfNotExists([]byte(bucketUnverifiedTransactionsKey)) if err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } if err := bucketUnverifiedTransactions.Delete(txHash[:]); err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } return tx.modifyTx(txHash[:], func(walletTx *transactions.DBTxInfo) { truth := true @@ -248,16 +248,16 @@ func (tx *Tx) Input(outPoint wire.OutPoint) (*chainhash.Hash, error) { return nil, nil } -// DeleteInput implements transactions.DBTxInterface. It panics if called from a read-only db -// transaction. -func (tx *Tx) DeleteInput(outPoint wire.OutPoint) { +// DeleteInput implements transactions.DBTxInterface. +func (tx *Tx) DeleteInput(outPoint wire.OutPoint) error { bucketInputs, err := tx.tx.CreateBucketIfNotExists([]byte(bucketInputsKey)) if err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } if err := bucketInputs.Delete([]byte(outPoint.String())); err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } + return nil } // PutOutput implements transactions.DBTxInterface. @@ -308,16 +308,16 @@ func (tx *Tx) Outputs() (map[wire.OutPoint]*wire.TxOut, error) { return outputs, nil } -// DeleteOutput implements transactions.DBTxInterface. It panics if called from a read-only db -// transaction. -func (tx *Tx) DeleteOutput(outPoint wire.OutPoint) { +// DeleteOutput implements transactions.DBTxInterface. +func (tx *Tx) DeleteOutput(outPoint wire.OutPoint) error { bucketOutputs, err := tx.tx.CreateBucketIfNotExists([]byte(bucketOutputsKey)) if err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } if err := bucketOutputs.Delete([]byte(outPoint.String())); err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } + return nil } // PutAddressHistory implements transactions.DBTxInterface. diff --git a/backend/coins/btc/db/transactionsdb/transactionsdb_test.go b/backend/coins/btc/db/transactionsdb/transactionsdb_test.go index 24acda39c2..04343c710a 100644 --- a/backend/coins/btc/db/transactionsdb/transactionsdb_test.go +++ b/backend/coins/btc/db/transactionsdb/transactionsdb_test.go @@ -263,7 +263,7 @@ func TestTxQuick(t *testing.T) { require.True(t, !txInfo.CreatedTimestamp.After(now) || *txInfo.CreatedTimestamp == now) - tx.DeleteTx(txHash) + require.NoError(t, tx.DeleteTx(txHash)) delete(allTxHashes, txHash) require.True(t, checkTxHashes()) }) @@ -328,7 +328,7 @@ func TestInput(t *testing.T) { require.Nil(t, input) // no-op, does not exist yet - tx.DeleteInput(outpoint1) + require.NoError(t, tx.DeleteInput(outpoint1)) require.NoError(t, tx.PutInput(outpoint1, txhash1)) require.NoError(t, tx.PutInput(outpoint2, txhash2)) @@ -351,7 +351,7 @@ func TestInput(t *testing.T) { require.NoError(t, err) require.Equal(t, &txhash2, input) - tx.DeleteInput(outpoint1) + require.NoError(t, tx.DeleteInput(outpoint1)) input, err = tx.Input(outpoint1) require.NoError(t, err) require.Nil(t, input) @@ -376,7 +376,7 @@ func TestInputQuick(t *testing.T) { for _, outPoint := range allOutpoints { t.Run("", func(t *testing.T) { - tx.DeleteInput(outPoint) + require.NoError(t, tx.DeleteInput(outPoint)) txHash, err := tx.Input(outPoint) require.NoError(t, err) require.Nil(t, txHash) @@ -475,7 +475,7 @@ func TestOutputsQuick(t *testing.T) { for outPoint := range allOutputs { t.Run("", func(t *testing.T) { delete(allOutputs, outPoint) - tx.DeleteOutput(outPoint) + require.NoError(t, tx.DeleteOutput(outPoint)) require.True(t, checkOutputs()) output, err := tx.Output(outPoint) require.NoError(t, err) diff --git a/backend/coins/btc/headers/headers.go b/backend/coins/btc/headers/headers.go index dde32ee176..1b5846ccd0 100644 --- a/backend/coins/btc/headers/headers.go +++ b/backend/coins/btc/headers/headers.go @@ -82,9 +82,9 @@ const ( // Interface represents the public API of this package. // -//go:generate mockery -name Interface +//go:generate mockery --name Interface type Interface interface { - Initialize() + Initialize() error SubscribeEvent(f func(Event)) func() VerifiedHeaderByHeight(int) (*wire.BlockHeader, error) HeaderByHeight(int) (*wire.BlockHeader, error) @@ -180,8 +180,12 @@ func (headers *Headers) TipHeight() int { } // Initialize starts the syncing process. -func (headers *Headers) Initialize() { - headers.tipAtInitTime = headers.tip() +func (headers *Headers) Initialize() error { + tip, err := headers.db.Tip() + if err != nil { + return err + } + headers.tipAtInitTime = tip headers.log.Infof("last tip loaded: %d", headers.tipAtInitTime) go headers.download() go headers.blockchain.HeadersSubscribe( @@ -190,6 +194,7 @@ func (headers *Headers) Initialize() { }, ) headers.kickChan <- struct{}{} + return nil } func (headers *Headers) download() { @@ -370,15 +375,16 @@ func (headers *Headers) canConnect(db DBInterface, tip int, header *wire.BlockHe return nil } -func (headers *Headers) reorg(db DBInterface, tip int) { +func (headers *Headers) reorg(db DBInterface, tip int) error { // Simple reorg method: re-fetch headers up to the maximum reorg limit. The server can shorten // our chain by sending a fake header and set us back by `reorgLimit` blocks, but it needs to // contain the correct PoW to do so. newTip := max(tip-reorgLimit, -1) if err := db.RevertTo(newTip); err != nil { - panic(err) + return err } headers.kick() + return nil } func (headers *Headers) notifyEvent(event Event) { @@ -395,7 +401,9 @@ func (headers *Headers) processBatch( err := headers.canConnect(db, tip+1, header) if errp.Cause(err) == errPrevHash { headers.log.WithError(err).Infof("Reorg detected at height %d", tip+1) - headers.reorg(db, tip) + if err := headers.reorg(db, tip); err != nil { + return err + } return nil } if err != nil { @@ -463,15 +471,6 @@ func (headers *Headers) update(blockHeight int) { headers.notifyEvent(EventNewTip) } -func (headers *Headers) tip() int { - defer headers.lock.RLock()() - tip, err := headers.db.Tip() - if err != nil { - panic(err) - } - return tip -} - // Status returns the current sync status. func (headers *Headers) Status() (*Status, error) { defer headers.lock.RLock()() diff --git a/backend/coins/btc/headers/headers_test.go b/backend/coins/btc/headers/headers_test.go index ee44430787..aef5e0da58 100644 --- a/backend/coins/btc/headers/headers_test.go +++ b/backend/coins/btc/headers/headers_test.go @@ -23,7 +23,7 @@ func TestClose(t *testing.T) { headers.testDownloadFinished = func() { close(didFinish) } - headers.Initialize() + require.NoError(t, headers.Initialize()) require.NoError(t, headers.Close()) select { diff --git a/backend/coins/btc/headers/mocks/Interface.go b/backend/coins/btc/headers/mocks/Interface.go index 2a3a6d4244..8fca9e93a9 100644 --- a/backend/coins/btc/headers/mocks/Interface.go +++ b/backend/coins/btc/headers/mocks/Interface.go @@ -4,9 +4,8 @@ package mocks import ( headers "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/headers" - mock "github.com/stretchr/testify/mock" - wire "github.com/btcsuite/btcd/wire" + mock "github.com/stretchr/testify/mock" ) // Interface is an autogenerated mock type for the Interface type @@ -45,8 +44,21 @@ func (_m *Interface) HeaderByHeight(_a0 int) (*wire.BlockHeader, error) { } // Initialize provides a mock function with no fields -func (_m *Interface) Initialize() { - _m.Called() +func (_m *Interface) Initialize() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Initialize") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 } // Status provides a mock function with no fields diff --git a/backend/coins/btc/transactions/db.go b/backend/coins/btc/transactions/db.go index 7abf281078..9f5aed96d8 100644 --- a/backend/coins/btc/transactions/db.go +++ b/backend/coins/btc/transactions/db.go @@ -41,7 +41,7 @@ type DBTxInterface interface { PutTx(txHash chainhash.Hash, tx *wire.MsgTx, height int, headerTimestamp *time.Time) error // DeleteTx deletes a transaction (nothing happens if not found). - DeleteTx(txHash chainhash.Hash) + DeleteTx(txHash chainhash.Hash) error // AddAddressToTx adds an address associated with a transaction. Retrieve them with `TxInfo()`. AddAddressToTx(chainhash.Hash, blockchain.ScriptHashHex) error @@ -69,7 +69,7 @@ type DBTxInterface interface { Input(wire.OutPoint) (*chainhash.Hash, error) // DeleteInput deletes an input (nothing happens if not found). - DeleteInput(wire.OutPoint) + DeleteInput(wire.OutPoint) error // PutOutput stores an Output. PutOutput(wire.OutPoint, *wire.TxOut) error @@ -79,7 +79,7 @@ type DBTxInterface interface { Outputs() (map[wire.OutPoint]*wire.TxOut, error) // DeleteOutput deletes an output (nothing happens if not found). - DeleteOutput(wire.OutPoint) + DeleteOutput(wire.OutPoint) error // PutAddressHistory stores an address history. PutAddressHistory(blockchain.ScriptHashHex, blockchain.TxHistory) error diff --git a/backend/coins/btc/transactions/mocks/transactions.go b/backend/coins/btc/transactions/mocks/transactions.go index ddd81f91a0..00f936c149 100644 --- a/backend/coins/btc/transactions/mocks/transactions.go +++ b/backend/coins/btc/transactions/mocks/transactions.go @@ -33,7 +33,7 @@ var _ transactions.Interface = &InterfaceMock{} // TransactionsFunc: func(isChange func(blockchain.ScriptHashHex) bool) (accounts.OrderedTransactions, error) { // panic("mock out the Transactions method") // }, -// UpdateAddressHistoryFunc: func(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) { +// UpdateAddressHistoryFunc: func(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error { // panic("mock out the UpdateAddressHistory method") // }, // } @@ -56,7 +56,7 @@ type InterfaceMock struct { TransactionsFunc func(isChange func(blockchain.ScriptHashHex) bool) (accounts.OrderedTransactions, error) // UpdateAddressHistoryFunc mocks the UpdateAddressHistory method. - UpdateAddressHistoryFunc func(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) + UpdateAddressHistoryFunc func(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error // calls tracks calls to the methods. calls struct { @@ -203,7 +203,7 @@ func (mock *InterfaceMock) TransactionsCalls() []struct { } // UpdateAddressHistory calls UpdateAddressHistoryFunc. -func (mock *InterfaceMock) UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) { +func (mock *InterfaceMock) UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error { if mock.UpdateAddressHistoryFunc == nil { panic("InterfaceMock.UpdateAddressHistoryFunc: method is nil but Interface.UpdateAddressHistory was just called") } @@ -217,7 +217,7 @@ func (mock *InterfaceMock) UpdateAddressHistory(scriptHashHex blockchain.ScriptH mock.lockUpdateAddressHistory.Lock() mock.calls.UpdateAddressHistory = append(mock.calls.UpdateAddressHistory, callInfo) mock.lockUpdateAddressHistory.Unlock() - mock.UpdateAddressHistoryFunc(scriptHashHex, txs) + return mock.UpdateAddressHistoryFunc(scriptHashHex, txs) } // UpdateAddressHistoryCalls gets all the calls that were made to UpdateAddressHistory. diff --git a/backend/coins/btc/transactions/transactions.go b/backend/coins/btc/transactions/transactions.go index e9247d6842..a6fb5130e5 100644 --- a/backend/coins/btc/transactions/transactions.go +++ b/backend/coins/btc/transactions/transactions.go @@ -58,7 +58,7 @@ type Interface interface { // UpdateAddressHistory should be called when initializing a wallet address, or when the history of // an address changes (a new transaction that touches it appears or disappears). The transactions // are downloaded and indexed. - UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) + UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error } // Transactions handles wallet transactions: keeping an index of the transactions, inputs, (unspent) @@ -188,6 +188,9 @@ func (transactions *Transactions) getCachedTimestampAtHeight(height int, txInfoH return headerTimestamp } +// processTxForAddress stores and indexes a transaction for the given address. +// It returns true if the transaction became newly confirmed while processing this address, so +// verification can be started after the DB update succeeds. func (transactions *Transactions) processTxForAddress( dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, @@ -195,30 +198,25 @@ func (transactions *Transactions) processTxForAddress( tx *wire.MsgTx, height int, headerTimestamp *time.Time, -) { +) (bool, error) { txInfo, err := dbTx.TxInfo(txHash) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve tx info") + return false, errp.WithMessage(err, "failed to retrieve tx info") } if err := dbTx.PutTx(txHash, tx, height, headerTimestamp); err != nil { - transactions.log.WithError(err).Panic("Failed to put tx") + return false, errp.WithMessage(err, "failed to put tx") } - if err := transactions.notifier.Put(txHash[:]); err != nil { - transactions.log.WithError(err).Error("Failed notifier.Put") - } - - // Newly confirmed tx. Try to verify it. - if txInfo.Height <= 0 && height > 0 { - transactions.log.Debug("Try to verify newly confirmed tx") - go transactions.verifyTransaction(txHash, height) - } + newlyConfirmed := txInfo.Height <= 0 && height > 0 if err := dbTx.AddAddressToTx(txHash, scriptHashHex); err != nil { - transactions.log.WithError(err).Panic("Failed to add address to tx") + return false, errp.WithMessage(err, "failed to add address to tx") } - transactions.processInputsAndOutputsForAddress(dbTx, scriptHashHex, txHash, tx) + if err := transactions.processInputsAndOutputsForAddress(dbTx, scriptHashHex, txHash, tx); err != nil { + return false, err + } + return newlyConfirmed, nil } // Go through the tx and extract all inputs and outputs which touch the address. @@ -226,7 +224,7 @@ func (transactions *Transactions) processInputsAndOutputsForAddress( dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, txHash chainhash.Hash, - tx *wire.MsgTx) { + tx *wire.MsgTx) error { // Gather transaction inputs that spend outputs of the given address. for _, txIn := range tx.TxIn { // Since transactions can be processed in any order, and we might process the same tx @@ -235,7 +233,7 @@ func (transactions *Transactions) processInputsAndOutputsForAddress( // since the output that it spends might be indexed later. txInTxHash, err := dbTx.Input(txIn.PreviousOutPoint) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve input from previous outpoint") + return errp.WithMessage(err, "failed to retrieve input from previous outpoint") } if txInTxHash != nil && *txInTxHash != txHash { transactions.log.WithFields(logrus.Fields{"txIn.PreviousOutPoint": txIn.PreviousOutPoint, @@ -243,7 +241,7 @@ func (transactions *Transactions) processInputsAndOutputsForAddress( Warning("Double spend detected") } if err := dbTx.PutInput(txIn.PreviousOutPoint, txHash); err != nil { - transactions.log.WithError(err).Panic("Failed to store the transaction input") + return errp.WithMessage(err, "failed to store the transaction input") } } // Gather transaction outputs that belong to us. @@ -255,23 +253,24 @@ func (transactions *Transactions) processInputsAndOutputsForAddress( txOut, ) if err != nil { - transactions.log.WithError(err).Panic("Failed to store the transaction output") + return errp.WithMessage(err, "failed to store the transaction output") } } } + return nil } -func (transactions *Transactions) allInputsOurs(dbTx DBTxInterface, transaction *wire.MsgTx) bool { +func (transactions *Transactions) allInputsOurs(dbTx DBTxInterface, transaction *wire.MsgTx) (bool, error) { for _, txIn := range transaction.TxIn { txOut, err := dbTx.Output(txIn.PreviousOutPoint) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve output") + return false, errp.WithMessage(err, "failed to retrieve output") } if txOut == nil { - return false + return false, nil } } - return true + return true, nil } // SpendableOutputs returns all unspent outputs of the wallet which are eligible to be spent. Those @@ -285,7 +284,10 @@ func (transactions *Transactions) SpendableOutputs() (map[wire.OutPoint]*Spendab } result := map[wire.OutPoint]*SpendableOutput{} for outPoint, txOut := range outputs { - spent := transactions.isInputSpent(dbTx, outPoint) + spent, err := transactions.isInputSpent(dbTx, outPoint) + if err != nil { + return nil, err + } if !spent { txInfo, err := dbTx.TxInfo(outPoint.Hash) if err != nil { @@ -293,7 +295,15 @@ func (transactions *Transactions) SpendableOutputs() (map[wire.OutPoint]*Spendab } confirmed := txInfo.Height > 0 - if confirmed || transactions.allInputsOurs(dbTx, txInfo.Tx) { + spendable := confirmed + if !spendable { + allInputsOurs, err := transactions.allInputsOurs(dbTx, txInfo.Tx) + if err != nil { + return nil, err + } + spendable = allInputsOurs + } + if spendable { result[outPoint] = &SpendableOutput{ TxOut: txOut, HeaderTimestamp: txInfo.HeaderTimestamp, @@ -305,63 +315,77 @@ func (transactions *Transactions) SpendableOutputs() (map[wire.OutPoint]*Spendab }) } -func (transactions *Transactions) isInputSpent(dbTx DBTxInterface, outPoint wire.OutPoint) bool { +func (transactions *Transactions) isInputSpent(dbTx DBTxInterface, outPoint wire.OutPoint) (bool, error) { input, err := dbTx.Input(outPoint) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve input for outPoint") + return false, errp.WithMessage(err, "failed to retrieve input for outPoint") } - return input != nil + return input != nil, nil } +// removeTxForAddress removes the address association for a transaction. +// It returns true if the transaction was removed from the wallet index entirely, so notifier +// cleanup can happen after the DB update succeeds. func (transactions *Transactions) removeTxForAddress( - dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, txHash chainhash.Hash) { + dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, txHash chainhash.Hash) (bool, error) { transactions.log.Debug("Remove transaction for address") txInfo, err := dbTx.TxInfo(txHash) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve tx info") + return false, errp.WithMessage(err, "failed to retrieve tx info") } if txInfo == nil { // Not yet indexed. transactions.log.Debug("Transaction hash not listed") - return + return false, nil } transactions.log.Debug("Deleting transaction address") empty, err := dbTx.RemoveAddressFromTx(txHash, scriptHashHex) if err != nil { - transactions.log.WithError(err).Panic("Failed to remove address from tx") + return false, errp.WithMessage(err, "failed to remove address from tx") } if empty { // Tx is not touching any of our outputs anymore. Remove. for _, txIn := range txInfo.Tx.TxIn { transactions.log.Debug("Deleting transaction iput") - dbTx.DeleteInput(txIn.PreviousOutPoint) + if err := dbTx.DeleteInput(txIn.PreviousOutPoint); err != nil { + return false, errp.WithMessage(err, "failed to delete input") + } } // Remove the outputs added by this tx. for index := range txInfo.Tx.TxOut { - dbTx.DeleteOutput(wire.OutPoint{ + if err := dbTx.DeleteOutput(wire.OutPoint{ Hash: txHash, Index: uint32(index), - }) + }); err != nil { + return false, errp.WithMessage(err, "failed to delete output") + } } - dbTx.DeleteTx(txHash) - if err := transactions.notifier.Delete(txHash[:]); err != nil { - transactions.log.WithError(err).Error("Failed notifier.Delete") + if err := dbTx.DeleteTx(txHash); err != nil { + return false, errp.WithMessage(err, "failed to delete tx") } } + return empty, nil } // UpdateAddressHistory should be called when initializing a wallet address, or when the history of // an address changes (a new transaction that touches it appears or disappears). The transactions // are downloaded and indexed. -func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) { +func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error { if transactions.isClosed() { transactions.log.Debug("UpdateAddressHistory after the instance was closed") - return + return nil + } + type txToVerify struct { + txHash chainhash.Hash + height int } + var txsToNotify []chainhash.Hash + var txsToVerify []txToVerify + var txsToDeleteFromNotifier []chainhash.Hash err := DBUpdate(transactions.db, func(dbTx DBTxInterface) error { txsSet := map[chainhash.Hash]struct{}{} for _, txInfo := range txs { @@ -381,7 +405,13 @@ func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain. // A tx was previously in the address history but is not anymore. If the tx was already // downloaded and indexed, it will be removed. If it is currently downloading (enqueued for // indexing), it will not be processed. - transactions.removeTxForAddress(dbTx, scriptHashHex, entry.TXHash.Hash()) + removed, err := transactions.removeTxForAddress(dbTx, scriptHashHex, entry.TXHash.Hash()) + if err != nil { + return err + } + if removed { + txsToDeleteFromNotifier = append(txsToDeleteFromNotifier, entry.TXHash.Hash()) + } } if err := dbTx.PutAddressHistory(scriptHashHex, txs); err != nil { @@ -390,14 +420,45 @@ func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain. for _, txInfo := range txs { txHash := txInfo.TXHash.Hash() height := txInfo.Height - tx, headerTimestamp := transactions.getTransactionCached(dbTx, txHash, height) - transactions.processTxForAddress(dbTx, scriptHashHex, txHash, tx, height, headerTimestamp) + tx, headerTimestamp, err := transactions.getTransactionCached(dbTx, txHash, height) + if err != nil { + return err + } + newlyConfirmed, err := transactions.processTxForAddress( + dbTx, scriptHashHex, txHash, tx, height, headerTimestamp, + ) + if err != nil { + return err + } + txsToNotify = append(txsToNotify, txHash) + if newlyConfirmed { + txsToVerify = append(txsToVerify, txToVerify{txHash: txHash, height: height}) + } } return nil }) if err != nil { - transactions.log.WithError(err).Panic("Failed to update address history") + return errp.WithMessage(err, "failed to update address history") + } + for _, txHash := range txsToDeleteFromNotifier { + if err := transactions.notifier.Delete(txHash[:]); err != nil { + transactions.log.WithError(err).Error("Failed notifier.Delete") + } + } + for _, txHash := range txsToNotify { + if err := transactions.notifier.Put(txHash[:]); err != nil { + transactions.log.WithError(err).Error("Failed notifier.Put") + } + } + if len(txsToVerify) != 0 { + go func() { + for _, tx := range txsToVerify { + transactions.log.Debug("Try to verify newly confirmed tx") + transactions.verifyTransaction(tx.txHash, tx.height) + } + }() } + return nil } // getTransactionCached requires transactions lock. @@ -405,22 +466,22 @@ func (transactions *Transactions) getTransactionCached( dbTx DBTxInterface, txHash chainhash.Hash, height int, -) (*wire.MsgTx, *time.Time) { +) (*wire.MsgTx, *time.Time, error) { txInfo, err := dbTx.TxInfo(txHash) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve transaction info") + return nil, nil, errp.WithMessage(err, "failed to retrieve transaction info") } headerTimestamp := transactions.getCachedTimestampAtHeight(height, txInfo.HeaderTimestamp) if txInfo.Tx != nil { - return txInfo.Tx, headerTimestamp + return txInfo.Tx, headerTimestamp, nil } tx, err := transactions.blockchain.TransactionGet(txHash) if err != nil { - transactions.log.WithError(err).Panic("TransactionGet failed") + return nil, nil, errp.WithMessage(err, "TransactionGet failed") } - return tx, headerTimestamp + return tx, headerTimestamp, nil } // Balance computes the confirmed and unconfirmed balance of the account. @@ -433,7 +494,11 @@ func (transactions *Transactions) Balance() (*accounts.Balance, error) { var available, incoming int64 for outPoint, txOut := range outputs { // What is spent can not be available nor incoming. - if spent := transactions.isInputSpent(dbTx, outPoint); spent { + spent, err := transactions.isInputSpent(dbTx, outPoint) + if err != nil { + return nil, err + } + if spent { continue } txInfo, err := dbTx.TxInfo(outPoint.Hash) @@ -441,7 +506,15 @@ func (transactions *Transactions) Balance() (*accounts.Balance, error) { return nil, err } confirmed := txInfo.Height > 0 - if confirmed || transactions.allInputsOurs(dbTx, txInfo.Tx) { + availableOutput := confirmed + if !availableOutput { + allInputsOurs, err := transactions.allInputsOurs(dbTx, txInfo.Tx) + if err != nil { + return nil, err + } + availableOutput = allInputsOurs + } + if availableOutput { available += txOut.Value } else { incoming += txOut.Value @@ -464,15 +537,14 @@ func (transactions *Transactions) outputToAddress(pkScript []byte) string { func (transactions *Transactions) txInfo( dbTx DBTxInterface, txInfo *DBTxInfo, - isChange func(blockchain.ScriptHashHex) bool) *accounts.TransactionData { + isChange func(blockchain.ScriptHashHex) bool) (*accounts.TransactionData, error) { var sumOurInputs btcutil.Amount var result btcutil.Amount allInputsOurs := true for _, txIn := range txInfo.Tx.TxIn { spentOut, err := dbTx.Output(txIn.PreviousOutPoint) if err != nil { - // TODO - transactions.log.WithError(err).Panic("Output() failed") + return nil, errp.WithMessage(err, "Output() failed") } if spentOut != nil { sumOurInputs += btcutil.Amount(spentOut.Value) @@ -491,8 +563,7 @@ func (transactions *Transactions) txInfo( Index: uint32(index), }) if err != nil { - // TODO - transactions.log.WithError(err).Panic("Output() failed") + return nil, errp.WithMessage(err, "Output() failed") } addressAndAmount := accounts.AddressAndAmount{ Address: transactions.outputToAddress(txOut.PkScript), @@ -580,7 +651,7 @@ func (transactions *Transactions) txInfo( Weight: btcdBlockchain.GetTransactionWeight(btcutilTx), CreatedTimestamp: txInfo.CreatedTimestamp, IsErc20: false, - } + }, nil } // Transactions returns an ordered list of transactions. @@ -597,7 +668,11 @@ func (transactions *Transactions) Transactions( if err != nil { return nil, err } - txs = append(txs, transactions.txInfo(dbTx, txInfo, isChange)) + txData, err := transactions.txInfo(dbTx, txInfo, isChange) + if err != nil { + return nil, err + } + txs = append(txs, txData) } return accounts.NewOrderedTransactions(txs), nil }) diff --git a/backend/coins/btc/transactions/transactions_test.go b/backend/coins/btc/transactions/transactions_test.go index a4ecff863d..8238f009ab 100644 --- a/backend/coins/btc/transactions/transactions_test.go +++ b/backend/coins/btc/transactions/transactions_test.go @@ -134,7 +134,7 @@ func (s *transactionsSuite) updateAddressHistory( s.notifierMock.On("Put", tx.TXHash[:]).Return(nil).Once() } - s.transactions.UpdateAddressHistory(address.PubkeyScriptHashHex(), txs) + s.Require().NoError(s.transactions.UpdateAddressHistory(address.PubkeyScriptHashHex(), txs)) } func newTx( diff --git a/backend/coins/btc/transactions/verification.go b/backend/coins/btc/transactions/verification.go index 0cb8c38330..b6b454a4ba 100644 --- a/backend/coins/btc/transactions/verification.go +++ b/backend/coins/btc/transactions/verification.go @@ -51,8 +51,8 @@ func hashMerkleRoot(merkle []blockchain.TXHash, start chainhash.Hash, pos int) c func (transactions *Transactions) verifyTransactions() { unverifiedTransactions, err := transactions.unverifiedTransactions() if err != nil { - // TODO - panic(err) + transactions.log.WithError(err).Error("unverifiedTransactions") + return } transactions.log.Debugf("verifying %d transactions", len(unverifiedTransactions)) for txHash, height := range unverifiedTransactions { diff --git a/backend/coins/coin/coin.go b/backend/coins/coin/coin.go index 6fa7b87650..498e22553f 100644 --- a/backend/coins/coin/coin.go +++ b/backend/coins/coin/coin.go @@ -61,7 +61,7 @@ type Coin interface { BlockExplorerTransactionURLPrefix() string // Initialize initializes the coin by connecting to a full node, downloading the headers, etc. - Initialize() + Initialize() error // SmallestUnit returns the name of the smallest unit of a given coin SmallestUnit() string diff --git a/backend/coins/coin/mocks/coin.go b/backend/coins/coin/mocks/coin.go index e357d2862d..a81fa2d4a3 100644 --- a/backend/coins/coin/mocks/coin.go +++ b/backend/coins/coin/mocks/coin.go @@ -38,7 +38,7 @@ var _ coin.Coin = &CoinMock{} // GetFormatUnitFunc: func(isFee bool) string { // panic("mock out the GetFormatUnit method") // }, -// InitializeFunc: func() { +// InitializeFunc: func() error { // panic("mock out the Initialize method") // }, // NameFunc: func() string { @@ -88,7 +88,7 @@ type CoinMock struct { GetFormatUnitFunc func(isFee bool) string // InitializeFunc mocks the Initialize method. - InitializeFunc func() + InitializeFunc func() error // NameFunc mocks the Name method. NameFunc func() string @@ -376,7 +376,7 @@ func (mock *CoinMock) GetFormatUnitCalls() []struct { } // Initialize calls InitializeFunc. -func (mock *CoinMock) Initialize() { +func (mock *CoinMock) Initialize() error { if mock.InitializeFunc == nil { panic("CoinMock.InitializeFunc: method is nil but Coin.Initialize was just called") } @@ -385,7 +385,7 @@ func (mock *CoinMock) Initialize() { mock.lockInitialize.Lock() mock.calls.Initialize = append(mock.calls.Initialize, callInfo) mock.lockInitialize.Unlock() - mock.InitializeFunc() + return mock.InitializeFunc() } // InitializeCalls gets all the calls that were made to Initialize. diff --git a/backend/coins/eth/account.go b/backend/coins/eth/account.go index c32dfb6669..ffc8df4480 100644 --- a/backend/coins/eth/account.go +++ b/backend/coins/eth/account.go @@ -191,7 +191,9 @@ func (account *Account) Initialize() error { account.signingConfiguration.ExtendedPublicKey(), ) - account.coin.Initialize() + if err := account.coin.Initialize(); err != nil { + return err + } account.initDone = account.Synchronizer.IncRequestsCounter() if !account.Config().SkipInitialSync { go account.EnqueueUpdate() diff --git a/backend/coins/eth/coin.go b/backend/coins/eth/coin.go index 3b406c48e1..6b7b1e6d31 100644 --- a/backend/coins/eth/coin.go +++ b/backend/coins/eth/coin.go @@ -100,7 +100,9 @@ func (coin *Coin) ChainIDstr() string { } // Initialize implements coin.Coin. -func (coin *Coin) Initialize() {} +func (coin *Coin) Initialize() error { + return nil +} // Name implements coin.Coin. func (coin *Coin) Name() string {