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  }