github.com/deroproject/derosuite@v2.1.6-1.0.20200307070847-0f2e589c7a2b+incompatible/walletapi/daemon_communication.go (about)

     1  // Copyright 2017-2018 DERO Project. All rights reserved.
     2  // Use of this source code in any form is governed by RESEARCH license.
     3  // license can be found in the LICENSE file.
     4  // GPG: 0F39 E425 8C65 3947 702A  8234 08B2 0360 A03A 9DE8
     5  //
     6  //
     7  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
     8  // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
     9  // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
    10  // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    11  // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    12  // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    13  // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
    14  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
    15  // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    16  
    17  package walletapi
    18  
    19  /* this file handles communication with the daemon
    20   * this includes receiving output information
    21   * *
    22   */
    23  import "io"
    24  import "os"
    25  import "fmt"
    26  import "net"
    27  import "time"
    28  import "sync"
    29  import "bytes"
    30  import "net/http"
    31  import "bufio"
    32  import "strings"
    33  import "runtime"
    34  import "compress/gzip"
    35  import "encoding/hex"
    36  import "encoding/json"
    37  import "runtime/debug"
    38  
    39  import "github.com/romana/rlog"
    40  
    41  //import "github.com/pierrec/lz4"
    42  import "github.com/ybbus/jsonrpc"
    43  import "github.com/vmihailenco/msgpack"
    44  
    45  import "github.com/deroproject/derosuite/config"
    46  import "github.com/deroproject/derosuite/crypto"
    47  import "github.com/deroproject/derosuite/globals"
    48  import "github.com/deroproject/derosuite/structures"
    49  import "github.com/deroproject/derosuite/transaction"
    50  
    51  // this global variable should be within wallet structure
    52  var Connected bool = false
    53  
    54  // there should be no global variables, so multiple wallets can run at the same time with different assset
    55  var rpcClient *jsonrpc.RPCClient
    56  var netClient *http.Client
    57  var endpoint string
    58  
    59  var output_lock sync.Mutex
    60  
    61  // returns whether wallet was online some time ago
    62  func (w *Wallet) IsDaemonOnlineCached() bool {
    63  	return Connected
    64  }
    65  
    66  // currently process url  with compatibility for older ip address
    67  func buildurl(endpoint string) string {
    68      if strings.IndexAny(endpoint,"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") >= 0 { // url is already complete
    69          return strings.TrimSuffix(endpoint,"/")
    70      }else{
    71          return "http://" + endpoint
    72      }
    73      
    74      
    75  }
    76  
    77  // this is as simple as it gets
    78  // single threaded communication to get the daemon status and height
    79  // this will tell whether the wallet can connection successfully to  daemon or not
    80  func (w *Wallet) IsDaemonOnline() (err error) {
    81  
    82      if globals.Arguments["--remote"] == true  && globals.IsMainnet() {
    83          w.Daemon_Endpoint = config.REMOTE_DAEMON
    84      }
    85  
    86  	// if user provided endpoint has error, use default
    87  	if w.Daemon_Endpoint == "" {
    88  		w.Daemon_Endpoint = "127.0.0.1:" + fmt.Sprintf("%d", config.Mainnet.RPC_Default_Port)
    89  		if !globals.IsMainnet() {
    90  			w.Daemon_Endpoint = "127.0.0.1:" + fmt.Sprintf("%d", config.Testnet.RPC_Default_Port)
    91  		}
    92  	}
    93  
    94  	if globals.Arguments["--daemon-address"] != nil {
    95  		w.Daemon_Endpoint = globals.Arguments["--daemon-address"].(string)
    96  	}
    97  
    98  	rlog.Infof("Daemon endpoint %s", w.Daemon_Endpoint)
    99  
   100  	// TODO enable socks support here
   101  	var netTransport = &http.Transport{
   102  		Dial: (&net.Dialer{
   103  			Timeout: 5 * time.Second, // 5 second timeout
   104  		}).Dial,
   105  		TLSHandshakeTimeout: 5 * time.Second,
   106  	}
   107  
   108  	netClient = &http.Client{
   109  		Timeout:   time.Second * 10,
   110  		Transport: netTransport,
   111  	}
   112  
   113  	// create client
   114  	rpcClient = jsonrpc.NewRPCClient(buildurl(w.Daemon_Endpoint) + "/json_rpc")
   115  
   116  	// execute rpc to service
   117  	response, err := rpcClient.Call("get_info")
   118  
   119          // notify user of any state change
   120  	// if daemon connection breaks or comes live again
   121  	if err == nil {
   122  		if !Connected {
   123  			rlog.Infof("Connection to RPC server successful %s", buildurl(w.Daemon_Endpoint))
   124  			Connected = true
   125  		}
   126  	} else {
   127  		rlog.Errorf("Error executing getinfo_rpc err %s", err)
   128  
   129  		if Connected {
   130  			rlog.Warnf("Connection to RPC server Failed err %s endpoint %s ", err, buildurl(w.Daemon_Endpoint))
   131  			fmt.Printf("Connection to RPC server Failed err %s endpoint %s ", err, buildurl(w.Daemon_Endpoint))
   132  
   133  		}
   134  		Connected = false
   135  
   136  		return
   137  	}
   138  	var info structures.GetInfo_Result
   139  	err = response.GetObject(&info)
   140  	if err != nil {
   141  		rlog.Errorf("Daemon getinfo RPC parsing error err: %s\n", err)
   142  		Connected = false
   143  		return
   144  	}
   145  	// detect whether both are in different modes
   146  	//  daemon is in testnet and wallet in mainnet or
   147  	// daemon
   148  	if info.Testnet != !globals.IsMainnet() {
   149  		err = fmt.Errorf("Mainnet/TestNet  is different between wallet/daemon.Please run daemon/wallet without --testnet")
   150  		rlog.Criticalf("%s", err)
   151  		return
   152  	}
   153  
   154  	w.Lock()
   155  	defer w.Unlock()
   156  
   157  	if info.Height >= 0 {
   158  		w.Daemon_Height = uint64(info.Height)
   159  		w.Daemon_TopoHeight = info.TopoHeight
   160  	}
   161  	w.dynamic_fees_per_kb = info.Dynamic_fee_per_kb // set fee rate, it can work for quite some time,
   162  
   163  	return nil
   164  }
   165  
   166  // do the entire sync
   167  // lagging behind is the NOT the major problem
   168  // the problem is the frequent soft-forks
   169  // once an input has been detected, we must check whether the block is not orphan
   170  // we must do this without leaking ourselves to the daemon itself
   171  // this means, we will have to keep track of the chain in the wallet also, ofcouse for syncing purposes only
   172  // we have a bucket where we store  topo height to block hash links , of course in encrypted form
   173  // during syncing we do a a binary search style matching and then sync up from that point
   174  func (w *Wallet) DetectSyncPoint() (start_sync_at_height uint64, err error) {
   175  	err = w.IsDaemonOnline()
   176  	if err != nil { // if we cannot connect with daemon bail out now
   177  		return
   178  	}
   179  
   180  	rlog.Tracef(2, "Detection of Sync Point started")
   181  
   182  	w.Lock()
   183  
   184  	minimum := int64(0)
   185  	maximum := int64(w.account.TopoHeight)
   186  
   187  	// if wallet height >  daemon height, we can only do sanity chec
   188  	if maximum > w.Daemon_TopoHeight {
   189  		w.Unlock()
   190  		err = fmt.Errorf("Wallet can never be ahead than daemon. Make sure daemon is synced")
   191  		return
   192  	}
   193  	w.Unlock()
   194  
   195  	//rlog.Debugf("min %d max %d", minimum, maximum)
   196  
   197  	//corruption_point := uint64(40000)
   198  	// maximum is overridden the the current blockchain height
   199  	//base := minimum
   200  	for minimum <= maximum { //  with just 24 requests we can find, the mismatch point,in 16 million chain height
   201  		// get hash at height (maximum-minimum)/2
   202  		// compare it with what is stored in wallet
   203  		// if match, increase minimum
   204  		// of no match , decrease maximum
   205  		median := ((minimum + maximum) / 2)
   206  
   207  		// try to load first
   208  		rlog.Tracef(4, "min %d max %d median %d\n", minimum, maximum, median)
   209  
   210  		var local_hash []byte
   211  		var response *jsonrpc.RPCResponse
   212  		local_hash, err = w.load_key_value(BLOCKCHAIN_UNIVERSE, []byte(HEIGHT_TO_BLOCK_BUCKET), itob(uint64(median)))
   213  		if err != nil {
   214  			maximum = median - 1
   215  			continue
   216  		}
   217  
   218  		/* if len(local_hash) == 32 && median >= corruption_point {
   219  		   local_hash[0]=0;
   220  		   local_hash[1]=1;
   221  		   local_hash[2]=2;
   222  		   local_hash[3]=3;
   223  		   local_hash[4]=4;
   224  
   225  		  }*/
   226  
   227  		response, err = rpcClient.CallNamed("getblockheaderbytopoheight", map[string]interface{}{"topoheight": median})
   228  		if err != nil {
   229  			rlog.Errorf("Connection to RPC server Failed err %s", err)
   230  			return
   231  		}
   232  
   233  		// parse response
   234  		if response.Error != nil {
   235  			rlog.Errorf("Connection to RPC server Failed err %s", response.Error)
   236  			return
   237  		}
   238  
   239  		var bresult structures.GetBlockHeaderByHeight_Result
   240  
   241  		err = response.GetObject(&bresult)
   242  		if err != nil {
   243  			return // err
   244  		}
   245  		if bresult.Status != "OK" {
   246  			err = fmt.Errorf("%s", bresult.Status)
   247  			return
   248  		}
   249  		rlog.Tracef(4, "hash %s local_hash %s \n", bresult.Block_Header.Hash, fmt.Sprintf("%x", local_hash))
   250  		if fmt.Sprintf("%x", local_hash) == bresult.Block_Header.Hash {
   251  			minimum = median + 1
   252  		} else {
   253  			maximum = median - 1
   254  		}
   255  	}
   256  
   257  	if minimum >= 1 {
   258  		minimum--
   259  	}
   260  
   261  	// we should start syncing from the minimum, this will help us override any soft-forks, though however deep them may be
   262  	start_sync_at_height = uint64(minimum)
   263  	rlog.Infof("sync height  %d\n", start_sync_at_height)
   264  
   265  	return
   266  }
   267  
   268  // get the outputs from the daemon, requesting specfic outputs
   269  // the range can be anything
   270  // if stop is zero,
   271  // the daemon will flush out everything it has ASAP
   272  // the stream can be saved and used later on
   273  
   274  func (w *Wallet) Sync_Wallet_With_Daemon() {
   275  
   276  	w.IsDaemonOnline()
   277  	output_lock.Lock()
   278  	defer output_lock.Unlock()
   279  
   280  	// only sync if both height are different
   281  	if w.Daemon_TopoHeight == w.account.TopoHeight && w.account.TopoHeight != 0 { // wallet is already synced
   282  		return
   283  	}
   284  
   285  	rlog.Infof("wallet topo height %d daemon online topo height %d\n", w.account.TopoHeight, w.Daemon_TopoHeight)
   286  
   287  	start_height, err := w.DetectSyncPoint()
   288  	if err != nil {
   289  		rlog.Errorf("Error while detecting sync point err %s", err)
   290  		return
   291  	}
   292  
   293  	// the safety cannot be tuned off in openbsd, see boltdb  documentation
   294  	// if we are doing major rescanning, turn of db safety features
   295  	// they will be activated again on resync
   296  	if (w.Daemon_TopoHeight - int64(start_height)) > 50 { // get db into async mode
   297  		w.Lock()
   298  		w.db.NoSync = true
   299  		w.Unlock()
   300  		defer func() {
   301  			w.Lock()
   302  			w.db.NoSync = false
   303  			w.db.Sync()
   304  			w.Unlock()
   305  		}()
   306  	}
   307  
   308  	rlog.Infof("requesting outputs from height %d\n", start_height)
   309  
   310  	response, err := http.Get(fmt.Sprintf("%s/getoutputs.bin?startheight=%d",buildurl(w.Daemon_Endpoint), start_height))
   311  	if err != nil {
   312  		rlog.Errorf("Error while requesting outputs from daemon err %s", err)
   313  	} else {
   314  		defer response.Body.Close()
   315  		gzipreader, err := gzip.NewReader(response.Body)
   316  		if err != nil {
   317  			rlog.Errorf("Error while decompressing output from daemon  err: %s   ", err)
   318  			return
   319  		}
   320  		defer gzipreader.Close()
   321  
   322  		// use the reader and feed the error free stream, if error occurs bailout
   323  		decoder := msgpack.NewDecoder(gzipreader)
   324  		rlog.Debugf("Scanning started\n")
   325  
   326  		workers := make(chan int, runtime.GOMAXPROCS(0))
   327  		for i := 0; i < runtime.GOMAXPROCS(0); i++ {
   328  			workers <- i
   329  		}
   330  		for {
   331  			var output globals.TX_Output_Data
   332  
   333  			err = decoder.Decode(&output)
   334  			if err == io.EOF { // reached eof
   335  				break
   336  			}
   337  			if err != nil {
   338  				rlog.Errorf("err while decoding msgpack stream err %s\n", err)
   339  				break
   340  			}
   341  
   342  			select { // quit midway if required
   343  			case <-w.quit:
   344  				return
   345  			default:
   346  			}
   347  
   348  			<-workers
   349  			// try to consume all data sent by the daemon
   350  			go func() {
   351  				w.Add_Transaction_Record_Funds(&output) // add the funds to wallet if they are ours
   352  				workers <- 0
   353  			}()
   354  
   355  		}
   356  
   357  		rlog.Debugf("Scanning finised\n")
   358  
   359  	}
   360  
   361  	return
   362  }
   363  
   364  // triggers syncing with wallet every 5 seconds
   365  func (w *Wallet) sync_loop() {
   366  	for {
   367  		select { // quit midway if required
   368  		case <-w.quit:
   369  			return
   370  		case <-time.After(5 * time.Second):
   371  		}
   372  
   373  		if !w.wallet_online_mode { // wallet requested to be in offline mode
   374  			return
   375  		}
   376  
   377  		w.Sync_Wallet_With_Daemon() // sync with the daemon
   378  		//TODO we must sync up with pool also
   379  	}
   380  }
   381  
   382  func (w *Wallet) Rescan_From_Height(startheight uint64) {
   383  	w.Lock()
   384  	defer w.Unlock()
   385  	if startheight < uint64(w.account.TopoHeight) {
   386  		w.account.TopoHeight = int64(startheight) // we will rescan from this height
   387  	}
   388  
   389  }
   390  
   391  // offline file is scanned from start till finish
   392  func (w *Wallet) Scan_Offline_File(filename string) {
   393  	w.Lock()
   394  	defer w.Unlock()
   395  
   396  	f, err := os.Open(filename)
   397  	if err != nil {
   398  		fmt.Printf("Cannot read offline data file=\"%s\"  err: %s   ", filename, err)
   399  		return
   400  	}
   401  	bufreader := bufio.NewReader(f)
   402  	gzipreader, err := gzip.NewReader(bufreader)
   403  	if err != nil {
   404  		fmt.Printf("Error while decompressing offline data file=\"%s\"  err: %s   ", filename, err)
   405  		return
   406  	}
   407  	defer gzipreader.Close()
   408  
   409  	// use the reader and feed the error free stream, if error occurs bailout
   410  	decoder := msgpack.NewDecoder(gzipreader)
   411  	rlog.Debugf("Scanning started")
   412  	for {
   413  		var output globals.TX_Output_Data
   414  
   415  		err = decoder.Decode(&output)
   416  		if err == io.EOF { // reached eof
   417  			break
   418  		}
   419  		if err != nil {
   420  			fmt.Printf("err while decoding msgpack stream err %s\n", err)
   421  			break
   422  		}
   423  		// try to consume all data sent by the daemon
   424  		w.Add_Transaction_Record_Funds(&output) // add the funds to wallet if they are ours
   425  
   426  	}
   427  	rlog.Debugf("Scanning finised")
   428  
   429  }
   430  
   431  // this is as simple as it gets
   432  // single threaded communication to relay TX to daemon
   433  // if this is successful, then daemon is in control
   434  
   435  func (w *Wallet) SendTransaction(tx *transaction.Transaction) (err error) {
   436  
   437  	if tx == nil {
   438  		return fmt.Errorf("Can not send nil transaction")
   439  	}
   440  
   441  	var params structures.SendRawTransaction_Params
   442  	var result structures.SendRawTransaction_Result
   443  
   444  	params.Tx_as_hex = hex.EncodeToString(tx.Serialize())
   445  
   446  	var buf bytes.Buffer
   447  	err = json.NewEncoder(&buf).Encode(&params)
   448  	if err != nil {
   449  		return
   450  	}
   451  
   452  	// this method is NOT JSON RPC method, send raw as http request and parse response
   453  	resp, err := http.Post(fmt.Sprintf("%s/sendrawtransaction", buildurl(w.Daemon_Endpoint)), "application/json", &buf)
   454  	if err != nil {
   455  		return
   456  	}
   457  
   458  	decoder := json.NewDecoder(resp.Body)
   459  	err = decoder.Decode(&result)
   460  	if err != nil {
   461  		err = fmt.Errorf("err while decoding incoming sendrawtransaction response json err: %s", err)
   462  		return
   463  	}
   464  
   465  	if result.Status == "OK" {
   466  		return nil
   467  	} else {
   468  		err = fmt.Errorf("Err %s", result.Status)
   469  	}
   470  
   471  	//fmt.Printf("err in response %+v", result)
   472  
   473  	return
   474  }
   475  
   476  // this is as simple as it gets
   477  // single threaded communication  gets whether the the key image is spent in pool or in blockchain
   478  // this can leak informtion which keyimage belongs to us
   479  // TODO in order to stop privacy leaks we must guess this information somehow on client side itself
   480  // maybe the server can broadcast a bloomfilter or something else from the mempool keyimages
   481  //
   482  func (w *Wallet) IsKeyImageSpent(keyimage crypto.Key) (spent bool) {
   483  
   484  	defer func() {
   485  		if r := recover(); r != nil {
   486  			rlog.Warnf("Recovered while adding new block, Stack trace below keyimage %s", keyimage)
   487  			rlog.Warnf("Stack trace  \n%s", debug.Stack())
   488  			spent = false
   489  		}
   490  	}()
   491  
   492  	if !w.GetMode() { // if wallet is in offline mode , we cannot do anything
   493  		return false
   494  	}
   495  
   496  	spent = true // default assume the funds are spent
   497  
   498  	rlog.Warnf("checking whether key image are spent in pool %s", keyimage)
   499  
   500  	var params structures.Is_Key_Image_Spent_Params
   501  	var result structures.Is_Key_Image_Spent_Result
   502  
   503  	params.Key_images = append(params.Key_images, keyimage.String())
   504  
   505  	var buf bytes.Buffer
   506  	err := json.NewEncoder(&buf).Encode(&params)
   507  	if err != nil {
   508  		return
   509  	}
   510  
   511  	// this method is NOT JSON RPC method, send raw as http request and parse response
   512  
   513  	resp, err := http.Post(fmt.Sprintf("%s/is_key_image_spent",buildurl(w.Daemon_Endpoint)), "application/json", &buf)
   514  	if err != nil {
   515  		return
   516  	}
   517  
   518  	decoder := json.NewDecoder(resp.Body)
   519  	err = decoder.Decode(&result)
   520  	if err != nil {
   521  		err = fmt.Errorf("err while decoding incoming sendrawtransaction response json err: %s", err)
   522  		return
   523  	}
   524  
   525  	if result.Status == "OK" {
   526  		if len(result.Spent_Status) == 1 && result.Spent_Status[0] >= 1 {
   527  			return true
   528  		} else {
   529  			return false // if daemon says not spent return as available
   530  		}
   531  
   532  	}
   533  
   534  	//fmt.Printf("err in response %+v", result)
   535  
   536  	return
   537  }