diff --git a/uspv/README.md b/uspv/README.md index 44ff72d9..08cade5e 100644 --- a/uspv/README.md +++ b/uspv/README.md @@ -50,10 +50,21 @@ There's still quite a bit left, though most of it hopefully won't be too hard. Problems / still to do: * Only connects to one node, and that node is hard-coded. -* Currently doesn't check signatures of received transactions. * Re-orgs affect only headers, and don't evict confirmed transactions. * Double spends are not detected; Double spent txs will stay at height 0. * Tx creation and signing is still very rudimentary. * There may be wire-protocol irregularities which can get it kicked off. Hopefully I can get most of that list deleted soon. + +(Now sanity checks txs, but can't check sigs... because it's SPV. Right.) + +Later functionality to implement: + +* "Desktop Mode" SPV, or "Unfiltered" SPV or some other name + +This would be a mode where uspv doesn't use bloom filters and request merkle blocks, but instead grabs everything in the block and discards most of the data. This prevents nodes from learning about your utxo set. To further enhance this, it should connect to multiple nodes and relay txs and inv messages to blend in. + +* Ironman SPV + +Never request txs. Only merkleBlocks (or in above mode, blocks). No unconfirmed transactions are presented to the user, which makes a whole lot of sense as with unconfirmed SPV transactions you're relying completely on the honesty of the reporting node. \ No newline at end of file diff --git a/uspv/eight333.go b/uspv/eight333.go index 0d9bb007..eb2bbd85 100644 --- a/uspv/eight333.go +++ b/uspv/eight333.go @@ -50,6 +50,10 @@ type SPVCon struct { mBlockQueue chan HashAndHeight // fPositives is a channel to keep track of bloom filter false positives. fPositives chan int32 + + // waitState is a channel that is empty while in the header and block + // sync modes, but when in the idle state has a "true" in it. + inWaitState chan bool } func OpenSPV(remoteNode string, hfn, tsfn string, @@ -134,6 +138,7 @@ func OpenSPV(remoteNode string, hfn, tsfn string, go s.outgoingMessageHandler() s.mBlockQueue = make(chan HashAndHeight, 32) // queue depth 32 is a thing s.fPositives = make(chan int32, 4000) // a block full, approx + s.inWaitState = make(chan bool) go s.fPositiveHandler() return s, nil @@ -317,11 +322,16 @@ func (s *SPVCon) IngestMerkleBlock(m *wire.MsgMerkleBlock) error { return fmt.Errorf("Txid store error: %s\n", err.Error()) } } + err = s.TS.SetDBSyncHeight(hah.height) if err != nil { return err } + // not super comfortable with this but it seems to work. + if len(s.mBlockQueue) == 0 { // done and fully sync'd + s.inWaitState <- true + } return nil } @@ -385,7 +395,6 @@ func (s *SPVCon) IngestHeaders(m *wire.MsgHeaders) (bool, error) { if err != nil { return false, err } - // advance chain tip tip++ // check last header @@ -408,7 +417,6 @@ func (s *SPVCon) IngestHeaders(m *wire.MsgHeaders) (bool, error) { } } log.Printf("Headers to height %d OK.", tip) - return true, nil } diff --git a/uspv/msghandler.go b/uspv/msghandler.go index a6a8e0cc..8c6ecfbe 100644 --- a/uspv/msghandler.go +++ b/uspv/msghandler.go @@ -45,7 +45,7 @@ func (s *SPVCon) incomingMessageHandler() { log.Printf("Rejected! cmd: %s code: %s tx: %s reason: %s", m.Cmd, m.Code.String(), m.Hash.String(), m.Reason) case *wire.MsgInv: - go s.InvHandler(m) + s.InvHandler(m) case *wire.MsgNotFound: log.Printf("Got not found response from remote:") for i, thing := range m.InvList { @@ -162,12 +162,14 @@ func (s *SPVCon) InvHandler(m *wire.MsgInv) { s.TS.OKTxids[thing.Hash] = 0 // unconfirmed s.AskForTx(thing.Hash) } - if thing.Type == wire.InvTypeBlock { // new block, ingest - if len(s.mBlockQueue) == 0 { // this is not a good check... - // don't ask directly; instead ask for header + if thing.Type == wire.InvTypeBlock { // new block what to do? + select { + case <-s.inWaitState: + // start getting headers fmt.Printf("asking for headers due to inv block\n") s.AskForHeaders() - } else { + default: + // drop it as if its component particles had high thermal energies fmt.Printf("inv block but ignoring, not synched\n") } } diff --git a/uspv/utxodb.go b/uspv/utxodb.go index 7a3af432..07f3ee82 100644 --- a/uspv/utxodb.go +++ b/uspv/utxodb.go @@ -5,6 +5,8 @@ import ( "encoding/binary" "fmt" + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -238,7 +240,7 @@ func (ts *TxStore) Ingest(tx *wire.MsgTx) (uint32, error) { var spentOPs [][]byte var nUtxoBytes [][]byte - // check that we have a height and tx has been SPV OK'd + // first check that we have a height and tx has been SPV OK'd inTxid := tx.TxSha() height, ok := ts.OKTxids[inTxid] if !ok { @@ -246,6 +248,16 @@ func (ts *TxStore) Ingest(tx *wire.MsgTx) (uint32, error) { inTxid.String()) } + // tx has been OK'd by SPV; check tx sanity + utilTx := btcutil.NewTx(tx) // convert for validation + // checks stuff like inputs >= ouputs + err = blockchain.CheckTransactionSanity(utilTx) + if err != nil { + return hits, err + } + // note that you can't check signatures; this is SPV. + // 0 conf SPV means pretty much nothing. Anyone can say anything. + // before entering into db, serialize all inputs of the ingested tx for _, txin := range tx.TxIn { nOP, err := outPointToBytes(&txin.PreviousOutPoint)