// Copyright (c) 2013-2017 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package wtxmgr import ( "bytes" "encoding/hex" "io/ioutil" "os" "path/filepath" "testing" "time" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" "github.com/lightningnetwork/lnd/clock" ) // Received transaction output for mainnet outpoint // 61d3696de4c888730cbe06b0ad8ecb6d72d6108e893895aa9bc067bd7eba3fad:0 var ( TstRecvSerializedTx, _ = hex.DecodeString("010000000114d9ff358894c486b4ae11c2a8cf7851b1df64c53d2e511278eff17c22fb7373000000008c493046022100995447baec31ee9f6d4ec0e05cb2a44f6b817a99d5f6de167d1c75354a946410022100c9ffc23b64d770b0e01e7ff4d25fbc2f1ca8091053078a247905c39fce3760b601410458b8e267add3c1e374cf40f1de02b59213a82e1d84c2b94096e22e2f09387009c96debe1d0bcb2356ffdcf65d2a83d4b34e72c62eccd8490dbf2110167783b2bffffffff0280969800000000001976a914479ed307831d0ac19ebc5f63de7d5f1a430ddb9d88ac38bfaa00000000001976a914dadf9e3484f28b385ddeaa6c575c0c0d18e9788a88ac00000000") TstRecvTx, _ = btcutil.NewTxFromBytes(TstRecvSerializedTx) TstRecvTxSpendingTxBlockHash, _ = chainhash.NewHashFromStr("00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") TstRecvAmt = int64(10000000) TstRecvTxBlockDetails = &BlockMeta{ Block: Block{Hash: *TstRecvTxSpendingTxBlockHash, Height: 276425}, Time: time.Unix(1387737310, 0), } TstRecvCurrentHeight = int32(284498) // mainnet blockchain height at time of writing TstRecvTxOutConfirms = 8074 // hardcoded number of confirmations given the above block height TstSpendingSerializedTx, _ = hex.DecodeString("0100000003ad3fba7ebd67c09baa9538898e10d6726dcb8eadb006be0c7388c8e46d69d361000000006b4830450220702c4fbde5532575fed44f8d6e8c3432a2a9bd8cff2f966c3a79b2245a7c88db02210095d6505a57e350720cb52b89a9b56243c15ddfcea0596aedc1ba55d9fb7d5aa0012103cccb5c48a699d3efcca6dae277fee6b82e0229ed754b742659c3acdfed2651f9ffffffffdbd36173f5610e34de5c00ed092174603761595d90190f790e79cda3e5b45bc2010000006b483045022000fa20735e5875e64d05bed43d81b867f3bd8745008d3ff4331ef1617eac7c44022100ad82261fc57faac67fc482a37b6bf18158da0971e300abf5fe2f9fd39e107f58012102d4e1caf3e022757512c204bf09ff56a9981df483aba3c74bb60d3612077c9206ffffffff65536c9d964b6f89b8ef17e83c6666641bc495cb27bab60052f76cd4556ccd0d040000006a473044022068e3886e0299ffa69a1c3ee40f8b6700f5f6d463a9cf9dbf22c055a131fc4abc02202b58957fe19ff1be7a84c458d08016c53fbddec7184ac5e633f2b282ae3420ae012103b4e411b81d32a69fb81178a8ea1abaa12f613336923ee920ffbb1b313af1f4d2ffffffff02ab233200000000001976a91418808b2fbd8d2c6d022aed5cd61f0ce6c0a4cbb688ac4741f011000000001976a914f081088a300c80ce36b717a9914ab5ec8a7d283988ac00000000") TstSpendingTx, _ = btcutil.NewTxFromBytes(TstSpendingSerializedTx) TstSpendingTxBlockHeight = int32(279143) TstSignedTxBlockHash, _ = chainhash.NewHashFromStr("00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") TstSignedTxBlockDetails = &BlockMeta{ Block: Block{Hash: *TstSignedTxBlockHash, Height: TstSpendingTxBlockHeight}, Time: time.Unix(1389114091, 0), } // defaultDBTimeout specifies the timeout value when opening the wallet // database. defaultDBTimeout = 10 * time.Second ) func testDB() (walletdb.DB, func(), error) { tmpDir, err := ioutil.TempDir("", "wtxmgr_test") if err != nil { return nil, func() {}, err } db, err := walletdb.Create( "bdb", filepath.Join(tmpDir, "db"), true, defaultDBTimeout, ) return db, func() { os.RemoveAll(tmpDir) }, err } var namespaceKey = []byte("txstore") func testStore() (*Store, walletdb.DB, func(), error) { tmpDir, err := ioutil.TempDir("", "wtxmgr_test") if err != nil { return nil, nil, func() {}, err } db, err := walletdb.Create( "bdb", filepath.Join(tmpDir, "db"), true, defaultDBTimeout, ) if err != nil { os.RemoveAll(tmpDir) return nil, nil, nil, err } teardown := func() { db.Close() os.RemoveAll(tmpDir) } var s *Store err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { ns, err := tx.CreateTopLevelBucket(namespaceKey) if err != nil { return err } err = Create(ns) if err != nil { return err } s, err = Open(ns, &chaincfg.TestNet3Params) return err }) return s, db, teardown, err } func serializeTx(tx *btcutil.Tx) []byte { var buf bytes.Buffer err := tx.MsgTx().Serialize(&buf) if err != nil { panic(err) } return buf.Bytes() } func TestInsertsCreditsDebitsRollbacks(t *testing.T) { t.Parallel() // Create a double spend of the received blockchain transaction. dupRecvTx, _ := btcutil.NewTxFromBytes(TstRecvSerializedTx) // Switch txout amount to 1 BTC. Transaction store doesn't // validate txs, so this is fine for testing a double spend // removal. TstDupRecvAmount := int64(1e8) newDupMsgTx := dupRecvTx.MsgTx() newDupMsgTx.TxOut[0].Value = TstDupRecvAmount TstDoubleSpendTx := btcutil.NewTx(newDupMsgTx) TstDoubleSpendSerializedTx := serializeTx(TstDoubleSpendTx) // Create a "signed" (with invalid sigs) tx that spends output 0 of // the double spend. spendingTx := wire.NewMsgTx(wire.TxVersion) spendingTxIn := wire.NewTxIn(wire.NewOutPoint(TstDoubleSpendTx.Hash(), 0), []byte{0, 1, 2, 3, 4}, nil) spendingTx.AddTxIn(spendingTxIn) spendingTxOut1 := wire.NewTxOut(1e7, []byte{5, 6, 7, 8, 9}) spendingTxOut2 := wire.NewTxOut(9e7, []byte{10, 11, 12, 13, 14}) spendingTx.AddTxOut(spendingTxOut1) spendingTx.AddTxOut(spendingTxOut2) TstSpendingTx := btcutil.NewTx(spendingTx) TstSpendingSerializedTx := serializeTx(TstSpendingTx) var _ = TstSpendingTx tests := []struct { name string f func(*Store, walletdb.ReadWriteBucket) (*Store, error) bal, unc btcutil.Amount unspents map[wire.OutPoint]struct{} unmined map[chainhash.Hash]struct{} }{ { name: "new store", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { return s, nil }, bal: 0, unc: 0, unspents: map[wire.OutPoint]struct{}{}, unmined: map[chainhash.Hash]struct{}{}, }, { name: "txout insert", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, nil) if err != nil { return nil, err } err = s.AddCredit(ns, rec, nil, 0, false) return s, err }, bal: 0, unc: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), unspents: map[wire.OutPoint]struct{}{ { Hash: *TstRecvTx.Hash(), Index: 0, }: {}, }, unmined: map[chainhash.Hash]struct{}{ *TstRecvTx.Hash(): {}, }, }, { name: "insert duplicate unconfirmed", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, nil) if err != nil { return nil, err } err = s.AddCredit(ns, rec, nil, 0, false) return s, err }, bal: 0, unc: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), unspents: map[wire.OutPoint]struct{}{ { Hash: *TstRecvTx.Hash(), Index: 0, }: {}, }, unmined: map[chainhash.Hash]struct{}{ *TstRecvTx.Hash(): {}, }, }, { name: "confirmed txout insert", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, TstRecvTxBlockDetails) if err != nil { return nil, err } err = s.AddCredit(ns, rec, TstRecvTxBlockDetails, 0, false) return s, err }, bal: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), unc: 0, unspents: map[wire.OutPoint]struct{}{ { Hash: *TstRecvTx.Hash(), Index: 0, }: {}, }, unmined: map[chainhash.Hash]struct{}{}, }, { name: "insert duplicate confirmed", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, TstRecvTxBlockDetails) if err != nil { return nil, err } err = s.AddCredit(ns, rec, TstRecvTxBlockDetails, 0, false) return s, err }, bal: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), unc: 0, unspents: map[wire.OutPoint]struct{}{ { Hash: *TstRecvTx.Hash(), Index: 0, }: {}, }, unmined: map[chainhash.Hash]struct{}{}, }, { name: "rollback confirmed credit", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { err := s.Rollback(ns, TstRecvTxBlockDetails.Height) return s, err }, bal: 0, unc: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), unspents: map[wire.OutPoint]struct{}{ { Hash: *TstRecvTx.Hash(), Index: 0, }: {}, }, unmined: map[chainhash.Hash]struct{}{ *TstRecvTx.Hash(): {}, }, }, { name: "insert confirmed double spend", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstDoubleSpendSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, TstRecvTxBlockDetails) if err != nil { return nil, err } err = s.AddCredit(ns, rec, TstRecvTxBlockDetails, 0, false) return s, err }, bal: btcutil.Amount(TstDoubleSpendTx.MsgTx().TxOut[0].Value), unc: 0, unspents: map[wire.OutPoint]struct{}{ { Hash: *TstDoubleSpendTx.Hash(), Index: 0, }: {}, }, unmined: map[chainhash.Hash]struct{}{}, }, { name: "insert unconfirmed debit", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, nil) return s, err }, bal: 0, unc: 0, unspents: map[wire.OutPoint]struct{}{}, unmined: map[chainhash.Hash]struct{}{ *TstSpendingTx.Hash(): {}, }, }, { name: "insert unconfirmed debit again", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstDoubleSpendSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, TstRecvTxBlockDetails) return s, err }, bal: 0, unc: 0, unspents: map[wire.OutPoint]struct{}{}, unmined: map[chainhash.Hash]struct{}{ *TstSpendingTx.Hash(): {}, }, }, { name: "insert change (index 0)", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, nil) if err != nil { return nil, err } err = s.AddCredit(ns, rec, nil, 0, true) return s, err }, bal: 0, unc: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value), unspents: map[wire.OutPoint]struct{}{ { Hash: *TstSpendingTx.Hash(), Index: 0, }: {}, }, unmined: map[chainhash.Hash]struct{}{ *TstSpendingTx.Hash(): {}, }, }, { name: "insert output back to this own wallet (index 1)", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, nil) if err != nil { return nil, err } err = s.AddCredit(ns, rec, nil, 1, true) return s, err }, bal: 0, unc: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), unspents: map[wire.OutPoint]struct{}{ { Hash: *TstSpendingTx.Hash(), Index: 0, }: {}, { Hash: *TstSpendingTx.Hash(), Index: 1, }: {}, }, unmined: map[chainhash.Hash]struct{}{ *TstSpendingTx.Hash(): {}, }, }, { name: "confirm signed tx", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, TstSignedTxBlockDetails) return s, err }, bal: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), unc: 0, unspents: map[wire.OutPoint]struct{}{ { Hash: *TstSpendingTx.Hash(), Index: 0, }: {}, { Hash: *TstSpendingTx.Hash(), Index: 1, }: {}, }, unmined: map[chainhash.Hash]struct{}{}, }, { name: "rollback after spending tx", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { err := s.Rollback(ns, TstSignedTxBlockDetails.Height+1) return s, err }, bal: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), unc: 0, unspents: map[wire.OutPoint]struct{}{ { Hash: *TstSpendingTx.Hash(), Index: 0, }: {}, { Hash: *TstSpendingTx.Hash(), Index: 1, }: {}, }, unmined: map[chainhash.Hash]struct{}{}, }, { name: "rollback spending tx block", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { err := s.Rollback(ns, TstSignedTxBlockDetails.Height) return s, err }, bal: 0, unc: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), unspents: map[wire.OutPoint]struct{}{ { Hash: *TstSpendingTx.Hash(), Index: 0, }: {}, { Hash: *TstSpendingTx.Hash(), Index: 1, }: {}, }, unmined: map[chainhash.Hash]struct{}{ *TstSpendingTx.Hash(): {}, }, }, { name: "rollback double spend tx block", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { err := s.Rollback(ns, TstRecvTxBlockDetails.Height) return s, err }, bal: 0, unc: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), unspents: map[wire.OutPoint]struct{}{ *wire.NewOutPoint(TstSpendingTx.Hash(), 0): {}, *wire.NewOutPoint(TstSpendingTx.Hash(), 1): {}, }, unmined: map[chainhash.Hash]struct{}{ *TstDoubleSpendTx.Hash(): {}, *TstSpendingTx.Hash(): {}, }, }, { name: "insert original recv txout", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } err = s.InsertTx(ns, rec, TstRecvTxBlockDetails) if err != nil { return nil, err } err = s.AddCredit(ns, rec, TstRecvTxBlockDetails, 0, false) return s, err }, bal: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), unc: 0, unspents: map[wire.OutPoint]struct{}{ *wire.NewOutPoint(TstRecvTx.Hash(), 0): {}, }, unmined: map[chainhash.Hash]struct{}{}, }, } s, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() for _, test := range tests { err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(namespaceKey) tmpStore, err := test.f(s, ns) if err != nil { t.Fatalf("%s: got error: %v", test.name, err) } s = tmpStore bal, err := s.Balance(ns, 1, TstRecvCurrentHeight) if err != nil { t.Fatalf("%s: Confirmed Balance failed: %v", test.name, err) } if bal != test.bal { t.Fatalf("%s: balance mismatch: expected: %d, got: %d", test.name, test.bal, bal) } unc, err := s.Balance(ns, 0, TstRecvCurrentHeight) if err != nil { t.Fatalf("%s: Unconfirmed Balance failed: %v", test.name, err) } unc -= bal if unc != test.unc { t.Fatalf("%s: unconfirmed balance mismatch: expected %d, got %d", test.name, test.unc, unc) } // Check that unspent outputs match expected. unspent, err := s.UnspentOutputs(ns) if err != nil { t.Fatalf("%s: failed to fetch unspent outputs: %v", test.name, err) } for _, cred := range unspent { if _, ok := test.unspents[cred.OutPoint]; !ok { t.Errorf("%s: unexpected unspent output: %v", test.name, cred.OutPoint) } delete(test.unspents, cred.OutPoint) } if len(test.unspents) != 0 { t.Fatalf("%s: missing expected unspent output(s)", test.name) } // Check that unmined txs match expected. unmined, err := s.UnminedTxs(ns) if err != nil { t.Fatalf("%s: cannot load unmined transactions: %v", test.name, err) } for _, tx := range unmined { txHash := tx.TxHash() if _, ok := test.unmined[txHash]; !ok { t.Fatalf("%s: unexpected unmined tx: %v", test.name, txHash) } delete(test.unmined, txHash) } if len(test.unmined) != 0 { t.Fatalf("%s: missing expected unmined tx(s)", test.name) } return nil }) if err != nil { t.Fatal(err) } } } func TestFindingSpentCredits(t *testing.T) { t.Parallel() s, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() dbtx, err := db.BeginReadWriteTx() if err != nil { t.Fatal(err) } defer dbtx.Commit() ns := dbtx.ReadWriteBucket(namespaceKey) // Insert transaction and credit which will be spent. recvRec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) if err != nil { t.Fatal(err) } err = s.InsertTx(ns, recvRec, TstRecvTxBlockDetails) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, recvRec, TstRecvTxBlockDetails, 0, false) if err != nil { t.Fatal(err) } // Insert confirmed transaction which spends the above credit. spendingRec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) if err != nil { t.Fatal(err) } err = s.InsertTx(ns, spendingRec, TstSignedTxBlockDetails) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, spendingRec, TstSignedTxBlockDetails, 0, false) if err != nil { t.Fatal(err) } bal, err := s.Balance(ns, 1, TstSignedTxBlockDetails.Height) if err != nil { t.Fatal(err) } expectedBal := btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value) if bal != expectedBal { t.Fatalf("bad balance: %v != %v", bal, expectedBal) } unspents, err := s.UnspentOutputs(ns) if err != nil { t.Fatal(err) } op := wire.NewOutPoint(TstSpendingTx.Hash(), 0) if unspents[0].OutPoint != *op { t.Fatal("unspent outpoint doesn't match expected") } if len(unspents) > 1 { t.Fatal("has more than one unspent credit") } } func newCoinBase(outputValues ...int64) *wire.MsgTx { tx := wire.MsgTx{ TxIn: []*wire.TxIn{ { PreviousOutPoint: wire.OutPoint{Index: ^uint32(0)}, }, }, } for _, val := range outputValues { tx.TxOut = append(tx.TxOut, &wire.TxOut{Value: val}) } return &tx } func spendOutput(txHash *chainhash.Hash, index uint32, outputValues ...int64) *wire.MsgTx { tx := wire.MsgTx{ TxIn: []*wire.TxIn{ { PreviousOutPoint: wire.OutPoint{Hash: *txHash, Index: index}, }, }, } for _, val := range outputValues { tx.TxOut = append(tx.TxOut, &wire.TxOut{Value: val}) } return &tx } func spendOutputs(outputs []wire.OutPoint, outputValues ...int64) *wire.MsgTx { tx := &wire.MsgTx{} for _, output := range outputs { tx.TxIn = append(tx.TxIn, &wire.TxIn{PreviousOutPoint: output}) } for _, value := range outputValues { tx.TxOut = append(tx.TxOut, &wire.TxOut{Value: value}) } return tx } func TestCoinbases(t *testing.T) { t.Parallel() s, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() dbtx, err := db.BeginReadWriteTx() if err != nil { t.Fatal(err) } defer dbtx.Commit() ns := dbtx.ReadWriteBucket(namespaceKey) b100 := BlockMeta{ Block: Block{Height: 100}, Time: time.Now(), } cb := newCoinBase(20e8, 10e8, 30e8) cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) if err != nil { t.Fatal(err) } // Insert coinbase and mark outputs 0 and 2 as credits. err = s.InsertTx(ns, cbRec, &b100) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, cbRec, &b100, 0, false) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, cbRec, &b100, 2, false) if err != nil { t.Fatal(err) } coinbaseMaturity := int32(chaincfg.TestNet3Params.CoinbaseMaturity) // Balance should be 0 if the coinbase is immature, 50 BTC at and beyond // maturity. // // Outputs when depth is below maturity are never included, no matter // the required number of confirmations. Matured outputs which have // greater depth than minConf are still excluded. type balTest struct { height int32 minConf int32 bal btcutil.Amount } balTests := []balTest{ // Next block it is still immature { height: b100.Height + coinbaseMaturity - 2, minConf: 0, bal: 0, }, { height: b100.Height + coinbaseMaturity - 2, minConf: coinbaseMaturity, bal: 0, }, // Next block it matures { height: b100.Height + coinbaseMaturity - 1, minConf: 0, bal: 50e8, }, { height: b100.Height + coinbaseMaturity - 1, minConf: 1, bal: 50e8, }, { height: b100.Height + coinbaseMaturity - 1, minConf: coinbaseMaturity - 1, bal: 50e8, }, { height: b100.Height + coinbaseMaturity - 1, minConf: coinbaseMaturity, bal: 50e8, }, { height: b100.Height + coinbaseMaturity - 1, minConf: coinbaseMaturity + 1, bal: 0, }, // Matures at this block { height: b100.Height + coinbaseMaturity, minConf: 0, bal: 50e8, }, { height: b100.Height + coinbaseMaturity, minConf: 1, bal: 50e8, }, { height: b100.Height + coinbaseMaturity, minConf: coinbaseMaturity, bal: 50e8, }, { height: b100.Height + coinbaseMaturity, minConf: coinbaseMaturity + 1, bal: 50e8, }, { height: b100.Height + coinbaseMaturity, minConf: coinbaseMaturity + 2, bal: 0, }, } for i, tst := range balTests { bal, err := s.Balance(ns, tst.minConf, tst.height) if err != nil { t.Fatalf("Balance test %d: Store.Balance failed: %v", i, err) } if bal != tst.bal { t.Errorf("Balance test %d: Got %v Expected %v", i, bal, tst.bal) } } if t.Failed() { t.Fatal("Failed balance checks after inserting coinbase") } // Spend an output from the coinbase tx in an unmined transaction when // the next block will mature the coinbase. spenderATime := time.Now() spenderA := spendOutput(&cbRec.Hash, 0, 5e8, 15e8) spenderARec, err := NewTxRecordFromMsgTx(spenderA, spenderATime) if err != nil { t.Fatal(err) } err = s.InsertTx(ns, spenderARec, nil) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, spenderARec, nil, 0, false) if err != nil { t.Fatal(err) } balTests = []balTest{ // Next block it matures { height: b100.Height + coinbaseMaturity - 1, minConf: 0, bal: 35e8, }, { height: b100.Height + coinbaseMaturity - 1, minConf: 1, bal: 30e8, }, { height: b100.Height + coinbaseMaturity - 1, minConf: coinbaseMaturity, bal: 30e8, }, { height: b100.Height + coinbaseMaturity - 1, minConf: coinbaseMaturity + 1, bal: 0, }, // Matures at this block { height: b100.Height + coinbaseMaturity, minConf: 0, bal: 35e8, }, { height: b100.Height + coinbaseMaturity, minConf: 1, bal: 30e8, }, { height: b100.Height + coinbaseMaturity, minConf: coinbaseMaturity, bal: 30e8, }, { height: b100.Height + coinbaseMaturity, minConf: coinbaseMaturity + 1, bal: 30e8, }, { height: b100.Height + coinbaseMaturity, minConf: coinbaseMaturity + 2, bal: 0, }, } balTestsBeforeMaturity := balTests for i, tst := range balTests { bal, err := s.Balance(ns, tst.minConf, tst.height) if err != nil { t.Fatalf("Balance test %d: Store.Balance failed: %v", i, err) } if bal != tst.bal { t.Errorf("Balance test %d: Got %v Expected %v", i, bal, tst.bal) } } if t.Failed() { t.Fatal("Failed balance checks after spending coinbase with unmined transaction") } // Mine the spending transaction in the block the coinbase matures. bMaturity := BlockMeta{ Block: Block{Height: b100.Height + coinbaseMaturity}, Time: time.Now(), } err = s.InsertTx(ns, spenderARec, &bMaturity) if err != nil { t.Fatal(err) } balTests = []balTest{ // Maturity height { height: bMaturity.Height, minConf: 0, bal: 35e8, }, { height: bMaturity.Height, minConf: 1, bal: 35e8, }, { height: bMaturity.Height, minConf: 2, bal: 30e8, }, { height: bMaturity.Height, minConf: coinbaseMaturity, bal: 30e8, }, { height: bMaturity.Height, minConf: coinbaseMaturity + 1, bal: 30e8, }, { height: bMaturity.Height, minConf: coinbaseMaturity + 2, bal: 0, }, // Next block after maturity height { height: bMaturity.Height + 1, minConf: 0, bal: 35e8, }, { height: bMaturity.Height + 1, minConf: 2, bal: 35e8, }, { height: bMaturity.Height + 1, minConf: 3, bal: 30e8, }, { height: bMaturity.Height + 1, minConf: coinbaseMaturity + 2, bal: 30e8, }, { height: bMaturity.Height + 1, minConf: coinbaseMaturity + 3, bal: 0, }, } for i, tst := range balTests { bal, err := s.Balance(ns, tst.minConf, tst.height) if err != nil { t.Fatalf("Balance test %d: Store.Balance failed: %v", i, err) } if bal != tst.bal { t.Errorf("Balance test %d: Got %v Expected %v", i, bal, tst.bal) } } if t.Failed() { t.Fatal("Failed balance checks mining coinbase spending transaction") } // Create another spending transaction which spends the credit from the // first spender. This will be used to test removing the entire // conflict chain when the coinbase is later reorged out. // // Use the same output amount as spender A and mark it as a credit. // This will mean the balance tests should report identical results. spenderBTime := time.Now() spenderB := spendOutput(&spenderARec.Hash, 0, 5e8) spenderBRec, err := NewTxRecordFromMsgTx(spenderB, spenderBTime) if err != nil { t.Fatal(err) } err = s.InsertTx(ns, spenderBRec, &bMaturity) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, spenderBRec, &bMaturity, 0, false) if err != nil { t.Fatal(err) } for i, tst := range balTests { bal, err := s.Balance(ns, tst.minConf, tst.height) if err != nil { t.Fatalf("Balance test %d: Store.Balance failed: %v", i, err) } if bal != tst.bal { t.Errorf("Balance test %d: Got %v Expected %v", i, bal, tst.bal) } } if t.Failed() { t.Fatal("Failed balance checks mining second spending transaction") } // Reorg out the block that matured the coinbase and check balances // again. err = s.Rollback(ns, bMaturity.Height) if err != nil { t.Fatal(err) } balTests = balTestsBeforeMaturity for i, tst := range balTests { bal, err := s.Balance(ns, tst.minConf, tst.height) if err != nil { t.Fatalf("Balance test %d: Store.Balance failed: %v", i, err) } if bal != tst.bal { t.Errorf("Balance test %d: Got %v Expected %v", i, bal, tst.bal) } } if t.Failed() { t.Fatal("Failed balance checks after reorging maturity block") } // Reorg out the block which contained the coinbase. There should be no // more transactions in the store (since the previous outputs referenced // by the spending tx no longer exist), and the balance will always be // zero. err = s.Rollback(ns, b100.Height) if err != nil { t.Fatal(err) } balTests = []balTest{ // Current height { height: b100.Height - 1, minConf: 0, bal: 0, }, { height: b100.Height - 1, minConf: 1, bal: 0, }, // Next height { height: b100.Height, minConf: 0, bal: 0, }, { height: b100.Height, minConf: 1, bal: 0, }, } for i, tst := range balTests { bal, err := s.Balance(ns, tst.minConf, tst.height) if err != nil { t.Fatalf("Balance test %d: Store.Balance failed: %v", i, err) } if bal != tst.bal { t.Errorf("Balance test %d: Got %v Expected %v", i, bal, tst.bal) } } if t.Failed() { t.Fatal("Failed balance checks after reorging coinbase block") } unminedTxs, err := s.UnminedTxs(ns) if err != nil { t.Fatal(err) } if len(unminedTxs) != 0 { t.Fatalf("Should have no unmined transactions after coinbase reorg, found %d", len(unminedTxs)) } } // Test moving multiple transactions from unmined buckets to the same block. func TestMoveMultipleToSameBlock(t *testing.T) { t.Parallel() s, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() dbtx, err := db.BeginReadWriteTx() if err != nil { t.Fatal(err) } defer dbtx.Commit() ns := dbtx.ReadWriteBucket(namespaceKey) b100 := BlockMeta{ Block: Block{Height: 100}, Time: time.Now(), } cb := newCoinBase(20e8, 30e8) cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) if err != nil { t.Fatal(err) } // Insert coinbase and mark both outputs as credits. err = s.InsertTx(ns, cbRec, &b100) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, cbRec, &b100, 0, false) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, cbRec, &b100, 1, false) if err != nil { t.Fatal(err) } // Create and insert two unmined transactions which spend both coinbase // outputs. spenderATime := time.Now() spenderA := spendOutput(&cbRec.Hash, 0, 1e8, 2e8, 18e8) spenderARec, err := NewTxRecordFromMsgTx(spenderA, spenderATime) if err != nil { t.Fatal(err) } err = s.InsertTx(ns, spenderARec, nil) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, spenderARec, nil, 0, false) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, spenderARec, nil, 1, false) if err != nil { t.Fatal(err) } spenderBTime := time.Now() spenderB := spendOutput(&cbRec.Hash, 1, 4e8, 8e8, 18e8) spenderBRec, err := NewTxRecordFromMsgTx(spenderB, spenderBTime) if err != nil { t.Fatal(err) } err = s.InsertTx(ns, spenderBRec, nil) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, spenderBRec, nil, 0, false) if err != nil { t.Fatal(err) } err = s.AddCredit(ns, spenderBRec, nil, 1, false) if err != nil { t.Fatal(err) } coinbaseMaturity := int32(chaincfg.TestNet3Params.CoinbaseMaturity) // Mine both transactions in the block that matures the coinbase. bMaturity := BlockMeta{ Block: Block{Height: b100.Height + coinbaseMaturity}, Time: time.Now(), } err = s.InsertTx(ns, spenderARec, &bMaturity) if err != nil { t.Fatal(err) } err = s.InsertTx(ns, spenderBRec, &bMaturity) if err != nil { t.Fatal(err) } // Check that both transactions can be queried at the maturity block. detailsA, err := s.UniqueTxDetails(ns, &spenderARec.Hash, &bMaturity.Block) if err != nil { t.Fatal(err) } if detailsA == nil { t.Fatal("No details found for first spender") } detailsB, err := s.UniqueTxDetails(ns, &spenderBRec.Hash, &bMaturity.Block) if err != nil { t.Fatal(err) } if detailsB == nil { t.Fatal("No details found for second spender") } // Verify that the balance was correctly updated on the block record // append and that no unmined transactions remain. balTests := []struct { height int32 minConf int32 bal btcutil.Amount }{ // Maturity height { height: bMaturity.Height, minConf: 0, bal: 15e8, }, { height: bMaturity.Height, minConf: 1, bal: 15e8, }, { height: bMaturity.Height, minConf: 2, bal: 0, }, // Next block after maturity height { height: bMaturity.Height + 1, minConf: 0, bal: 15e8, }, { height: bMaturity.Height + 1, minConf: 2, bal: 15e8, }, { height: bMaturity.Height + 1, minConf: 3, bal: 0, }, } for i, tst := range balTests { bal, err := s.Balance(ns, tst.minConf, tst.height) if err != nil { t.Fatalf("Balance test %d: Store.Balance failed: %v", i, err) } if bal != tst.bal { t.Errorf("Balance test %d: Got %v Expected %v", i, bal, tst.bal) } } if t.Failed() { t.Fatal("Failed balance checks after moving both coinbase spenders") } unminedTxs, err := s.UnminedTxs(ns) if err != nil { t.Fatal(err) } if len(unminedTxs) != 0 { t.Fatalf("Should have no unmined transactions mining both, found %d", len(unminedTxs)) } } // Test the optional-ness of the serialized transaction in a TxRecord. // NewTxRecord and NewTxRecordFromMsgTx both save the serialized transaction, so // manually strip it out to test this code path. func TestInsertUnserializedTx(t *testing.T) { t.Parallel() s, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() dbtx, err := db.BeginReadWriteTx() if err != nil { t.Fatal(err) } defer dbtx.Commit() ns := dbtx.ReadWriteBucket(namespaceKey) tx := newCoinBase(50e8) rec, err := NewTxRecordFromMsgTx(tx, timeNow()) if err != nil { t.Fatal(err) } b100 := makeBlockMeta(100) err = s.InsertTx(ns, stripSerializedTx(rec), &b100) if err != nil { t.Fatalf("Insert for stripped TxRecord failed: %v", err) } // Ensure it can be retreived successfully. details, err := s.UniqueTxDetails(ns, &rec.Hash, &b100.Block) if err != nil { t.Fatal(err) } rec2, err := NewTxRecordFromMsgTx(&details.MsgTx, rec.Received) if err != nil { t.Fatal(err) } if !bytes.Equal(rec.SerializedTx, rec2.SerializedTx) { t.Fatal("Serialized txs for coinbase do not match") } // Now test that path with an unmined transaction. tx = spendOutput(&rec.Hash, 0, 50e8) rec, err = NewTxRecordFromMsgTx(tx, timeNow()) if err != nil { t.Fatal(err) } err = s.InsertTx(ns, rec, nil) if err != nil { t.Fatal(err) } details, err = s.UniqueTxDetails(ns, &rec.Hash, nil) if err != nil { t.Fatal(err) } rec2, err = NewTxRecordFromMsgTx(&details.MsgTx, rec.Received) if err != nil { t.Fatal(err) } if !bytes.Equal(rec.SerializedTx, rec2.SerializedTx) { t.Fatal("Serialized txs for coinbase spender do not match") } } // TestRemoveUnminedTx tests that if we add an umined transaction, then we're // able to remove that unmined transaction later along with any of its // descendants. Any balance modifications due to the unmined transaction should // be revered. func TestRemoveUnminedTx(t *testing.T) { t.Parallel() store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // In order to reproduce real-world scenarios, we'll use a new database // transaction for each interaction with the wallet. // // We'll start off the test by creating a new coinbase output at height // 100 and inserting it into the store. b100 := &BlockMeta{ Block: Block{Height: 100}, Time: time.Now(), } initialBalance := int64(1e8) cb := newCoinBase(initialBalance) cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, cbRec, b100); err != nil { t.Fatal(err) } err := store.AddCredit(ns, cbRec, b100, 0, false) if err != nil { t.Fatal(err) } }) // Determine the maturity height for the coinbase output created. coinbaseMaturity := int32(chaincfg.TestNet3Params.CoinbaseMaturity) maturityHeight := b100.Block.Height + coinbaseMaturity // checkBalance is a helper function that compares the balance of the // store with the expected value. The includeUnconfirmed boolean can be // used to include the unconfirmed balance as a part of the total // balance. checkBalance := func(expectedBalance btcutil.Amount, includeUnconfirmed bool) { t.Helper() minConfs := int32(1) if includeUnconfirmed { minConfs = 0 } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { t.Helper() b, err := store.Balance(ns, minConfs, maturityHeight) if err != nil { t.Fatalf("unable to retrieve balance: %v", err) } if b != expectedBalance { t.Fatalf("expected balance of %d, got %d", expectedBalance, b) } }) } // Since we don't have any unconfirmed transactions within the store, // the total balance reflecting confirmed and unconfirmed outputs should // match the initial balance. checkBalance(btcutil.Amount(initialBalance), false) checkBalance(btcutil.Amount(initialBalance), true) // Then, we'll create an unconfirmed spend for the coinbase output and // insert it into the store. b101 := &BlockMeta{ Block: Block{Height: 201}, Time: time.Now(), } changeAmount := int64(4e7) spendTx := spendOutput(&cbRec.Hash, 0, 5e7, changeAmount) spendTxRec, err := NewTxRecordFromMsgTx(spendTx, b101.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, spendTxRec, nil); err != nil { t.Fatal(err) } err := store.AddCredit(ns, spendTxRec, nil, 1, true) if err != nil { t.Fatal(err) } }) // With the unconfirmed spend inserted into the store, we'll query it // for its unconfirmed tranasctions to ensure it was properly added. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { unminedTxs, err := store.UnminedTxs(ns) if err != nil { t.Fatalf("unable to query for unmined txs: %v", err) } if len(unminedTxs) != 1 { t.Fatalf("expected 1 mined tx, instead got %v", len(unminedTxs)) } unminedTxHash := unminedTxs[0].TxHash() spendTxHash := spendTx.TxHash() if !unminedTxHash.IsEqual(&spendTxHash) { t.Fatalf("mismatch tx hashes: expected %v, got %v", spendTxHash, unminedTxHash) } }) // Now that an unconfirmed spend exists, there should no longer be any // confirmed balance. The total balance should now all be unconfirmed // and it should match the change amount of the unconfirmed spend // tranasction. checkBalance(0, false) checkBalance(btcutil.Amount(changeAmount), true) // Now, we'll remove the unconfirmed spend tranaction from the store. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.RemoveUnminedTx(ns, spendTxRec); err != nil { t.Fatal(err) } }) // We'll query the store one last time for its unconfirmed transactions // to ensure the unconfirmed spend was properly removed above. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { unminedTxs, err := store.UnminedTxs(ns) if err != nil { t.Fatalf("unable to query for unmined txs: %v", err) } if len(unminedTxs) != 0 { t.Fatalf("expected 0 mined txs, instead got %v", len(unminedTxs)) } }) // Finally, the total balance (including confirmed and unconfirmed) // should once again match the initial balance, as the uncofirmed spend // has already been removed. checkBalance(btcutil.Amount(initialBalance), false) checkBalance(btcutil.Amount(initialBalance), true) } // TestInsertMempoolTxAlreadyConfirmed ensures that transactions that already // exist within the store as confirmed cannot be added as unconfirmed. func TestInsertMempoolTxAlreadyConfirmed(t *testing.T) { t.Parallel() store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // In order to reproduce real-world scenarios, we'll use a new database // transaction for each interaction with the wallet. // // We'll start off the test by creating a new coinbase output at height // 100 and inserting it into the store. b100 := &BlockMeta{ Block: Block{Height: 100}, Time: time.Now(), } tx := newCoinBase(1e8) txRec, err := NewTxRecordFromMsgTx(tx, b100.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, txRec, b100); err != nil { t.Fatal(err) } }) // checkStore is a helper we'll use to ensure the transaction only // exists within the store's confirmed bucket. checkStore := func() { t.Helper() commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if existsRawUnmined(ns, txRec.Hash[:]) != nil { t.Fatalf("expected transaction to not exist " + "in unconfirmed bucket") } _, v := existsTxRecord(ns, &txRec.Hash, &b100.Block) if v == nil { t.Fatalf("expected transaction to exist in " + "confirmed bucket") } }) } checkStore() // Inserting the transaction again as unconfirmed should result in a // NOP, i.e., no error should be returned and no disk modifications are // needed. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, txRec, nil); err != nil { t.Fatal(err) } }) checkStore() } // TestInsertMempoolTxAfterSpentOutput ensures that transactions that were // both confirmed and spent cannot be added as unconfirmed. func TestInsertMempoolTxAfterSpentOutput(t *testing.T) { t.Parallel() store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // First we add a confirmed transaction to the wallet. b100 := BlockMeta{ Block: Block{Height: 100}, Time: time.Now(), } cb := newCoinBase(1e8) cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, cbRec, &b100); err != nil { t.Fatal(err) } err := store.AddCredit(ns, cbRec, &b100, 0, false) if err != nil { t.Fatal(err) } }) // Then create a transaction that spends the previous tx output. b101 := BlockMeta{ Block: Block{Height: 101}, Time: time.Now(), } amt := int64(1e7) spend := spendOutput(&cbRec.Hash, 0, amt) spendRec, err := NewTxRecordFromMsgTx(spend, time.Now()) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { // We add the spending tx to the wallet as confirmed. err := store.InsertTx(ns, spendRec, &b101) if err != nil { t.Fatal(err) } err = store.AddCredit(ns, spendRec, &b101, 0, false) if err != nil { t.Fatal(err) } // We now adding the original transaction as mempool to simulate // a real case where trying to broadcast a tx after it has been // confirmed and spent. if err := store.InsertTx(ns, cbRec, nil); err != nil { t.Fatal(err) } err = store.AddCredit(ns, cbRec, nil, 0, false) if err != nil { t.Fatal(err) } }) // now we check that there no unminedCredit exists for the original tx. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { k := canonicalOutPoint(&cbRec.Hash, 0) if existsRawUnminedCredit(ns, k) != nil { t.Fatalf("expected output to not exist " + "in unmined credit bucket") } }) } // TestOutputsAfterRemoveDoubleSpend ensures that when we remove a transaction // that double spends an existing output within the wallet, it doesn't remove // any other spending transactions of the same output. func TestOutputsAfterRemoveDoubleSpend(t *testing.T) { t.Parallel() store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // In order to reproduce real-world scenarios, we'll use a new database // transaction for each interaction with the wallet. // // We'll start off the test by creating a new coinbase output at height // 100 and inserting it into the store. b100 := BlockMeta{ Block: Block{Height: 100}, Time: time.Now(), } cb := newCoinBase(1e8) cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, cbRec, &b100); err != nil { t.Fatal(err) } err := store.AddCredit(ns, cbRec, nil, 0, false) if err != nil { t.Fatal(err) } }) // We'll create three spending transactions for the same output and add // them to the store. const numSpendRecs = 3 spendRecs := make([]*TxRecord, 0, numSpendRecs) for i := 0; i < numSpendRecs; i++ { amt := int64((i + 1) * 1e7) spend := spendOutput(&cbRec.Hash, 0, amt) spendRec, err := NewTxRecordFromMsgTx(spend, time.Now()) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { err := store.InsertTx(ns, spendRec, nil) if err != nil { t.Fatal(err) } err = store.AddCredit(ns, spendRec, nil, 0, false) if err != nil { t.Fatal(err) } }) spendRecs = append(spendRecs, spendRec) } // checkOutputs is a helper closure we'll use to ensure none of the // other outputs from spending transactions have been removed from the // store just because we removed one of the spending transactions. checkOutputs := func(txs ...*TxRecord) { t.Helper() ops := make(map[wire.OutPoint]struct{}) for _, tx := range txs { for i := range tx.MsgTx.TxOut { ops[wire.OutPoint{ Hash: tx.Hash, Index: uint32(i), }] = struct{}{} } } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { t.Helper() outputs, err := store.UnspentOutputs(ns) if err != nil { t.Fatalf("unable to get unspent outputs: %v", err) } if len(outputs) != len(ops) { t.Fatalf("expected %d outputs, got %d", len(ops), len(outputs)) } for _, output := range outputs { op := output.OutPoint if _, ok := ops[op]; !ok { t.Fatalf("found unexpected output %v", op) } } }) } // All of the outputs of our spending transactions should exist. checkOutputs(spendRecs...) // We'll then remove the last transaction we crafted from the store and // check our outputs again to ensure they still exist. spendToRemove := spendRecs[numSpendRecs-1] spendRecs = spendRecs[:numSpendRecs-1] commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.RemoveUnminedTx(ns, spendToRemove); err != nil { t.Fatalf("unable to remove unmined transaction: %v", err) } }) checkOutputs(spendRecs...) } // commitDBTx is a helper function that allows us to perform multiple operations // on a specific database's bucket as a single atomic operation. func commitDBTx(t *testing.T, store *Store, db walletdb.DB, f func(walletdb.ReadWriteBucket)) { t.Helper() dbTx, err := db.BeginReadWriteTx() if err != nil { t.Fatal(err) } defer dbTx.Commit() ns := dbTx.ReadWriteBucket(namespaceKey) f(ns) } // testInsertDoubleSpendTx is a helper test which double spends an output. The // boolean parameter indicates whether the first spending transaction should be // the one confirmed. This test ensures that when a double spend occurs and both // spending transactions are present in the mempool, if one of them confirms, // then the remaining conflicting transactions within the mempool should be // removed from the wallet's store. func testInsertMempoolDoubleSpendTx(t *testing.T, first bool) { store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // In order to reproduce real-world scenarios, we'll use a new database // transaction for each interaction with the wallet. // // We'll start off the test by creating a new coinbase output at height // 100 and inserting it into the store. b100 := BlockMeta{ Block: Block{Height: 100}, Time: time.Now(), } cb := newCoinBase(1e8) cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, cbRec, &b100); err != nil { t.Fatal(err) } err := store.AddCredit(ns, cbRec, &b100, 0, false) if err != nil { t.Fatal(err) } }) // Then, we'll create two spends from the same coinbase output, in order // to replicate a double spend scenario. firstSpend := spendOutput(&cbRec.Hash, 0, 5e7, 5e7) firstSpendRec, err := NewTxRecordFromMsgTx(firstSpend, time.Now()) if err != nil { t.Fatal(err) } secondSpend := spendOutput(&cbRec.Hash, 0, 4e7, 6e7) secondSpendRec, err := NewTxRecordFromMsgTx(secondSpend, time.Now()) if err != nil { t.Fatal(err) } // We'll insert both of them into the store without confirming them. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, firstSpendRec, nil); err != nil { t.Fatal(err) } err := store.AddCredit(ns, firstSpendRec, nil, 0, false) if err != nil { t.Fatal(err) } }) commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, secondSpendRec, nil); err != nil { t.Fatal(err) } err := store.AddCredit(ns, secondSpendRec, nil, 0, false) if err != nil { t.Fatal(err) } }) // Ensure that both spends are found within the unconfirmed transactions // in the wallet's store. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { unminedTxs, err := store.UnminedTxs(ns) if err != nil { t.Fatal(err) } if len(unminedTxs) != 2 { t.Fatalf("expected 2 unmined txs, got %v", len(unminedTxs)) } }) // Then, we'll confirm either the first or second spend, depending on // the boolean passed, with a height deep enough that allows us to // succesfully spend the coinbase output. coinbaseMaturity := int32(chaincfg.TestNet3Params.CoinbaseMaturity) bMaturity := BlockMeta{ Block: Block{Height: b100.Height + coinbaseMaturity}, Time: time.Now(), } var confirmedSpendRec *TxRecord if first { confirmedSpendRec = firstSpendRec } else { confirmedSpendRec = secondSpendRec } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { err := store.InsertTx(ns, confirmedSpendRec, &bMaturity) if err != nil { t.Fatal(err) } err = store.AddCredit( ns, confirmedSpendRec, &bMaturity, 0, false, ) if err != nil { t.Fatal(err) } }) // This should now trigger the store to remove any other pending double // spends for this coinbase output, as we've already seen the confirmed // one. Therefore, we shouldn't see any other unconfirmed transactions // within it. We also ensure that the transaction that confirmed and is // now listed as a UTXO within the wallet is the correct one. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { unminedTxs, err := store.UnminedTxs(ns) if err != nil { t.Fatal(err) } if len(unminedTxs) != 0 { t.Fatalf("expected 0 unmined txs, got %v", len(unminedTxs)) } minedTxs, err := store.UnspentOutputs(ns) if err != nil { t.Fatal(err) } if len(minedTxs) != 1 { t.Fatalf("expected 1 mined tx, got %v", len(minedTxs)) } if !minedTxs[0].Hash.IsEqual(&confirmedSpendRec.Hash) { t.Fatalf("expected confirmed tx hash %v, got %v", confirmedSpendRec.Hash, minedTxs[0].Hash) } }) } // TestInsertMempoolDoubleSpendConfirmedFirstTx ensures that when a double spend // occurs and both spending transactions are present in the mempool, if the // first spend seen is confirmed, then the second spend transaction within the // mempool should be removed from the wallet's store. func TestInsertMempoolDoubleSpendConfirmedFirstTx(t *testing.T) { t.Parallel() testInsertMempoolDoubleSpendTx(t, true) } // TestInsertMempoolDoubleSpendConfirmedFirstTx ensures that when a double spend // occurs and both spending transactions are present in the mempool, if the // second spend seen is confirmed, then the first spend transaction within the // mempool should be removed from the wallet's store. func TestInsertMempoolDoubleSpendConfirmSecondTx(t *testing.T) { t.Parallel() testInsertMempoolDoubleSpendTx(t, false) } // TestInsertConfirmedDoubleSpendTx tests that when one or more double spends // occur and a spending transaction confirms that was not known to the wallet, // then the unconfirmed double spends within the mempool should be removed from // the wallet's store. func TestInsertConfirmedDoubleSpendTx(t *testing.T) { t.Parallel() store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // In order to reproduce real-world scenarios, we'll use a new database // transaction for each interaction with the wallet. // // We'll start off the test by creating a new coinbase output at height // 100 and inserting it into the store. b100 := BlockMeta{ Block: Block{Height: 100}, Time: time.Now(), } cb1 := newCoinBase(1e8) cbRec1, err := NewTxRecordFromMsgTx(cb1, b100.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, cbRec1, &b100); err != nil { t.Fatal(err) } err := store.AddCredit(ns, cbRec1, &b100, 0, false) if err != nil { t.Fatal(err) } }) // Then, we'll create three spends from the same coinbase output. The // first two will remain unconfirmed, while the last should confirm and // remove the remaining unconfirmed from the wallet's store. firstSpend1 := spendOutput(&cbRec1.Hash, 0, 5e7) firstSpendRec1, err := NewTxRecordFromMsgTx(firstSpend1, time.Now()) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, firstSpendRec1, nil); err != nil { t.Fatal(err) } err := store.AddCredit(ns, firstSpendRec1, nil, 0, false) if err != nil { t.Fatal(err) } }) secondSpend1 := spendOutput(&cbRec1.Hash, 0, 4e7) secondSpendRec1, err := NewTxRecordFromMsgTx(secondSpend1, time.Now()) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, secondSpendRec1, nil); err != nil { t.Fatal(err) } err := store.AddCredit(ns, secondSpendRec1, nil, 0, false) if err != nil { t.Fatal(err) } }) // We'll also create another output and have one unconfirmed and one // confirmed spending transaction also spend it. cb2 := newCoinBase(2e8) cbRec2, err := NewTxRecordFromMsgTx(cb2, b100.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, cbRec2, &b100); err != nil { t.Fatal(err) } err := store.AddCredit(ns, cbRec2, &b100, 0, false) if err != nil { t.Fatal(err) } }) firstSpend2 := spendOutput(&cbRec2.Hash, 0, 5e7) firstSpendRec2, err := NewTxRecordFromMsgTx(firstSpend2, time.Now()) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, firstSpendRec2, nil); err != nil { t.Fatal(err) } err := store.AddCredit(ns, firstSpendRec2, nil, 0, false) if err != nil { t.Fatal(err) } }) // At this point, we should see all unconfirmed transactions within the // store. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { unminedTxs, err := store.UnminedTxs(ns) if err != nil { t.Fatal(err) } if len(unminedTxs) != 3 { t.Fatalf("expected 3 unmined txs, got %d", len(unminedTxs)) } }) // Then, we'll insert the confirmed spend at a height deep enough that // allows us to successfully spend the coinbase outputs. coinbaseMaturity := int32(chaincfg.TestNet3Params.CoinbaseMaturity) bMaturity := BlockMeta{ Block: Block{Height: b100.Height + coinbaseMaturity}, Time: time.Now(), } outputsToSpend := []wire.OutPoint{ {Hash: cbRec1.Hash, Index: 0}, {Hash: cbRec2.Hash, Index: 0}, } confirmedSpend := spendOutputs(outputsToSpend, 3e7) confirmedSpendRec, err := NewTxRecordFromMsgTx( confirmedSpend, bMaturity.Time, ) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { err := store.InsertTx(ns, confirmedSpendRec, &bMaturity) if err != nil { t.Fatal(err) } err = store.AddCredit( ns, confirmedSpendRec, &bMaturity, 0, false, ) if err != nil { t.Fatal(err) } }) // Now that the confirmed spend exists within the store, we should no // longer see the unconfirmed spends within it. We also ensure that the // transaction that confirmed and is now listed as a UTXO within the // wallet is the correct one. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { unminedTxs, err := store.UnminedTxs(ns) if err != nil { t.Fatal(err) } if len(unminedTxs) != 0 { t.Fatalf("expected 0 unmined txs, got %v", len(unminedTxs)) } minedTxs, err := store.UnspentOutputs(ns) if err != nil { t.Fatal(err) } if len(minedTxs) != 1 { t.Fatalf("expected 1 mined tx, got %v", len(minedTxs)) } if !minedTxs[0].Hash.IsEqual(&confirmedSpendRec.Hash) { t.Fatalf("expected confirmed tx hash %v, got %v", confirmedSpend, minedTxs[0].Hash) } }) } // TestAddDuplicateCreditAfterConfirm aims to test the case where a duplicate // unconfirmed credit is added to the store after the intial credit has already // confirmed. This can lead to outputs being duplicated in the store, which can // lead to creating double spends when querying the wallet's UTXO set. func TestAddDuplicateCreditAfterConfirm(t *testing.T) { t.Parallel() store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // In order to reproduce real-world scenarios, we'll use a new database // transaction for each interaction with the wallet. // // We'll start off the test by creating a new coinbase output at height // 100 and inserting it into the store. b100 := &BlockMeta{ Block: Block{Height: 100}, Time: time.Now(), } cb := newCoinBase(1e8) cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, cbRec, b100); err != nil { t.Fatal(err) } err := store.AddCredit(ns, cbRec, b100, 0, false) if err != nil { t.Fatal(err) } }) // We'll confirm that there is one unspent output in the store, which // should be the coinbase output created above. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { minedTxs, err := store.UnspentOutputs(ns) if err != nil { t.Fatal(err) } if len(minedTxs) != 1 { t.Fatalf("expected 1 mined tx, got %v", len(minedTxs)) } if !minedTxs[0].Hash.IsEqual(&cbRec.Hash) { t.Fatalf("expected tx hash %v, got %v", cbRec.Hash, minedTxs[0].Hash) } }) // Then, we'll create an unconfirmed spend for the coinbase output. b101 := &BlockMeta{ Block: Block{Height: 101}, Time: time.Now(), } spendTx := spendOutput(&cbRec.Hash, 0, 5e7, 4e7) spendTxRec, err := NewTxRecordFromMsgTx(spendTx, b101.Time) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, spendTxRec, nil); err != nil { t.Fatal(err) } err := store.AddCredit(ns, spendTxRec, nil, 1, true) if err != nil { t.Fatal(err) } }) // Confirm the spending transaction at the next height. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, spendTxRec, b101); err != nil { t.Fatal(err) } err := store.AddCredit(ns, spendTxRec, b101, 1, true) if err != nil { t.Fatal(err) } }) // We should see one unspent output within the store once again, this // time being the change output of the spending transaction. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { minedTxs, err := store.UnspentOutputs(ns) if err != nil { t.Fatal(err) } if len(minedTxs) != 1 { t.Fatalf("expected 1 mined txs, got %v", len(minedTxs)) } if !minedTxs[0].Hash.IsEqual(&spendTxRec.Hash) { t.Fatalf("expected tx hash %v, got %v", spendTxRec.Hash, minedTxs[0].Hash) } }) // Now, we'll insert the spending transaction once again, this time as // unconfirmed. This can happen if the backend happens to forward an // unconfirmed chain.RelevantTx notification to the client even after it // has confirmed, which results in us adding it to the store once again. // // TODO(wilmer): ideally this shouldn't happen, so we should identify // the real reason for this. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, spendTxRec, nil); err != nil { t.Fatal(err) } err := store.AddCredit(ns, spendTxRec, nil, 1, true) if err != nil { t.Fatal(err) } }) // Finally, we'll ensure the change output is still the only unspent // output within the store. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { minedTxs, err := store.UnspentOutputs(ns) if err != nil { t.Fatal(err) } if len(minedTxs) != 1 { t.Fatalf("expected 1 mined txs, got %v", len(minedTxs)) } if !minedTxs[0].Hash.IsEqual(&spendTxRec.Hash) { t.Fatalf("expected tx hash %v, got %v", spendTxRec.Hash, minedTxs[0].Hash) } }) } // TestInsertMempoolTxAndConfirm ensures that there aren't any lingering // unconfirmed records for a transaction that existed within the store as // unconfirmed before becoming confirmed. func TestInsertMempoolTxAndConfirm(t *testing.T) { t.Parallel() store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // Create a transaction which we'll insert into the store as // unconfirmed. tx := newCoinBase(1e8) txRec, err := NewTxRecordFromMsgTx(tx, time.Now()) if err != nil { t.Fatal(err) } commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { if err := store.InsertTx(ns, txRec, nil); err != nil { t.Fatal(err) } err := store.AddCredit(ns, txRec, nil, 0, false) if err != nil { t.Fatal(err) } }) // Then, proceed to confirm it. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { block := &BlockMeta{ Block: Block{Height: 1337}, Time: time.Now(), } if err := store.InsertTx(ns, txRec, block); err != nil { t.Fatal(err) } err := store.AddCredit(ns, txRec, block, 0, false) if err != nil { t.Fatal(err) } }) // We should not see any lingering unconfirmed records for it once it's // been confirmed. commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { for _, input := range tx.TxIn { prevOut := input.PreviousOutPoint k := canonicalOutPoint(&prevOut.Hash, prevOut.Index) if existsRawUnminedInput(ns, k) != nil { t.Fatalf("found transaction input %v as "+ "unconfirmed", prevOut) } } if existsRawUnmined(ns, txRec.Hash[:]) != nil { t.Fatal("found transaction as unconfirmed") } for i := range tx.TxOut { k := canonicalOutPoint(&txRec.Hash, uint32(i)) if existsRawUnminedCredit(ns, k) != nil { t.Fatalf("found transaction output %v as "+ "unconfirmed", i) } } }) } // TestTxLabel tests reading and writing of transaction labels. func TestTxLabel(t *testing.T) { t.Parallel() store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // txid is the transaction hash we will use to write and get labels for. txid := TstRecvTx.Hash() // txidNotFound is distinct from txid, and will not have a label written // to disk. txidNotFound := TstSpendingTx.Hash() // getBucket gets the top level bucket, and fails the test if it is // not found. getBucket := func(tx walletdb.ReadWriteTx) walletdb.ReadWriteBucket { testBucket := tx.ReadWriteBucket(namespaceKey) if testBucket == nil { t.Fatalf("could not get bucket: %v", err) } return testBucket } // tryPutLabel attempts to write a label to disk. tryPutLabel := func(label string) error { return walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { // Try to write the label to disk. return store.PutTxLabel(getBucket(tx), *txid, label) }) } // tryReadLabel attempts to retrieve a label for a given txid. tryReadLabel := func(labelTx chainhash.Hash) (string, error) { var label string err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { var err error label, err = FetchTxLabel(getBucket(tx), labelTx) return err }) return label, err } // First, try to lookup a label when the labels bucket does not exist // yet. _, err = tryReadLabel(*txid) if err != ErrNoLabelBucket { t.Fatalf("expected: %v, got: %v", ErrNoLabelBucket, err) } // Now try to write an empty label, which should fail. err = tryPutLabel("") if err != ErrEmptyLabel { t.Fatalf("expected: %v, got: %v", ErrEmptyLabel, err) } // Create a label which exceeds the length limit. longLabel := make([]byte, TxLabelLimit+1) err = tryPutLabel(string(longLabel)) if err != ErrLabelTooLong { t.Fatalf("expected: %v, got: %v", ErrLabelTooLong, err) } // Write an acceptable length label to disk, this should succeed. testLabel := "test label" err = tryPutLabel(testLabel) if err != nil { t.Fatalf("expected: no error, got: %v", err) } diskLabel, err := tryReadLabel(*txid) if err != nil { t.Fatalf("expected: no error, got: %v", err) } if diskLabel != testLabel { t.Fatalf("expected: %v, got: %v", testLabel, diskLabel) } // Finally, try to read a label for a transaction which does not have // one. _, err = tryReadLabel(*txidNotFound) if err != ErrTxLabelNotFound { t.Fatalf("expected: %v, got: %v", ErrTxLabelNotFound, err) } } func assertBalance(t *testing.T, s *Store, ns walletdb.ReadWriteBucket, confirmed bool, blockHeight int32, exp btcutil.Amount) { t.Helper() minConf := int32(0) if confirmed { minConf = 1 } balance, err := s.Balance(ns, minConf, blockHeight) if err != nil { t.Fatal(err) } if balance != exp { t.Fatalf("expected balance %v, got %v", exp, balance) } } func assertUtxos(t *testing.T, s *Store, ns walletdb.ReadWriteBucket, exp []wire.OutPoint) { t.Helper() utxos, err := s.UnspentOutputs(ns) if err != nil { t.Fatal(err) } for _, expUtxo := range exp { found := false for _, utxo := range utxos { if expUtxo == utxo.OutPoint { found = true break } } if !found { t.Fatalf("expected utxo %v", expUtxo) } } } func assertLocked(t *testing.T, ns walletdb.ReadWriteBucket, op wire.OutPoint, timeNow time.Time, exp bool) { t.Helper() _, _, locked := isLockedOutput(ns, op, timeNow) if locked && locked != exp { t.Fatalf("expected locked output %v", op) } if !locked && locked != exp { t.Fatalf("unexpected locked output %v", op) } } func assertOutputLocksExist(t *testing.T, s *Store, ns walletdb.ReadBucket, exp ...wire.OutPoint) { t.Helper() outputs, err := s.ListLockedOutputs(ns) if err != nil { t.Fatal(err) } if len(outputs) != len(exp) { t.Fatalf("expected to find %v locked output(s), found %v", len(exp), len(outputs)) } for _, expOp := range exp { exists := false for _, found := range outputs { if expOp == found.Outpoint { exists = true break } } if !exists { t.Fatalf("expected output lock for %v to exist", expOp) } } } func lock(t *testing.T, s *Store, ns walletdb.ReadWriteBucket, id LockID, op wire.OutPoint, exp error) time.Time { t.Helper() expiry, err := s.LockOutput(ns, id, op) if err != exp { t.Fatalf("expected err %q, got %q", exp, err) } if exp != nil && exp != ErrOutputAlreadyLocked { assertLocked(t, ns, op, s.clock.Now(), false) } else { assertLocked(t, ns, op, s.clock.Now(), true) } return expiry } func unlock(t *testing.T, s *Store, ns walletdb.ReadWriteBucket, id LockID, op wire.OutPoint, exp error) { t.Helper() if err := s.UnlockOutput(ns, id, op); err != exp { t.Fatalf("expected err %q, got %q", exp, err) } if exp != nil { assertLocked(t, ns, op, s.clock.Now(), true) } else { assertLocked(t, ns, op, s.clock.Now(), false) } } func insertUnconfirmedCredit(t *testing.T, store *Store, db walletdb.DB, tx *wire.MsgTx, idx uint32) { t.Helper() insertConfirmedCredit(t, store, db, tx, idx, nil) } func insertConfirmedCredit(t *testing.T, store *Store, db walletdb.DB, tx *wire.MsgTx, idx uint32, block *BlockMeta) { t.Helper() commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { rec, err := NewTxRecordFromMsgTx(tx, time.Now()) if err != nil { t.Fatal(err) } if err := store.InsertTx(ns, rec, block); err != nil { t.Fatal(err) } if err := store.AddCredit(ns, rec, block, idx, false); err != nil { t.Fatal(err) } }) } // TestOutputLocks aims to test all cases revolving output locks, ensuring they // are and aren't eligible for coin selection after certain operations. func TestOutputLocks(t *testing.T) { t.Parallel() // Define a series of constants we'll use throughout our tests. block := &BlockMeta{ Block: Block{ Hash: chainhash.Hash{1, 3, 3, 7}, Height: 1337, }, Time: time.Now(), } // Create a coinbase transaction with two outputs, which we'll spend. coinbase := newCoinBase( btcutil.SatoshiPerBitcoin, btcutil.SatoshiPerBitcoin*2, ) coinbaseHash := coinbase.TxHash() // One of the spends will be unconfirmed. const unconfirmedBalance = btcutil.SatoshiPerBitcoin / 2 unconfirmedTx := spendOutput(&coinbaseHash, 0, unconfirmedBalance) unconfirmedOutPoint := wire.OutPoint{ Hash: unconfirmedTx.TxHash(), Index: 0, } // The other will be confirmed. const confirmedBalance = btcutil.SatoshiPerBitcoin confirmedTx := spendOutput(&coinbaseHash, 1, confirmedBalance) confirmedOutPoint := wire.OutPoint{ Hash: confirmedTx.TxHash(), Index: 0, } const balance = unconfirmedBalance + confirmedBalance testCases := []struct { name string run func(*testing.T, *Store, walletdb.ReadWriteBucket) }{ { // Asserts that we cannot lock unknown outputs to the // store. name: "unknown output", run: func(t *testing.T, s *Store, ns walletdb.ReadWriteBucket) { lockID := LockID{1} op := wire.OutPoint{Index: 1} _ = lock(t, s, ns, lockID, op, ErrUnknownOutput) }, }, { // Asserts that we cannot lock outputs that have already // been locked to someone else. name: "already locked output", run: func(t *testing.T, s *Store, ns walletdb.ReadWriteBucket) { lockID1 := LockID{1} lockID2 := LockID{2} _ = lock( t, s, ns, lockID1, unconfirmedOutPoint, nil, ) _ = lock( t, s, ns, lockID2, unconfirmedOutPoint, ErrOutputAlreadyLocked, ) _ = lock( t, s, ns, lockID1, confirmedOutPoint, nil, ) _ = lock( t, s, ns, lockID2, confirmedOutPoint, ErrOutputAlreadyLocked, ) }, }, { // Asserts that only the ID which locked at output can // manually unlock it. name: "unlock output", run: func(t *testing.T, s *Store, ns walletdb.ReadWriteBucket) { lockID1 := LockID{1} lockID2 := LockID{2} _ = lock(t, s, ns, lockID1, confirmedOutPoint, nil) unlock( t, s, ns, lockID2, confirmedOutPoint, ErrOutputUnlockNotAllowed, ) unlock(t, s, ns, lockID1, confirmedOutPoint, nil) }, }, { // Asserts that locking an output that's already locked // with the correct ID results in an extension of the // lock. name: "extend locked output lease", run: func(t *testing.T, s *Store, ns walletdb.ReadWriteBucket) { // Lock the output and set the clock time a // minute before the expiration. It should // remain locked. lockID := LockID{1} expiry := lock( t, s, ns, lockID, confirmedOutPoint, nil, ) s.clock.(*clock.TestClock).SetTime( expiry.Add(-time.Minute), ) assertLocked( t, ns, confirmedOutPoint, s.clock.Now(), true, ) // Lock it once again, extending its expiration, // and set the clock time a second before the // expiration. It should remain locked. s.clock.(*clock.TestClock).SetTime( expiry.Add(-time.Minute), ) newExpiry := lock( t, s, ns, lockID, confirmedOutPoint, nil, ) if !newExpiry.After(expiry) { t.Fatal("expected output lock " + "duration to be renewed") } s.clock.(*clock.TestClock).SetTime( newExpiry.Add(-time.Second), ) assertLocked( t, ns, confirmedOutPoint, s.clock.Now(), true, ) // Set the clock time to the new expiration, it // should now be unlocked. s.clock.(*clock.TestClock).SetTime(newExpiry) assertLocked( t, ns, confirmedOutPoint, s.clock.Now(), false, ) }, }, { // Asserts that balances are reflected properly after // locking confirmed and unconfirmed outputs. name: "balance after locked outputs", run: func(t *testing.T, s *Store, ns walletdb.ReadWriteBucket) { // We should see our full balance before locking // any outputs. assertBalance( t, s, ns, false, block.Height, balance, ) // Lock all of our outputs. Our balance should // be 0. lockID := LockID{1} _ = lock( t, s, ns, lockID, unconfirmedOutPoint, nil, ) expiry := lock( t, s, ns, lockID, confirmedOutPoint, nil, ) assertBalance(t, s, ns, false, block.Height, 0) // Wait for the output locks to expire, causing // our full balance to return . s.clock.(*clock.TestClock).SetTime(expiry) assertBalance( t, s, ns, false, block.Height, balance, ) }, }, { // Asserts that the available utxos are reflected // properly after locking confirmed and unconfirmed // outputs. name: "utxos after locked outputs", run: func(t *testing.T, s *Store, ns walletdb.ReadWriteBucket) { // We should see all of our utxos before locking // any. assertUtxos(t, s, ns, []wire.OutPoint{ unconfirmedOutPoint, confirmedOutPoint, }) // Lock the unconfirmed utxo, we should now only // see the confirmed. lockID := LockID{1} _ = lock(t, s, ns, lockID, unconfirmedOutPoint, nil) assertUtxos(t, s, ns, []wire.OutPoint{ confirmedOutPoint, }) // Now lock the confirmed utxo, we should no // longer see any utxos available. expiry := lock( t, s, ns, lockID, confirmedOutPoint, nil, ) assertUtxos(t, s, ns, nil) // Wait for the output locks to expire for the // utxos to become available once again. s.clock.(*clock.TestClock).SetTime(expiry) assertUtxos(t, s, ns, []wire.OutPoint{ unconfirmedOutPoint, confirmedOutPoint, }) }, }, { // Asserts that output locks are removed for outputs // which have had a confirmed spend, ensuring the // database doesn't store stale data. name: "clear locked outputs after confirmed spend", run: func(t *testing.T, s *Store, ns walletdb.ReadWriteBucket) { // Lock an output. lockID := LockID{1} lock(t, s, ns, lockID, confirmedOutPoint, nil) // Create a spend and add it to the store as // confirmed. txHash := confirmedTx.TxHash() spendTx := spendOutput(&txHash, 0, 500) spendRec, err := NewTxRecordFromMsgTx( spendTx, time.Now(), ) if err != nil { t.Fatal(err) } err = s.InsertTx(ns, spendRec, block) if err != nil { t.Fatal(err) } // The output should no longer be locked. assertLocked( t, ns, confirmedOutPoint, s.clock.Now(), false, ) }, }, { // Assert that deleting expired locked outputs works as // intended. name: "delete expired locked outputs", run: func(t *testing.T, s *Store, ns walletdb.ReadWriteBucket) { // Lock an output. lockID := LockID{1} expiry := lock( t, s, ns, lockID, confirmedOutPoint, nil, ) // We should expect to find it if we iterate // over the locked outputs bucket. assertOutputLocksExist(t, s, ns, confirmedOutPoint) // Delete all expired locked outputs. Since the // lock hasn't expired yet, it should still // exist. err := s.DeleteExpiredLockedOutputs(ns) if err != nil { t.Fatalf("unable to delete expired "+ "locked outputs: %v", err) } assertOutputLocksExist(t, s, ns, confirmedOutPoint) // Let the output lock expired. s.clock.(*clock.TestClock).SetTime(expiry) // Delete all expired locked outputs. We should // no longer see any locked outputs. err = s.DeleteExpiredLockedOutputs(ns) if err != nil { t.Fatalf("unable to delete expired "+ "locked outputs: %v", err) } assertOutputLocksExist(t, s, ns) }, }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() store, db, teardown, err := testStore() if err != nil { t.Fatal(err) } defer teardown() // Replace the store's default clock with a mock one in // order to simulate a real clock and speed up our // tests. store.clock = clock.NewTestClock(time.Time{}) // Add the spends we created above to the store. insertConfirmedCredit(t, store, db, confirmedTx, 0, block) insertUnconfirmedCredit(t, store, db, unconfirmedTx, 0) // Run the test! commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { testCase.run(t, store, ns) }) }) } }