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(¶ms) 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(¶ms) 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 }