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.
This commit is contained in:
Tadge Dryja 2016-02-02 23:37:29 -08:00
parent 2f9fc87636
commit 624e776987
7 changed files with 358 additions and 20 deletions

4
.gitignore vendored
View File

@ -37,3 +37,7 @@ test_wal/*
# vim
**.swp
*.hex
*.db
*.bin

View File

@ -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 {

6
lnd.go
View File

@ -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",

328
shell.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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())
}

View File

@ -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
}