/* * Copyright (c) 2013 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/wallet" "github.com/conformal/btcwire" "github.com/conformal/btcws" "io/ioutil" "net" "net/http" _ "net/http/pprof" "os" "sync" "time" ) var ( // ErrNoWallet describes an error where a wallet does not exist and // must be created first. ErrNoWallet = errors.New("wallet file does not exist") // ErrNoUtxos describes an error where the wallet file was successfully // read, but the UTXO file was not. To properly handle this error, // a rescan should be done since the wallet creation block. ErrNoUtxos = errors.New("utxo file cannot be read") // 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 } // This is a hack and may result in races, but we need to make // sure that btcd is connected and sending a message will succeed, // or this will block forever. A better solution is to return an // error to the reply handler immediately if btcd is disconnected. if !btcdConnected.b { return wallet.BlockStamp{ Height: int32(btcutil.BlockHeightUnknown), }, errors.New("current block unavailable") } n := <-NewJSONID cmd := btcws.NewGetBestBlockCmd(fmt.Sprintf("btcwallet(%v)", n)) mcmd, err := cmd.MarshalJSON() if err != nil { return wallet.BlockStamp{ Height: int32(btcutil.BlockHeightUnknown), }, errors.New("cannot ask for best block") } c := make(chan *struct { hash *btcwire.ShaHash height int32 }) replyHandlers.Lock() replyHandlers.m[n] = func(result interface{}, e *btcjson.Error) bool { if e != nil { c <- nil return true } m, ok := result.(map[string]interface{}) if !ok { c <- nil return true } hashBE, ok := m["hash"].(string) if !ok { c <- nil return true } hash, err := btcwire.NewShaHashFromStr(hashBE) if err != nil { c <- nil return true } fheight, ok := m["height"].(float64) if !ok { c <- nil return true } c <- &struct { hash *btcwire.ShaHash height int32 }{ hash: hash, height: int32(fheight), } return true } replyHandlers.Unlock() // send message btcdMsgs <- mcmd // Block until reply is ready. reply, ok := <-c if !ok || reply == nil { return wallet.BlockStamp{ Height: int32(btcutil.BlockHeightUnknown), }, errors.New("current block unavailable") } curBlock.Lock() if reply.height > curBlock.BlockStamp.Height { bs = wallet.BlockStamp{ Height: reply.height, Hash: *reply.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 { log.Error(err) 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() // Open default account. // TODO(jrick): open all available accounts. err = accountstore.OpenAccount("", cfg) if err != nil { log.Warnf("cannot open default account: %v", err) } // 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) } // Start account disk syncer goroutine. go DirtyAccountSyncer() 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 maintanence goroutines. go SendBeforeReceiveHistorySync(SendTxHistSyncChans.add, SendTxHistSyncChans.done, SendTxHistSyncChans.remove, SendTxHistSyncChans.access) go StoreNotifiedMempoolRecvTxs(NotifiedRecvTxChans.add, NotifiedRecvTxChans.remove, NotifiedRecvTxChans.access) go NotifyMinedTxSender(NotifyMinedTx) go NotifyBalanceSyncer(NotifyBalanceSyncerChans.add, NotifyBalanceSyncerChans.remove, NotifyBalanceSyncerChans.access) for { replies := make(chan error) done := make(chan int) go func() { BtcdConnect(cafile, replies) close(done) }() selectLoop: for { select { case <-done: break selectLoop case err := <-replies: switch err { case ErrConnRefused: btcdConnected.c <- false log.Info("btcd connection refused, retying in 5 seconds") time.Sleep(5 * time.Second) case ErrConnLost: btcdConnected.c <- false log.Info("btcd connection lost, retrying in 5 seconds") time.Sleep(5 * time.Second) case nil: btcdConnected.c <- true log.Info("Established connection to btcd.") default: log.Infof("Unhandled error: %v", err) } } } } }