// GetDoubleSpends takes a transaction and compares it with // all transactions in the db. It returns a slice of all txids in the db // which are double spent by the received tx. func CheckDoubleSpends( argTx *wire.MsgTx, txs []*wire.MsgTx) ([]*wire.ShaHash, error) { var dubs []*wire.ShaHash // slice of all double-spent txs argTxid := argTx.TxSha() for _, compTx := range txs { compTxid := compTx.TxSha() // check if entire tx is dup if argTxid.IsEqual(&compTxid) { return nil, fmt.Errorf("tx %s is dup", argTxid.String()) } // not dup, iterate through inputs of argTx for _, argIn := range argTx.TxIn { // iterate through inputs of compTx for _, compIn := range compTx.TxIn { if OutPointsEqual( argIn.PreviousOutPoint, compIn.PreviousOutPoint) { // found double spend dubs = append(dubs, &compTxid) break // back to argIn loop } } } } return dubs, nil }
// AbsorbTx absorbs money into wallet from a tx func (t *TxStore) AbsorbTx(tx *wire.MsgTx) error { if tx == nil { return fmt.Errorf("Tried to add nil tx") } var hits uint32 var acq int64 // check if any of the tx's outputs match my adrs for i, out := range tx.TxOut { // in each output of tx for _, a := range t.Adrs { // compare to each adr we have // more correct would be to check for full script // contains could have false positive? (p2sh/p2pkh same hash ..?) if bytes.Contains(out.PkScript, a.ScriptAddress()) { // hit hits++ acq += out.Value var newu Utxo newu.KeyIdx = a.KeyIdx newu.Txo = *out var newop wire.OutPoint newop.Hash = tx.TxSha() newop.Index = uint32(i) newu.Op = newop t.Utxos = append(t.Utxos, newu) break } } } log.Printf("%d hits, acquired %d", hits, acq) t.Sum += acq return nil }
// TxToString prints out some info about a transaction. for testing / debugging func TxToString(tx *wire.MsgTx) string { str := fmt.Sprintf("\t - Tx %s\n", tx.TxSha().String()) for i, in := range tx.TxIn { str += fmt.Sprintf("Input %d: %s\n", i, in.PreviousOutPoint.String()) str += fmt.Sprintf("SigScript for input %d: %x\n", i, in.SignatureScript) } for i, out := range tx.TxOut { if out != nil { str += fmt.Sprintf("\toutput %d script: %x amt: %d\n", i, out.PkScript, out.Value) } else { str += fmt.Sprintf("output %d nil (WARNING)\n", i) } } return str }
func NewDetailsRecord(msg *wire.MsgTx) *DetailsRecord { record := &DetailsRecord{ hash: msg.TxSha(), ins: make([]*InputRecord, len(msg.TxIn)), outs: make([]*OutputRecord, len(msg.TxOut)), } for i, txin := range msg.TxIn { record.ins[i] = NewInputRecord(txin) } for i, txout := range msg.TxOut { record.outs[i] = NewOutputRecord(txout) } return record }
func (s *SPVCon) NewOutgoingTx(tx *wire.MsgTx) error { txid := tx.TxSha() // assign height of zero for txs we create err := s.TS.AddTxid(&txid, 0) if err != nil { return err } _, err = s.TS.Ingest(tx, 0) // our own tx; don't keep track of false positives if err != nil { return err } // make an inv message instead of a tx message to be polite iv1 := wire.NewInvVect(wire.InvTypeTx, &txid) invMsg := wire.NewMsgInv() err = invMsg.AddInvVect(iv1) if err != nil { return err } s.outMsgQueue <- invMsg return nil }
// IngestTx ingests a tx into wallet, dealing with both gains and losses func (t *TxStore) IngestTx(tx *wire.MsgTx) error { var match bool inTxid := tx.TxSha() for _, ktxid := range t.KnownTxids { if inTxid.IsEqual(ktxid) { match = true break // found tx match, } } if !match { return fmt.Errorf("we don't care about tx %s", inTxid.String()) } err := t.AbsorbTx(tx) if err != nil { return err } err = t.ExpellTx(tx) if err != nil { return err } // fmt.Printf("ingested tx %s total amt %d\n", inTxid.String(), t.Sum) return nil }
// TxHandler takes in transaction messages that come in from either a request // after an inv message or after a merkle block message. func (s *SPVCon) TxHandler(m *wire.MsgTx) { s.TS.OKMutex.Lock() height, ok := s.TS.OKTxids[m.TxSha()] s.TS.OKMutex.Unlock() if !ok { log.Printf("Tx %s unknown, will not ingest\n") return } // check for double spends allTxs, err := s.TS.GetAllTxs() if err != nil { log.Printf("Can't get txs from db: %s", err.Error()) return } dubs, err := CheckDoubleSpends(m, allTxs) if err != nil { log.Printf("CheckDoubleSpends error: %s", err.Error()) return } if len(dubs) > 0 { for i, dub := range dubs { fmt.Printf("dub %d known tx %s and new tx %s are exclusive!!!\n", i, dub.String(), m.TxSha().String()) } } hits, err := s.TS.Ingest(m, height) if err != nil { log.Printf("Incoming Tx error: %s\n", err.Error()) return } if hits == 0 && !s.HardMode { log.Printf("tx %s had no hits, filter false positive.", m.TxSha().String()) s.fPositives <- 1 // add one false positive to chan return } log.Printf("tx %s ingested and matches %d utxo/adrs.", m.TxSha().String(), hits) }
// loadTxStore returns a transaction store loaded from a file. func loadTxStore(filename string) (blockchain.TxStore, error) { // The txstore file format is: // <num tx data entries> <tx length> <serialized tx> <blk height> // <num spent bits> <spent bits> // // All num and length fields are little-endian uint32s. The spent bits // field is padded to a byte boundary. filename = filepath.Join("testdata/", filename) fi, err := os.Open(filename) if err != nil { return nil, err } // Choose read based on whether the file is compressed or not. var r io.Reader if strings.HasSuffix(filename, ".bz2") { r = bzip2.NewReader(fi) } else { r = fi } defer fi.Close() // Num of transaction store objects. var numItems uint32 if err := binary.Read(r, binary.LittleEndian, &numItems); err != nil { return nil, err } txStore := make(blockchain.TxStore) var uintBuf uint32 for height := uint32(0); height < numItems; height++ { txD := blockchain.TxData{} // Serialized transaction length. err = binary.Read(r, binary.LittleEndian, &uintBuf) if err != nil { return nil, err } serializedTxLen := uintBuf if serializedTxLen > wire.MaxBlockPayload { return nil, fmt.Errorf("Read serialized transaction "+ "length of %d is larger max allowed %d", serializedTxLen, wire.MaxBlockPayload) } // Transaction. var msgTx wire.MsgTx err = msgTx.Deserialize(r) if err != nil { return nil, err } txD.Tx = btcutil.NewTx(&msgTx) // Transaction hash. txHash, err := msgTx.TxSha() if err != nil { return nil, err } txD.Hash = &txHash // Block height the transaction came from. err = binary.Read(r, binary.LittleEndian, &uintBuf) if err != nil { return nil, err } txD.BlockHeight = int64(uintBuf) // Num spent bits. err = binary.Read(r, binary.LittleEndian, &uintBuf) if err != nil { return nil, err } numSpentBits := uintBuf numSpentBytes := numSpentBits / 8 if numSpentBits%8 != 0 { numSpentBytes++ } // Packed spent bytes. spentBytes := make([]byte, numSpentBytes) _, err = io.ReadFull(r, spentBytes) if err != nil { return nil, err } // Populate spent data based on spent bits. txD.Spent = make([]bool, numSpentBits) for byteNum, spentByte := range spentBytes { for bit := 0; bit < 8; bit++ { if uint32((byteNum*8)+bit) < numSpentBits { if spentByte&(1<<uint(bit)) != 0 { txD.Spent[(byteNum*8)+bit] = true } } } } txStore[*txD.Hash] = &txD } return txStore, nil }
// TestSort ensures the transaction sorting works according to the BIP. func TestSort(t *testing.T) { tests := []struct { name string hexFile string isSorted bool unsortedHash string sortedHash string }{ { name: "first test case from BIPLI01 - sorts inputs only, based on hash", hexFile: "li01-1.hex", isSorted: false, unsortedHash: "0a6a357e2f7796444e02638749d9611c008b253fb55f5dc88b739b230ed0c4c3", sortedHash: "839503cb611a3e3734bd521c608f881be2293ff77b7384057ab994c794fce623", }, { name: "second test case from BIPLI01 - already sorted", hexFile: "li01-2.hex", isSorted: true, unsortedHash: "28204cad1d7fc1d199e8ef4fa22f182de6258a3eaafe1bbe56ebdcacd3069a5f", sortedHash: "28204cad1d7fc1d199e8ef4fa22f182de6258a3eaafe1bbe56ebdcacd3069a5f", }, { name: "block 100001 tx[1] - sorts outputs only, based on amount", hexFile: "li01-3.hex", isSorted: false, unsortedHash: "fbde5d03b027d2b9ba4cf5d4fecab9a99864df2637b25ea4cbcb1796ff6550ca", sortedHash: "0a8c246c55f6b82f094d211f4f57167bf2ea4898741d218b09bdb2536fd8d13f", }, { name: "block 100001 tx[2] - sorts both inputs and outputs", hexFile: "li01-4.hex", isSorted: false, unsortedHash: "8131ffb0a2c945ecaf9b9063e59558784f9c3a74741ce6ae2a18d0571dac15bb", sortedHash: "a3196553b928b0b6154b002fa9a1ce875adabc486fedaaaf4c17430fd4486329", }, { name: "block 100998 tx[6] - sorts outputs only, based on output script", hexFile: "li01-5.hex", isSorted: false, unsortedHash: "ff85e8fc92e71bbc217e3ea9a3bacb86b435e52b6df0b089d67302c293a2b81d", sortedHash: "9a6c24746de024f77cac9b2138694f11101d1c66289261224ca52a25155a7c94", }, } for _, test := range tests { // Load and deserialize the test transaction. filePath := filepath.Join("testdata", test.hexFile) txHexBytes, err := ioutil.ReadFile(filePath) if err != nil { t.Errorf("ReadFile (%s): failed to read test file: %v", test.name, err) continue } txBytes, err := hex.DecodeString(string(txHexBytes)) if err != nil { t.Errorf("DecodeString (%s): failed to decode tx: %v", test.name, err) continue } var tx wire.MsgTx err = tx.Deserialize(bytes.NewReader(txBytes)) if err != nil { t.Errorf("Deserialize (%s): unexpected error %v", test.name, err) continue } // Ensure the sort order of the original transaction matches the // expected value. if got := txsort.IsSorted(&tx); got != test.isSorted { t.Errorf("IsSorted (%s): sort does not match "+ "expected - got %v, want %v", test.name, got, test.isSorted) continue } // Sort the transaction and ensure the resulting hash is the // expected value. sortedTx := txsort.Sort(&tx) if got := sortedTx.TxSha().String(); got != test.sortedHash { t.Errorf("Sort (%s): sorted hash does not match "+ "expected - got %v, want %v", test.name, got, test.sortedHash) continue } // Ensure the original transaction is not modified. if got := tx.TxSha().String(); got != test.unsortedHash { t.Errorf("Sort (%s): unsorted hash does not match "+ "expected - got %v, want %v", test.name, got, test.unsortedHash) continue } // Now sort the transaction using the mutable version and ensure // the resulting hash is the expected value. txsort.InPlaceSort(&tx) if got := tx.TxSha().String(); got != test.sortedHash { t.Errorf("SortMutate (%s): sorted hash does not match "+ "expected - got %v, want %v", test.name, got, test.sortedHash) continue } } }
// Ingest puts a tx into the DB atomically. This can result in a // gain, a loss, or no result. Gain or loss in satoshis is returned. func (ts *TxStore) Ingest(tx *wire.MsgTx, height int32) (uint32, error) { var hits uint32 var err error var spentOPs [][]byte var nUtxoBytes [][]byte // tx has been OK'd by SPV; check tx sanity utilTx := btcutil.NewTx(tx) // convert for validation // checks basic stuff like there are inputs and 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) if err != nil { return hits, err } spentOPs = append(spentOPs, nOP) } // also generate PKscripts for all addresses (maybe keep storing these?) for _, adr := range ts.Adrs { // iterate through all our addresses aPKscript, err := txscript.PayToAddrScript(adr.PkhAdr) if err != nil { return hits, err } // iterate through all outputs of this tx for i, out := range tx.TxOut { if bytes.Equal(out.PkScript, aPKscript) { // new utxo for us var newu Utxo newu.AtHeight = height newu.KeyIdx = adr.KeyIdx newu.Value = out.Value var newop wire.OutPoint newop.Hash = tx.TxSha() newop.Index = uint32(i) newu.Op = newop b, err := newu.ToBytes() if err != nil { return hits, err } nUtxoBytes = append(nUtxoBytes, b) hits++ break // only one match } } } err = ts.StateDB.Update(func(btx *bolt.Tx) error { // get all 4 buckets duf := btx.Bucket(BKTUtxos) // sta := btx.Bucket(BKTState) old := btx.Bucket(BKTStxos) txns := btx.Bucket(BKTTxns) if duf == nil || old == nil || txns == nil { return fmt.Errorf("error: db not initialized") } // first see if we lose utxos // iterate through duffel bag and look for matches // this makes us lose money, which is regrettable, but we need to know. for _, nOP := range spentOPs { duf.ForEach(func(k, v []byte) error { if bytes.Equal(k, nOP) { // matched, we lost utxo // do all this just to figure out value we lost x := make([]byte, len(k)+len(v)) copy(x, k) copy(x[len(k):], v) lostTxo, err := UtxoFromBytes(x) if err != nil { return err } hits++ // then delete the utxo from duf, save to old err = duf.Delete(k) if err != nil { return err } // after deletion, save stxo to old bucket var st Stxo // generate spent txo st.Utxo = lostTxo // assign outpoint st.SpendHeight = height // spent at height st.SpendTxid = tx.TxSha() // spent by txid stxb, err := st.ToBytes() // serialize if err != nil { return err } err = old.Put(k, stxb) // write k:v outpoint:stxo bytes if err != nil { return err } // store this relevant tx sha := tx.TxSha() var buf bytes.Buffer tx.Serialize(&buf) err = txns.Put(sha.Bytes(), buf.Bytes()) if err != nil { return err } return nil // matched utxo k, won't match another } return nil // no match }) } // done losing utxos, next gain utxos // next add all new utxos to db, this is quick as the work is above for _, ub := range nUtxoBytes { err = duf.Put(ub[:36], ub[36:]) if err != nil { return err } } return nil }) return hits, err }