github.com/mit-dci/lit@v0.0.0-20221102210550-8c3d3b49f2ce/powless/powless.go (about) 1 package powless 2 3 import ( 4 "bytes" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/mit-dci/lit/bech32" 16 "github.com/mit-dci/lit/coinparam" 17 "github.com/mit-dci/lit/lnutil" 18 "github.com/mit-dci/lit/logging" 19 "github.com/mit-dci/lit/wire" 20 "golang.org/x/net/proxy" 21 ) 22 23 // powless is a couple steps below uspv in that it doesn't check 24 // proof of work. It just asks some web API thing about if it has 25 // received money or not. 26 27 /* 28 Powless is a package to hook up a lit wallit to a block explorer. 29 The idea is that it's proof-of-work-less, in that there isn't any, which is 30 not great :( 31 32 Here's the API we're building for 33 https://github.com/gertjaap/blockchain-indexer 34 35 Soon we'll try to put in some merkle proofs and header chains, so there will be 36 less PoW, but still some, and the name still works. That's forwards-compatibility! 37 38 Basic way to works is that powless implements the chainhook interface: a wallit 39 starts a powless with Start() and then can register addresses and outpoints 40 with the register() calls. Powless can respond via 2 channels, the 41 TxUpToWallit channel and the CurrentHeightChan, which send transactions and 42 heights respectively. 43 44 The way it gets the data is from the indexer API calls, addressTxosSince and 45 outpointSpend. addressTxosSince gives us new coins, and outpointSpend takes 46 them away. 47 48 Next up: verify proof of work. Have all (? or just addressTxosSince?) return 49 merkle branches up to a header, and then a few headers after that (maybe 10?) 50 Then also have a hardcoded minimum block work to compare against. The powless 51 client will then verify the branch up to the header, and the short chain of 52 headers with work greater than minWork. This is weaker than SPV, but does 53 make it so the indexer has to do a bunch of work in order to send an invalid 54 transaction. 55 56 */ 57 58 /* 59 implement this: 60 61 ChainHook interface 62 63 Start(height int32, host, path string, params *coinparam.Params) ( 64 chan lnutil.TxAndHeight, chan int32, error) 65 66 RegisterAddress(address [20]byte) error 67 RegisterOutPoint(wire.OutPoint) error 68 69 PushTx(tx *wire.MsgTx) error 70 71 RawBlocks() chan *wire.MsgBlock 72 73 */ 74 75 // APILink is a link to a web API that can tell you about blockchain data. 76 type APILink struct { 77 apiUrl string 78 proxyURL string 79 80 // TrackingAdrs and OPs are slices of addresses and outpoints to watch for. 81 // Using struct{} saves a byte of RAM but is ugly so I'll use bool. 82 TrackingAdrs map[[20]byte]bool 83 TrackingAdrsMtx sync.Mutex 84 85 TrackingOPs map[wire.OutPoint]bool 86 TrackingOPsMtx sync.Mutex 87 88 TxUpToWallit chan lnutil.TxAndHeight 89 90 CurrentHeightChan chan int32 91 92 // Might as well add this in here too 93 HeightDistribute []chan int32 94 95 // we've "synced" up to this height; older txs won't get pushed up to wallit 96 height int32 97 98 // this is the hash on the tip of the chain; if it changes, we need to update 99 tipBlockHash string 100 101 // time based polling 102 dirtyChan chan interface{} 103 104 client http.Client 105 106 p *coinparam.Params 107 } 108 109 // Start starts the APIlink 110 func (a *APILink) Start( 111 startHeight int32, host, path string, proxyURL string, params *coinparam.Params) ( 112 chan lnutil.TxAndHeight, chan int32, error) { 113 114 a.proxyURL = proxyURL 115 116 if proxyURL != "" { 117 dialer, err := proxy.SOCKS5("tcp", proxyURL, nil, proxy.Direct) 118 if err != nil { 119 return nil, nil, err 120 } 121 122 a.client.Transport = &http.Transport{ 123 Dial: dialer.Dial, 124 } 125 } 126 127 // later, use params to detect which api to connect to 128 a.p = params 129 130 a.TrackingAdrs = make(map[[20]byte]bool) 131 a.TrackingOPs = make(map[wire.OutPoint]bool) 132 133 a.TxUpToWallit = make(chan lnutil.TxAndHeight, 1) 134 a.CurrentHeightChan = make(chan int32, 1) 135 136 a.dirtyChan = make(chan interface{}, 100) 137 138 a.height = startHeight 139 a.apiUrl = host 140 141 go a.DirtyCheckLoop() 142 go a.TipRefreshLoop() 143 144 return a.TxUpToWallit, a.CurrentHeightChan, nil 145 } 146 147 // PushTx for indexer 148 func (a *APILink) PushTx(tx *wire.MsgTx) error { 149 if tx == nil { 150 return fmt.Errorf("tx is nil") 151 } 152 var b bytes.Buffer 153 154 err := tx.Serialize(&b) 155 if err != nil { 156 return err 157 } 158 159 txHexString := fmt.Sprintf("%x", b.Bytes()) 160 161 // guess I just put the bytes as the body...? 162 163 apiurl := a.apiUrl + "sendRawTransaction" 164 165 response, err := 166 a.client.Post(apiurl, "text/plain", bytes.NewBuffer([]byte(txHexString))) 167 if err != nil { 168 return err 169 } 170 logging.Infof("respo nse: %s", response.Status) 171 _, err = io.Copy(os.Stdout, response.Body) 172 173 return err 174 } 175 176 // RegisterAddress gets a 20 byte address from the wallit and starts 177 // watching for utxos at that address. 178 func (a *APILink) RegisterAddress(adr160 [20]byte) error { 179 logging.Infof("register %x\n", adr160) 180 a.TrackingAdrsMtx.Lock() 181 a.TrackingAdrs[adr160] = true 182 a.TrackingAdrsMtx.Unlock() 183 a.dirtyChan <- nil 184 logging.Infof("Register %x complete\n", adr160) 185 186 return nil 187 } 188 189 // RegisterOutPoint gets an outpoint from the wallit and starts looking 190 // for txins that spend it. 191 func (a *APILink) RegisterOutPoint(op wire.OutPoint) error { 192 logging.Infof("register %s\n", op.String()) 193 a.TrackingOPsMtx.Lock() 194 a.TrackingOPs[op] = true 195 a.TrackingOPsMtx.Unlock() 196 197 a.dirtyChan <- nil 198 logging.Infof("Register %s complete\n", op.String()) 199 return nil 200 } 201 202 // UnregisterOutPoint stops watching an outpoint for spends. 203 func (a *APILink) UnregisterOutPoint(op wire.OutPoint) error { 204 logging.Infof("unregister %s\n", op.String()) 205 a.TrackingOPsMtx.Lock() 206 delete(a.TrackingOPs, op) 207 a.TrackingOPsMtx.Unlock() 208 209 a.dirtyChan <- nil 210 logging.Infof("Unregister %s complete\n", op.String()) 211 return nil 212 } 213 214 // DirtyCheckLoop checks with the server once things have changed on the client end. 215 // this is actually a bit ugly because it checks *everything* when *anything* has 216 // changed. It could be much more efficient if, eg it checks for a newly created 217 // address by itself. 218 func (a *APILink) DirtyCheckLoop() { 219 220 for { 221 // wait here until something marks the state as dirty 222 logging.Infof("Waiting for dirt...\n") 223 <-a.dirtyChan 224 225 logging.Infof("Dirt detected\n") 226 227 err := a.GetVAdrTxos() 228 if err != nil { 229 logging.Errorf(err.Error()) 230 } 231 err = a.GetVOPTxs() 232 if err != nil { 233 logging.Errorf(err.Error()) 234 } 235 236 // probably clean, empty it out to prevent cascades 237 for len(a.dirtyChan) > 0 { 238 <-a.dirtyChan 239 } 240 } 241 } 242 243 // VBlocksResponse is a list of Vblocks, which comes back from the /blocks 244 // query to the indexer 245 type VBlocksResponse []VBlock 246 247 // VBlock is the json data that comes back from the /blocks query to the indexer 248 type VBlock struct { 249 Height int32 250 Hash string 251 Time int64 252 TxLength int32 253 Size int32 254 PoolInfo string 255 } 256 257 // TipRefreshLoop checks for a change in the highest block hash 258 // if so it sets the dirty flag to true to trigger a refresh 259 func (a *APILink) TipRefreshLoop() error { 260 for { 261 // Fetch the current highest block 262 apiurl := a.apiUrl + "blocks?limit=1" 263 264 response, err := a.client.Get(apiurl) 265 if err != nil { 266 return err 267 } 268 269 var blockjsons VBlocksResponse 270 err = json.NewDecoder(response.Body).Decode(&blockjsons) 271 if err != nil { 272 return err 273 } 274 275 // only height needed here? 276 if blockjsons[0].Hash != a.tipBlockHash && 277 blockjsons[0].Height > a.height { 278 279 a.tipBlockHash = blockjsons[0].Hash 280 a.UpdateHeight(blockjsons[0].Height) 281 a.dirtyChan <- nil 282 } 283 284 logging.Infof("blockchain tip %v\n", a.tipBlockHash) 285 286 time.Sleep(time.Second * 60) 287 } 288 } 289 290 // do you even need a struct here..? 291 type RawTxResponse struct { 292 RawTx string 293 } 294 295 type VRawResponse []VRawTx 296 297 type VRawTx struct { 298 Height int32 299 Spender string 300 Tx string 301 } 302 303 // GetVAdrTxos gets new utxos for the wallet from the indexer. 304 func (a *APILink) GetVAdrTxos() error { 305 306 apitxourl := a.apiUrl + "addressTxosSince/" 307 308 var urls []string 309 a.TrackingAdrsMtx.Lock() 310 for adr160, _ := range a.TrackingAdrs { 311 // make the bech32 segwit address 312 adrBch, err := bech32.SegWitV0Encode(a.p.Bech32Prefix, adr160[:]) 313 if err != nil { 314 return err 315 } 316 // make the old base58 address 317 adr58 := lnutil.OldAddressFromPKH(adr160, a.p.PubKeyHashAddrID) 318 319 // make request URLs for both 320 urls = append(urls, 321 fmt.Sprintf("%s%d/%s%s", apitxourl, a.height, adrBch, "?raw=1")) 322 urls = append(urls, 323 fmt.Sprintf("%s%d/%s%s", apitxourl, a.height, adr58, "?raw=1")) 324 } 325 a.TrackingAdrsMtx.Unlock() 326 327 logging.Infof("have %d adr urls to check\n", len(urls)) 328 329 // make an API call for every adr in adrs 330 // then grab the tx hex, decode and send up to the wallit 331 for _, url := range urls { 332 logging.Infof("Requesting adr %s\n", url) 333 response, err := a.client.Get(url) 334 if err != nil { 335 return err 336 } 337 338 // bd, err := ioutil.ReadAll(response.Body) 339 // if err != nil { 340 // return err 341 // } 342 343 // logging.Infof(string(bd)) 344 345 var txojsons VRawResponse 346 347 err = json.NewDecoder(response.Body).Decode(&txojsons) 348 if err != nil { 349 return err 350 } 351 352 for _, txjson := range txojsons { 353 // if there's some text in the spender field, skip this as it's already gone 354 // also, really, this shouldn't be returned by the API at all because 355 // who cares how much money we used to have. 356 if len(txjson.Spender) > 32 { 357 continue 358 } 359 txBytes, err := hex.DecodeString(txjson.Tx) 360 if err != nil { 361 return err 362 } 363 buf := bytes.NewBuffer(txBytes) 364 tx := wire.NewMsgTx() 365 err = tx.Deserialize(buf) 366 if err != nil { 367 return err 368 } 369 370 var txah lnutil.TxAndHeight 371 txah.Height = int32(txjson.Height) 372 txah.Tx = tx 373 374 logging.Infof("tx %s at height %d", txah.Tx.TxHash().String(), txah.Height) 375 // send the tx and height back up to the wallit 376 a.TxUpToWallit <- txah 377 logging.Infof("sent\n") 378 } 379 } 380 logging.Infof("GetVAdrTxos complete\n") 381 return nil 382 } 383 384 // VSpendResponse is the JSON response from the / outpointSpend call 385 type VSpendResponse struct { 386 Error bool 387 Spender string 388 SpenderRaw string 389 Spent bool 390 } 391 392 func (a *APILink) GetVOPTxs() error { 393 apitxourl := a.apiUrl + "outpointSpend/" 394 395 var oplist []wire.OutPoint 396 397 // copy registered ops here to minimize time mutex is locked 398 a.TrackingOPsMtx.Lock() 399 for op, checking := range a.TrackingOPs { 400 if checking { 401 oplist = append(oplist, op) 402 } 403 } 404 a.TrackingOPsMtx.Unlock() 405 406 // need to query each txid with a different http request 407 for _, op := range oplist { 408 logging.Infof("asking for %s\n", op.String()) 409 // get full tx info for the outpoint's tx 410 // (if we have 2 outpoints with the same txid we query twice...) 411 opstring := op.String() 412 opstring = strings.Replace(opstring, ";", "/", 1) 413 response, err := a.client.Get(apitxourl + opstring + "?raw=1") 414 if err != nil { 415 return err 416 } 417 418 var txr VSpendResponse 419 // parse the response to get the spending txid 420 err = json.NewDecoder(response.Body).Decode(&txr) 421 if err != nil || txr.Error { 422 logging.Errorf("json decode error; op %s not found\n", op.String()) 423 continue 424 } 425 426 // see if this utxo is spent 427 if txr.Spent { 428 429 // if so, decode the tx indicated and give it to the wallit. 430 txBytes, err := hex.DecodeString(txr.SpenderRaw) 431 if err != nil { 432 return err 433 } 434 buf := bytes.NewBuffer(txBytes) 435 tx := wire.NewMsgTx() 436 err = tx.Deserialize(buf) 437 if err != nil { 438 return err 439 } 440 441 var txah lnutil.TxAndHeight 442 txah.Tx = tx 443 // don't know height from returned data, assume it's current height 444 // which could be wrong, gotta fix this. 445 // TODO 446 txah.Height = a.height 447 a.TxUpToWallit <- txah 448 449 // assume you no longer need to monitor this outpoint, 450 // because it's gone, and you just told the wallit how it disappeared 451 a.TrackingOPsMtx.Lock() 452 // mark this outpoint as not checked. It stays in ram though. 453 a.TrackingOPs[op] = false 454 a.TrackingOPsMtx.Unlock() 455 } 456 // don't need per-txout check here; the outpoint itself is spent 457 } 458 459 return nil 460 } 461 462 // VGetRawTx is a helper function to get a tx from the indexer 463 func (a *APILink) VGetRawTx(txid string) (*wire.MsgTx, error) { 464 rawTxURL := a.apiUrl + "getTransaction/" 465 response, err := a.client.Get(rawTxURL + txid) 466 if err != nil { 467 return nil, err 468 } 469 470 var rtx RawTxResponse 471 472 err = json.NewDecoder(response.Body).Decode(&rtx) 473 if err != nil { 474 return nil, err 475 } 476 477 txBytes, err := hex.DecodeString(rtx.RawTx) 478 if err != nil { 479 return nil, err 480 } 481 buf := bytes.NewBuffer(txBytes) 482 tx := wire.NewMsgTx() 483 err = tx.Deserialize(buf) 484 if err != nil { 485 return nil, err 486 } 487 488 return tx, nil 489 } 490 491 // UpdateHeight updates APILink height variables and channels after the 492 // height supposedly changed. 493 func (a *APILink) UpdateHeight(height int32) { 494 // if it's an increment (note reorgs are still... not a thing yet) 495 if height > a.height { 496 // update internal height 497 a.height = height 498 499 // CurrentHeightChan is "How we tell the wallet that a block has come in" 500 // so I guess this applies here as well. 501 for i := range a.HeightDistribute { 502 a.HeightDistribute[i] <- height 503 } 504 505 // send that back up to the wallit 506 a.CurrentHeightChan <- height 507 } 508 } 509 510 // RawBlocks returns a dummy channel for now 511 func (a *APILink) RawBlocks() chan *wire.MsgBlock { 512 // dummy channel for now 513 return make(chan *wire.MsgBlock, 1) 514 } 515 516 // NewRawBlocksChannel returns a dummy channel that does nothing. In the 517 // future make this more like NewHeightChannel if that feature is going to 518 // be in APILink. Included to satisfy ChainHook interface. 519 func (a *APILink) NewRawBlocksChannel() chan *wire.MsgBlock { 520 // dummy channel for now 521 return make(chan *wire.MsgBlock, 1) 522 } 523 524 // NewHeightChannel creates a new channel that should be pushed to whenever the 525 // height changes. If multiple things have separate or obscured handlers which 526 // require the height, use this. 527 func (a *APILink) NewHeightChannel() chan int32 { 528 heightDistChan := make(chan int32, 1) 529 a.HeightDistribute = append(a.HeightDistribute, heightDistChan) 530 return heightDistChan 531 }