/* * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package main import ( "errors" "fmt" "github.com/conformal/btcjson" "github.com/conformal/btcutil" "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" "io/ioutil" "net" "net/http" _ "net/http/pprof" "os" "strings" "sync" "time" ) var ( // ErrNoWallet describes an error where a wallet does not exist and // must be created first. ErrNoWallet = &WalletOpenError{ Err: "wallet file does not exist", } // ErrNoTxs describes an error where the wallet and UTXO files were // successfully read, but the TX history file was not. It is up to // the caller whether this necessitates a rescan or not. ErrNoTxs = errors.New("tx file cannot be read") cfg *config curBlock = struct { sync.RWMutex wallet.BlockStamp }{ BlockStamp: wallet.BlockStamp{ Height: int32(btcutil.BlockHeightUnknown), }, } ) // GetCurBlock returns the blockchain height and SHA hash of the most // recently seen block. If no blocks have been seen since btcd has // connected, btcd is queried for the current block height and hash. func GetCurBlock() (bs wallet.BlockStamp, err error) { curBlock.RLock() bs = curBlock.BlockStamp curBlock.RUnlock() if bs.Height != int32(btcutil.BlockHeightUnknown) { return bs, nil } bb, _ := GetBestBlock(CurrentServerConn()) if bb == nil { return wallet.BlockStamp{ Height: int32(btcutil.BlockHeightUnknown), }, errors.New("current block unavailable") } hash, err := btcwire.NewShaHashFromStr(bb.Hash) if err != nil { return wallet.BlockStamp{ Height: int32(btcutil.BlockHeightUnknown), }, err } curBlock.Lock() if bb.Height > curBlock.BlockStamp.Height { bs = wallet.BlockStamp{ Height: bb.Height, Hash: *hash, } curBlock.BlockStamp = bs } curBlock.Unlock() return bs, nil } // NewJSONID is used to receive the next unique JSON ID for btcd // requests, starting from zero and incrementing by one after each // read. var NewJSONID = make(chan uint64) // JSONIDGenerator sends incremental integers across a channel. This // is meant to provide a unique value for the JSON ID field for btcd // messages. func JSONIDGenerator(c chan uint64) { var n uint64 for { c <- n n++ } } func main() { // Initialize logging and setup deferred flushing to ensure all // outstanding messages are written on shutdown loggers := setLogLevel(defaultLogLevel) defer func() { for _, logger := range loggers { logger.Flush() } }() tcfg, _, err := loadConfig() if err != nil { os.Exit(1) } cfg = tcfg // Change the logging level if needed. if cfg.DebugLevel != defaultLogLevel { loggers = setLogLevel(cfg.DebugLevel) } if cfg.Profile != "" { go func() { listenAddr := net.JoinHostPort("", cfg.Profile) log.Infof("Profile server listening on %s", listenAddr) profileRedirect := http.RedirectHandler("/debug/pprof", http.StatusSeeOther) http.Handle("/", profileRedirect) log.Errorf("%v", http.ListenAndServe(listenAddr, nil)) }() } // Check and update any old file locations. updateOldFileLocations() // Start account manager and open accounts. go AcctMgr.Start() AcctMgr.OpenAccounts() // Read CA file to verify a btcd TLS connection. cafile, err := ioutil.ReadFile(cfg.CAFile) if err != nil { log.Errorf("cannot open CA file: %v", err) os.Exit(1) } go func() { s, err := newServer(cfg.SvrListeners) if err != nil { log.Errorf("Unable to create HTTP server: %v", err) os.Exit(1) } // Start HTTP server to listen and send messages to frontend and btcd // backend. Try reconnection if connection failed. s.Start() }() // Begin generating new IDs for JSON calls. go JSONIDGenerator(NewJSONID) // Begin RPC server goroutines. go RPCGateway() go WalletRequestProcessor() // Begin maintanence goroutines. go SendBeforeReceiveHistorySync(SendTxHistSyncChans.add, SendTxHistSyncChans.done, SendTxHistSyncChans.remove, SendTxHistSyncChans.access) go StoreNotifiedMempoolRecvTxs(NotifiedRecvTxChans.add, NotifiedRecvTxChans.remove, NotifiedRecvTxChans.access) go NotifyBalanceSyncer(NotifyBalanceSyncerChans.add, NotifyBalanceSyncerChans.remove, NotifyBalanceSyncerChans.access) updateBtcd := make(chan *BtcdRPCConn) go func() { // Create an RPC connection and close the closed channel. // // It might be a better idea to create a new concrete type // just for an always disconnected RPC connection and begin // with that. btcd := NewBtcdRPCConn(nil) close(btcd.closed) // Maintain the current btcd connection. After reconnects, // the current connection should be updated. for { select { case conn := <-updateBtcd: btcd = conn case access := <-accessServer: access.server <- btcd } } }() for { btcd, err := BtcdConnect(cafile) if err != nil { log.Info("Retrying btcd connection in 5 seconds") time.Sleep(5 * time.Second) continue } updateBtcd <- btcd NotifyBtcdConnection(allClients) log.Info("Established connection to btcd") // Perform handshake. if err := Handshake(btcd); err != nil { var message string if jsonErr, ok := err.(*btcjson.Error); ok { message = jsonErr.Message } else { message = err.Error() } log.Errorf("Cannot complete handshake: %v", message) log.Info("Retrying btcd connection in 5 seconds") time.Sleep(5 * time.Second) continue } // Block goroutine until the connection is lost. <-btcd.closed NotifyBtcdConnection(allClients) log.Info("Lost btcd connection") } } // WalletOpenError is a special error type so problems opening wallet // files can be differentiated (by a type assertion) from other errors. type WalletOpenError struct { Err string } // Error satisifies the builtin error interface. func (e *WalletOpenError) Error() string { return e.Err } // OpenSavedAccount opens a named account from disk. If the wallet does not // exist, ErrNoWallet is returned as an error. func OpenSavedAccount(name string, cfg *config) (*Account, error) { netdir := networkDir(cfg.Net()) if err := checkCreateDir(netdir); err != nil { return nil, &WalletOpenError{ Err: err.Error(), } } wlt := new(wallet.Wallet) txs := tx.NewStore() a := &Account{ name: name, Wallet: wlt, TxStore: txs, } wfilepath := accountFilename("wallet.bin", name, netdir) txfilepath := accountFilename("tx.bin", name, netdir) var wfile, txfile *os.File // Read wallet file. wfile, err := os.Open(wfilepath) if err != nil { if os.IsNotExist(err) { // Must create and save wallet first. return nil, ErrNoWallet } msg := fmt.Sprintf("cannot open wallet file: %s", err) return nil, &WalletOpenError{msg} } defer wfile.Close() if _, err = wlt.ReadFrom(wfile); err != nil { msg := fmt.Sprintf("cannot read wallet: %s", err) return nil, &WalletOpenError{msg} } // Read tx file. If this fails, return a ErrNoTxs error and let // the caller decide if a rescan is necessary. var finalErr error if txfile, err = os.Open(txfilepath); err != nil { log.Errorf("cannot open tx file: %s", err) // This is not a error we should immediately return with, // but other errors can be more important, so only return // this if none of the others are hit. finalErr = ErrNoTxs a.fullRescan = true } else { defer txfile.Close() if _, err = txs.ReadFrom(txfile); err != nil { log.Errorf("cannot read tx file: %s", err) a.fullRescan = true finalErr = ErrNoTxs } } // Mark all active payment addresses as belonging to this account. for addr := range a.ActivePaymentAddresses() { MarkAddressForAccount(addr, name) } return a, finalErr } // OpenAccounts attempts to open all saved accounts. func OpenAccounts() map[string]*Account { accounts := make(map[string]*Account) // If the network (account) directory is missing, but the temporary // directory exists, move it. This is unlikely to happen, but possible, // if writing out every account file at once to a tmp directory (as is // done for changing a wallet passphrase) and btcwallet closes after // removing the network directory but before renaming the temporary // directory. netDir := networkDir(cfg.Net()) tmpNetDir := tmpNetworkDir(cfg.Net()) if !fileExists(netDir) && fileExists(tmpNetDir) { if err := Rename(tmpNetDir, netDir); err != nil { log.Errorf("Cannot move temporary network dir: %v", err) return accounts } } // The default account must exist, or btcwallet acts as if no // wallets/accounts have been created yet. a, err := OpenSavedAccount("", cfg) if err != nil { switch err.(type) { case *WalletOpenError: log.Errorf("Default account wallet file unreadable: %v", err) return accounts default: log.Warnf("Non-critical problem opening an account file: %v", err) } } accounts[""] = a // Read all filenames in the account directory, and look for any // filenames matching '*-wallet.bin'. These are wallets for // additional saved accounts. accountDir, err := os.Open(netDir) if err != nil { // Can't continue. log.Errorf("Unable to open account directory: %v", err) return accounts } defer accountDir.Close() fileNames, err := accountDir.Readdirnames(0) if err != nil { // fileNames might be partially set, so log an error and // at least try to open some accounts. log.Errorf("Unable to read all account files: %v", err) } var accountNames []string for _, file := range fileNames { if strings.HasSuffix(file, "-wallet.bin") { name := strings.TrimSuffix(file, "-wallet.bin") accountNames = append(accountNames, name) } } // Open all additional accounts. for _, acctName := range accountNames { // Log txstore/utxostore errors as these will be recovered // from with a rescan, but wallet errors must be returned // to the caller. a, err := OpenSavedAccount(acctName, cfg) if err != nil { switch err.(type) { case *WalletOpenError: log.Errorf("Error opening account's wallet: %v", err) default: log.Warnf("Non-critical error opening an account file: %v", err) } } else { accounts[acctName] = a } } return accounts } var accessServer = make(chan *AccessCurrentServerConn) // AccessCurrentServerConn is used to access the current RPC connection // from the goroutine managing btcd-side RPC connections. type AccessCurrentServerConn struct { server chan ServerConn } // CurrentServerConn returns the most recently-connected btcd-side // RPC connection. func CurrentServerConn() ServerConn { access := &AccessCurrentServerConn{ server: make(chan ServerConn), } accessServer <- access return <-access.server }