From 624e776987e3b8c199b74013f22e3e1e32bcb9bb Mon Sep 17 00:00:00 2001 From: Tadge Dryja Date: Tue, 2 Feb 2016 23:37:29 -0800 Subject: [PATCH] add shell spv testing mode To try uspv, do ./lnd -spv The remote node is hardcoded in shell.go. If you aren't running a full node on localhost, specify where to connect to. Nearby nodes will be much faster but random testnet nodes on the internet should also work. --- .gitignore | 4 + cmd/lnshell/commands.go | 4 +- lnd.go | 6 + shell.go | 328 ++++++++++++++++++++++++++++++++++++++++ uspv/eight333.go | 26 ++-- uspv/msghandler.go | 4 +- uspv/txstore.go | 6 +- 7 files changed, 358 insertions(+), 20 deletions(-) create mode 100644 shell.go diff --git a/.gitignore b/.gitignore index feb149c7..b55095c7 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,7 @@ test_wal/* # vim **.swp + +*.hex +*.db +*.bin diff --git a/cmd/lnshell/commands.go b/cmd/lnshell/commands.go index a883c9be..dc4e1506 100644 --- a/cmd/lnshell/commands.go +++ b/cmd/lnshell/commands.go @@ -22,8 +22,8 @@ func RpcConnect(args []string) error { } fmt.Printf("connection state: %s\n", state.String()) time.Sleep(time.Second * 2) - // lnClient := lnrpc.NewLightningClient(conn) - // lnClient.NewAddress(nil, nil, nil) // crashes + // lnClient := lnrpc.NewLightningClient(conn) + // lnClient.NewAddress(nil, nil, nil) // crashes state, err = conn.State() if err != nil { diff --git a/lnd.go b/lnd.go index afcc516f..5751379b 100644 --- a/lnd.go +++ b/lnd.go @@ -21,11 +21,17 @@ var ( rpcPort = flag.Int("rpcport", 10009, "The port for the rpc server") peerPort = flag.String("peerport", "10011", "The port to listen on for incoming p2p connections") dataDir = flag.String("datadir", "test_wal", "The directory to store lnd's data within") + spvMode = flag.Bool("spv", false, "assert to enter spv wallet mode") ) func main() { flag.Parse() + if *spvMode == true { + shell() + return + } + go func() { listenAddr := net.JoinHostPort("", "5009") profileRedirect := http.RedirectHandler("/debug/pprof", diff --git a/shell.go b/shell.go new file mode 100644 index 00000000..5602a18c --- /dev/null +++ b/shell.go @@ -0,0 +1,328 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "log" + "os" + "strconv" + "strings" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + + "github.com/lightningnetwork/lnd/uspv" +) + +/* this is a CLI shell for testing out LND. Right now it's only for uspv +testing. It can send and receive coins. +*/ + +const ( + keyFileName = "testkey.hex" + headerFileName = "headers.bin" + dbFileName = "utxo.db" + // this is my local testnet node, replace it with your own close by. + // Random internet testnet nodes usually work but sometimes don't, so + // maybe I should test against different versions out there. + SPVHostAdr = "127.0.0.1:18333" +) + +var ( + Params = &chaincfg.TestNet3Params + SCon uspv.SPVCon // global here for now +) + +func shell() { + fmt.Printf("LND spv shell v0.0\n") + fmt.Printf("Not yet well integrated, but soon.\n") + + // read key file (generate if not found) + rootPriv, err := uspv.ReadKeyFileToECPriv(keyFileName, Params) + if err != nil { + log.Fatal(err) + } + // setup TxStore first (before spvcon) + Store := uspv.NewTxStore(rootPriv, Params) + // setup spvCon + + SCon, err = uspv.OpenSPV( + SPVHostAdr, headerFileName, dbFileName, &Store, Params) + if err != nil { + log.Fatal(err) + } + + tip, err := SCon.TS.GetDBSyncHeight() // ask for sync height + if err != nil { + log.Fatal(err) + } + if tip == 0 { // DB has never been used, set to birthday + tip = 675000 // hardcoded; later base on keyfile date? + err = SCon.TS.SetDBSyncHeight(tip) + if err != nil { + log.Fatal(err) + } + } + + // once we're connected, initiate headers sync + err = Hdr() + if err != nil { + log.Fatal(err) + } + + // main shell loop + for { + // setup reader with max 4K input chars + reader := bufio.NewReaderSize(os.Stdin, 4000) + fmt.Printf("LND# ") // prompt + msg, err := reader.ReadString('\n') // input finishes on enter key + if err != nil { + log.Fatal(err) + } + + cmdslice := strings.Fields(msg) // chop input up on whitespace + if len(cmdslice) < 1 { + continue // no input, just prompt again + } + fmt.Printf("entered command: %s\n", msg) // immediate feedback + err = Shellparse(cmdslice) + if err != nil { // only error should be user exit + log.Fatal(err) + } + } + return +} + +// Shellparse parses user input and hands it to command functions if matching +func Shellparse(cmdslice []string) error { + var err error + var args []string + cmd := cmdslice[0] + if len(cmdslice) > 1 { + args = cmdslice[1:] + } + if cmd == "exit" || cmd == "quit" { + return fmt.Errorf("User exit") + } + + // help gives you really terse help. Just a list of commands. + if cmd == "help" { + err = Help(args) + if err != nil { + fmt.Printf("help error: %s\n", err) + } + return nil + } + + // adr generates a new address and displays it + if cmd == "adr" { + err = Adr(args) + if err != nil { + fmt.Printf("adr error: %s\n", err) + } + return nil + } + + // bal shows the current set of utxos, addresses and score + if cmd == "bal" { + err = Bal(args) + if err != nil { + fmt.Printf("bal error: %s\n", err) + } + return nil + } + + // send sends coins to the address specified + if cmd == "send" { + err = Send(args) + if err != nil { + fmt.Printf("send error: %s\n", err) + } + return nil + } + + fmt.Printf("Command not recognized. type help for command list.\n") + return nil +} + +// Hdr asks for headers. +func Hdr() error { + if SCon.RBytes == 0 { + return fmt.Errorf("No SPV connection, can't get headers.") + } + err := SCon.AskForHeaders() + if err != nil { + return err + } + return nil +} + +// Bal prints out your score. +func Bal(args []string) error { + if SCon.TS == nil { + return fmt.Errorf("Can't get balance, spv connection broken") + } + fmt.Printf(" ----- Account Balance ----- \n") + allUtxos, err := SCon.TS.GetAllUtxos() + if err != nil { + return err + } + var score int64 + for i, u := range allUtxos { + fmt.Printf("\tutxo %d height %d %s key: %d amt %d\n", + i, u.AtHeight, u.Op.String(), u.KeyIdx, u.Value) + score += u.Value + } + height, _ := SCon.TS.GetDBSyncHeight() + + for i, a := range SCon.TS.Adrs { + fmt.Printf("address %d %s\n", i, a.PkhAdr.String()) + } + fmt.Printf("Total known utxos: %d\n", len(allUtxos)) + fmt.Printf("Total spendable coin: %d\n", score) + fmt.Printf("DB sync height: %d\n", height) + return nil +} + +// Adr makes a new address. +func Adr(args []string) error { + a, err := SCon.TS.NewAdr() + if err != nil { + return err + } + fmt.Printf("made new address %s, %d addresses total\n", + a.String(), len(SCon.TS.Adrs)) + + return nil +} + +// Send sends coins. +func Send(args []string) error { + // get all utxos from the database + allUtxos, err := SCon.TS.GetAllUtxos() + if err != nil { + return err + } + var score int64 // score is the sum of all utxo amounts. highest score wins. + // add all the utxos up to get the score + for _, u := range allUtxos { + score += u.Value + } + + // score is 0, cannot unlock 'send coins' acheivement + if score == 0 { + return fmt.Errorf("You don't have money. Work hard.") + } + // need args, fail + if len(args) < 2 { + return fmt.Errorf("need args: ssend amount(satoshis) address") + } + amt, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return err + } + if amt < 1000 { + return fmt.Errorf("can't send %d, too small", amt) + } + adr, err := btcutil.DecodeAddress(args[1], SCon.TS.Param) + if err != nil { + fmt.Printf("error parsing %s as address\t", args[1]) + return err + } + fmt.Printf("send %d to address: %s \n", + amt, adr.String()) + err = SendCoins(SCon, adr, amt) + if err != nil { + return err + } + return nil +} + +// SendCoins does send coins, but it's very rudimentary +func SendCoins(s uspv.SPVCon, adr btcutil.Address, sendAmt int64) error { + var err error + var score int64 + allUtxos, err := s.TS.GetAllUtxos() + if err != nil { + return err + } + + for _, utxo := range allUtxos { + score += utxo.Value + } + // important rule in bitcoin, output total > input total is invalid. + if sendAmt > score { + return fmt.Errorf("trying to send %d but %d available.", + sendAmt, score) + } + + tx := wire.NewMsgTx() // make new tx + // make address script 76a914...88ac + adrScript, err := txscript.PayToAddrScript(adr) + if err != nil { + return err + } + // make user specified txout and add to tx + txout := wire.NewTxOut(sendAmt, adrScript) + tx.AddTxOut(txout) + + nokori := sendAmt // nokori is how much is needed on input side + for _, utxo := range allUtxos { + // generate pkscript to sign + prevPKscript, err := txscript.PayToAddrScript( + s.TS.Adrs[utxo.KeyIdx].PkhAdr) + if err != nil { + return err + } + // make new input from this utxo + thisInput := wire.NewTxIn(&utxo.Op, prevPKscript) + tx.AddTxIn(thisInput) + nokori -= utxo.Value + if nokori < -10000 { // minimum overage / fee is 1K now + break + } + } + // there's enough left to make a change output + if nokori < -200000 { + change, err := s.TS.NewAdr() + if err != nil { + return err + } + + changeScript, err := txscript.PayToAddrScript(change) + if err != nil { + return err + } + changeOut := wire.NewTxOut((-100000)-nokori, changeScript) + tx.AddTxOut(changeOut) + } + + // use txstore method to sign + err = s.TS.SignThis(tx) + if err != nil { + return err + } + + fmt.Printf("tx: %s", uspv.TxToString(tx)) + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + tx.Serialize(buf) + fmt.Printf("tx: %x\n", buf.Bytes()) + + // send it out on the wire. hope it gets there. + // we should deal with rejects. Don't yet. + err = s.PushTx(tx) + if err != nil { + return err + } + return nil +} + +func Help(args []string) error { + fmt.Printf("commands:\n") + fmt.Printf("help adr bal send exit\n") + return nil +} diff --git a/uspv/eight333.go b/uspv/eight333.go index 26dd0a96..a757c4ab 100644 --- a/uspv/eight333.go +++ b/uspv/eight333.go @@ -43,8 +43,7 @@ type SPVCon struct { WBytes uint64 // total bytes written RBytes uint64 // total bytes read - TS *TxStore // transaction store to write to - param *chaincfg.Params // network parameters (testnet3, testnetL) + TS *TxStore // transaction store to write to // mBlockQueue is for keeping track of what height we've requested. mBlockQueue chan HashAndHeight @@ -56,16 +55,17 @@ type SPVCon struct { inWaitState chan bool } -func OpenSPV(remoteNode string, hfn, tsfn string, +func OpenSPV(remoteNode string, hfn, dbfn string, inTs *TxStore, p *chaincfg.Params) (SPVCon, error) { // create new SPVCon var s SPVCon - // assign network parameters to SPVCon - s.param = p + // I should really merge SPVCon and TxStore, they're basically the same + inTs.Param = p + s.TS = inTs // copy pointer of txstore into spvcon // open header file - err := s.openHeaderFile(headerFileName) + err := s.openHeaderFile(hfn) if err != nil { return s, err } @@ -80,12 +80,10 @@ func OpenSPV(remoteNode string, hfn, tsfn string, s.localVersion = VERSION // transaction store for this SPV connection - inTs.Param = p - err = inTs.OpenDB(tsfn) + err = inTs.OpenDB(dbfn) if err != nil { return s, err } - s.TS = inTs // copy pointer of txstore into spvcon myMsgVer, err := wire.NewMsgVersionFromConn(s.con, 0, 0) if err != nil { @@ -99,7 +97,7 @@ func OpenSPV(remoteNode string, hfn, tsfn string, myMsgVer.AddService(wire.SFNodeBloom) // this actually sends - n, err := wire.WriteMessageN(s.con, myMsgVer, s.localVersion, s.param.Net) + n, err := wire.WriteMessageN(s.con, myMsgVer, s.localVersion, s.TS.Param.Net) if err != nil { return s, err } @@ -107,7 +105,7 @@ func OpenSPV(remoteNode string, hfn, tsfn string, log.Printf("wrote %d byte version message to %s\n", n, s.con.RemoteAddr().String()) - n, m, b, err := wire.ReadMessageN(s.con, s.localVersion, s.param.Net) + n, m, b, err := wire.ReadMessageN(s.con, s.localVersion, s.TS.Param.Net) if err != nil { return s, err } @@ -126,7 +124,7 @@ func OpenSPV(remoteNode string, hfn, tsfn string, s.remoteHeight = mv.LastBlock mva := wire.NewMsgVerAck() - n, err = wire.WriteMessageN(s.con, mva, s.localVersion, s.param.Net) + n, err = wire.WriteMessageN(s.con, mva, s.localVersion, s.TS.Param.Net) if err != nil { return s, err } @@ -149,7 +147,7 @@ func (s *SPVCon) openHeaderFile(hfn string) error { if err != nil { if os.IsNotExist(err) { var b bytes.Buffer - err = s.param.GenesisBlock.Header.Serialize(&b) + err = s.TS.Param.GenesisBlock.Header.Serialize(&b) if err != nil { return err } @@ -405,7 +403,7 @@ func (s *SPVCon) IngestHeaders(m *wire.MsgHeaders) (bool, error) { // advance chain tip tip++ // check last header - worked := CheckHeader(s.headerFile, tip, s.param) + worked := CheckHeader(s.headerFile, tip, s.TS.Param) if !worked { if endPos < 8080 { // jeez I give up, back to genesis diff --git a/uspv/msghandler.go b/uspv/msghandler.go index 5b648e0a..926ae5da 100644 --- a/uspv/msghandler.go +++ b/uspv/msghandler.go @@ -10,7 +10,7 @@ import ( func (s *SPVCon) incomingMessageHandler() { for { - n, xm, _, err := wire.ReadMessageN(s.con, s.localVersion, s.param.Net) + n, xm, _, err := wire.ReadMessageN(s.con, s.localVersion, s.TS.Param.Net) if err != nil { log.Printf("ReadMessageN error. Disconnecting: %s\n", err.Error()) return @@ -64,7 +64,7 @@ func (s *SPVCon) incomingMessageHandler() { func (s *SPVCon) outgoingMessageHandler() { for { msg := <-s.outMsgQueue - n, err := wire.WriteMessageN(s.con, msg, s.localVersion, s.param.Net) + n, err := wire.WriteMessageN(s.con, msg, s.localVersion, s.TS.Param.Net) if err != nil { log.Printf("Write message error: %s", err.Error()) } diff --git a/uspv/txstore.go b/uspv/txstore.go index 505586ff..a5062a3b 100644 --- a/uspv/txstore.go +++ b/uspv/txstore.go @@ -20,7 +20,8 @@ type TxStore struct { Adrs []MyAdr // endeavouring to acquire capital StateDB *bolt.DB // place to write all this down - // this is redundant with the SPVCon param... ugly and should be taken out + + // Params live here, not SCon Param *chaincfg.Params // network parameters (testnet3, testnetL) // From here, comes everything. It's a secret to everybody. @@ -52,9 +53,10 @@ type MyAdr struct { // an address I have the private key for // inside the Adrs slice, right? leave for now } -func NewTxStore(rootkey *hdkeychain.ExtendedKey) TxStore { +func NewTxStore(rootkey *hdkeychain.ExtendedKey, p *chaincfg.Params) TxStore { var txs TxStore txs.rootPrivKey = rootkey + txs.Param = p txs.OKTxids = make(map[wire.ShaHash]int32) return txs }