diff --git a/chain/db.go b/chain/db.go index b1c3667..55035ac 100644 --- a/chain/db.go +++ b/chain/db.go @@ -647,25 +647,17 @@ func (db *DBStore) shouldFlush() bool { return db.unflushed >= flushSizeThreshold || time.Since(db.lastFlush) >= flushDurationThreshold } -func (db *DBStore) flush() { - if err := db.db.Flush(); err != nil { - panic(err) - } - db.unflushed = 0 - db.lastFlush = time.Now() -} - // ApplyBlock implements Store. -func (db *DBStore) ApplyBlock(s consensus.State, cau consensus.ApplyUpdate, mustCommit bool) (committed bool) { +func (db *DBStore) ApplyBlock(s consensus.State, cau consensus.ApplyUpdate) { db.applyState(s) if s.Index.Height <= db.n.HardforkV2.RequireHeight { db.applyElements(cau) } - committed = mustCommit || db.shouldFlush() - if committed { - db.flush() + if db.shouldFlush() { + if err := db.Flush(); err != nil { + panic(err) + } } - return } // RevertBlock implements Store. @@ -675,12 +667,19 @@ func (db *DBStore) RevertBlock(s consensus.State, cru consensus.RevertUpdate) { } db.revertState(s) if db.shouldFlush() { - db.flush() + if err := db.Flush(); err != nil { + panic(err) + } } } -// Close flushes any uncommitted data to the underlying DB. -func (db *DBStore) Close() error { +// Flush flushes any uncommitted data to the underlying DB. +func (db *DBStore) Flush() error { + if db.unflushed == 0 { + return nil + } + db.unflushed = 0 + db.lastFlush = time.Now() return db.db.Flush() } @@ -731,7 +730,10 @@ func NewDBStore(db DB, n *consensus.Network, genesisBlock types.Block) (_ *DBSto cs, cau := consensus.ApplyBlock(genesisState, genesisBlock, bs, time.Time{}) dbs.putBlock(genesisBlock, &bs) dbs.putState(cs) - dbs.ApplyBlock(cs, cau, true) + dbs.ApplyBlock(cs, cau) + if err := dbs.Flush(); err != nil { + return nil, consensus.State{}, err + } } else if dbGenesis.ID != genesisBlock.ID() { // try to detect network so we can provide a more helpful error message _, mainnetGenesis := Mainnet() diff --git a/chain/manager.go b/chain/manager.go index 202f07b..41a7009 100644 --- a/chain/manager.go +++ b/chain/manager.go @@ -9,6 +9,7 @@ import ( "go.sia.tech/core/consensus" "go.sia.tech/core/types" + "lukechampine.com/frand" ) var ( @@ -34,14 +35,6 @@ type RevertUpdate struct { State consensus.State // post-reversion, i.e. pre-application } -// A Subscriber processes updates to the blockchain. Implementations must not -// modify or retain the provided update object. -type Subscriber interface { - // Implementations MUST not commit updates to persistent storage unless mayCommit is set. - ProcessChainApplyUpdate(cau *ApplyUpdate, mayCommit bool) error - ProcessChainRevertUpdate(cru *RevertUpdate) error -} - // A Store durably commits Manager-related data to storage. I/O errors must be // handled internally, e.g. by panicking or calling os.Exit. type Store interface { @@ -55,10 +48,10 @@ type Store interface { AddState(cs consensus.State) AncestorTimestamp(id types.BlockID) (time.Time, bool) - // Except when mustCommit is set, ApplyBlock and RevertBlock are free to - // commit whenever they see fit. - ApplyBlock(s consensus.State, cau consensus.ApplyUpdate, mustCommit bool) (committed bool) + // ApplyBlock and RevertBlock are free to commit whenever they see fit. + ApplyBlock(s consensus.State, cau consensus.ApplyUpdate) RevertBlock(s consensus.State, cru consensus.RevertUpdate) + Flush() error } // blockAndParent returns the block with the specified ID, along with its parent @@ -82,8 +75,7 @@ func blockAndChild(s Store, id types.BlockID) (types.Block, *consensus.V1BlockSu type Manager struct { store Store tipState consensus.State - subscribers []Subscriber - lastCommit time.Time + onReorg map[[16]byte]func(types.ChainIndex) invalidBlocks map[types.BlockID]error txpool struct { @@ -247,6 +239,13 @@ func (m *Manager) AddBlocks(blocks []types.Block) error { } return fmt.Errorf("reorg failed: %w", err) } + // release lock while notifying listeners + tip := m.tipState.Index + m.mu.Unlock() + for _, fn := range m.onReorg { + fn(tip) + } + m.mu.Lock() } return nil } @@ -273,15 +272,7 @@ func (m *Manager) revertTip() error { } cru := consensus.RevertBlock(cs, b, *bs) m.store.RevertBlock(cs, cru) - - update := RevertUpdate{cru, b, cs} - for _, s := range m.subscribers { - if err := s.ProcessChainRevertUpdate(&update); err != nil { - return fmt.Errorf("subscriber %T: %w", s, err) - } - } - - m.revertPoolUpdate(&update) + m.revertPoolUpdate(cru, cs) m.tipState = cs return nil } @@ -316,23 +307,8 @@ func (m *Manager) applyTip(index types.ChainIndex) error { _, cau = consensus.ApplyBlock(m.tipState, b, *bs, ancestorTimestamp) } - // force the store to commit if we're at the tip (or close to it), or at - // least every 2 seconds; this ensures that the amount of uncommitted data - // never grows too large - forceCommit := time.Since(b.Timestamp) < cs.BlockInterval()*2 || time.Since(m.lastCommit) > 2*time.Second - committed := m.store.ApplyBlock(cs, cau, forceCommit) - if committed { - m.lastCommit = time.Now() - } - - update := &ApplyUpdate{cau, b, cs} - for _, s := range m.subscribers { - if err := s.ProcessChainApplyUpdate(update, committed); err != nil { - return fmt.Errorf("subscriber %T: %w", s, err) - } - } - - m.applyPoolUpdate(update) + m.store.ApplyBlock(cs, cau) + m.applyPoolUpdate(cau, cs) m.tipState = cs return nil } @@ -403,6 +379,9 @@ func (m *Manager) reorgTo(index types.ChainIndex) error { return fmt.Errorf("couldn't apply block %v: %w", index, err) } } + if err := m.store.Flush(); err != nil { + return err + } // invalidate txpool caches m.txpool.ms = nil @@ -413,65 +392,68 @@ func (m *Manager) reorgTo(index types.ChainIndex) error { m.txpool.lastReverted = b.Transactions m.txpool.lastRevertedV2 = b.V2Transactions() } - return nil } -// AddSubscriber subscribes s to m, ensuring that it will receive updates when -// the best chain changes. If tip does not match the Manager's current tip, s is -// updated accordingly. -func (m *Manager) AddSubscriber(s Subscriber, tip types.ChainIndex) error { +// UpdatesSince returns at most max updates on the path between index and the +// Manager's current tip. +func (m *Manager) UpdatesSince(index types.ChainIndex, max int) (rus []RevertUpdate, aus []ApplyUpdate, err error) { m.mu.Lock() defer m.mu.Unlock() - - // reorg s to the current tip, if necessary - revert, apply, err := m.reorgPath(tip, m.tipState.Index) - if err != nil { - return fmt.Errorf("couldn't determine reorg path from %v to %v: %w", tip, m.tipState.Index, err) - } - for _, index := range revert { - b, bs, cs, ok := blockAndParent(m.store, index.ID) - if !ok { - return fmt.Errorf("missing reverted block at index %v", index) - } else if bs == nil { - panic("missing supplement for reverted block") - } - cru := consensus.RevertBlock(cs, b, *bs) - if err := s.ProcessChainRevertUpdate(&RevertUpdate{cru, b, cs}); err != nil { - return fmt.Errorf("couldn't process revert update: %w", err) - } + onBestChain := func(index types.ChainIndex) bool { + bi, _ := m.store.BestIndex(index.Height) + return bi.ID == index.ID || index == types.ChainIndex{} } - for _, index := range apply { - b, bs, cs, ok := blockAndParent(m.store, index.ID) - if !ok { - return fmt.Errorf("missing applied block at index %v", index) - } else if bs == nil { - panic("missing supplement for applied block") - } - ancestorTimestamp, ok := m.store.AncestorTimestamp(b.ParentID) - if !ok && index.Height != 0 { - return fmt.Errorf("missing ancestor timestamp for block %v", b.ParentID) - } - cs, cau := consensus.ApplyBlock(cs, b, *bs, ancestorTimestamp) - // TODO: commit every minute for large len(apply)? - shouldCommit := index == m.tipState.Index - if err := s.ProcessChainApplyUpdate(&ApplyUpdate{cau, b, cs}, shouldCommit); err != nil { - return fmt.Errorf("couldn't process apply update: %w", err) + + for index != m.tipState.Index && len(rus)+len(aus) <= max { + // revert until we are on the best chain, then apply + if !onBestChain(index) { + b, bs, cs, ok := blockAndParent(m.store, index.ID) + if !ok { + return nil, nil, fmt.Errorf("missing block at index %v", index) + } else if bs == nil { + return nil, nil, fmt.Errorf("missing supplement for block %v", index) + } + cru := consensus.RevertBlock(cs, b, *bs) + rus = append(rus, RevertUpdate{cru, b, cs}) + index = cs.Index + } else { + // special case: if index is uninitialized, we're starting from genesis + if index == (types.ChainIndex{}) { + index, _ = m.store.BestIndex(0) + } else { + index, _ = m.store.BestIndex(index.Height + 1) + } + b, bs, cs, ok := blockAndParent(m.store, index.ID) + if !ok { + return nil, nil, fmt.Errorf("missing block at index %v", index) + } else if bs == nil { + return nil, nil, fmt.Errorf("missing supplement for block %v", index) + } + ancestorTimestamp, ok := m.store.AncestorTimestamp(b.ParentID) + if !ok && index.Height != 0 { + return nil, nil, fmt.Errorf("missing ancestor timestamp for block %v", b.ParentID) + } + cs, cau := consensus.ApplyBlock(cs, b, *bs, ancestorTimestamp) + aus = append(aus, ApplyUpdate{cau, b, cs}) } } - m.subscribers = append(m.subscribers, s) - return nil + return } -// RemoveSubscriber unsubscribes s from m. -func (m *Manager) RemoveSubscriber(s Subscriber) { +// OnReorg adds fn to the set of functions that are called whenever the best +// chain changes. It returns a function that removes fn from the set. +// +// The supplied function must not block or call any Manager methods. +func (m *Manager) OnReorg(fn func(types.ChainIndex)) (cancel func()) { m.mu.Lock() defer m.mu.Unlock() - for i := range m.subscribers { - if m.subscribers[i] == s { - m.subscribers = append(m.subscribers[:i], m.subscribers[i+1:]...) - return - } + key := frand.Entropy128() + m.onReorg[key] = fn + return func() { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.onReorg, key) } } @@ -654,7 +636,7 @@ func updateTxnProofs(txn *types.V2Transaction, updateElementProof func(*types.St return } -func (m *Manager) revertPoolUpdate(cru *RevertUpdate) { +func (m *Manager) revertPoolUpdate(cru consensus.RevertUpdate, cs consensus.State) { // restore ephemeral elements, if necessary var uncreated map[types.Hash256]bool replaceEphemeral := func(e *types.StateElement) { @@ -704,14 +686,14 @@ func (m *Manager) revertPoolUpdate(cru *RevertUpdate) { rem := m.txpool.v2txns[:0] for _, txn := range m.txpool.v2txns { - if updateTxnProofs(&txn, cru.UpdateElementProof, cru.State.Elements.NumLeaves) { + if updateTxnProofs(&txn, cru.UpdateElementProof, cs.Elements.NumLeaves) { rem = append(rem, txn) } } m.txpool.v2txns = rem } -func (m *Manager) applyPoolUpdate(cau *ApplyUpdate) { +func (m *Manager) applyPoolUpdate(cau consensus.ApplyUpdate, cs consensus.State) { // replace ephemeral elements, if necessary var newElements map[types.Hash256]types.StateElement replaceEphemeral := func(e *types.StateElement) { @@ -759,7 +741,7 @@ func (m *Manager) applyPoolUpdate(cau *ApplyUpdate) { rem := m.txpool.v2txns[:0] for _, txn := range m.txpool.v2txns { - if updateTxnProofs(&txn, cau.UpdateElementProof, cau.State.Elements.NumLeaves) { + if updateTxnProofs(&txn, cau.UpdateElementProof, cs.Elements.NumLeaves) { rem = append(rem, txn) } } @@ -1113,7 +1095,7 @@ func NewManager(store Store, cs consensus.State) *Manager { m := &Manager{ store: store, tipState: cs, - lastCommit: time.Now(), + onReorg: make(map[[16]byte]func(types.ChainIndex)), invalidBlocks: make(map[types.BlockID]error), } m.txpool.indices = make(map[types.TransactionID]int) diff --git a/chain/manager_test.go b/chain/manager_test.go index 4dfcdcf..d90639d 100644 --- a/chain/manager_test.go +++ b/chain/manager_test.go @@ -1,6 +1,7 @@ package chain import ( + "fmt" "reflect" "testing" @@ -19,21 +20,6 @@ func findBlockNonce(cs consensus.State, b *types.Block) { } } -type historySubscriber struct { - revertHistory []uint64 - applyHistory []uint64 -} - -func (hs *historySubscriber) ProcessChainApplyUpdate(cau *ApplyUpdate, _ bool) error { - hs.applyHistory = append(hs.applyHistory, cau.State.Index.Height) - return nil -} - -func (hs *historySubscriber) ProcessChainRevertUpdate(cru *RevertUpdate) error { - hs.revertHistory = append(hs.revertHistory, cru.State.Index.Height) - return nil -} - func TestManager(t *testing.T) { n, genesisBlock := TestnetZen() @@ -43,12 +29,8 @@ func TestManager(t *testing.T) { if err != nil { t.Fatal(err) } - defer store.Close() cm := NewManager(store, tipState) - var hs historySubscriber - cm.AddSubscriber(&hs, cm.Tip()) - mine := func(cs consensus.State, n int) (blocks []types.Block) { for i := 0; i < n; i++ { b := types.Block{ @@ -67,6 +49,11 @@ func TestManager(t *testing.T) { return } + var reorgs []uint64 + cm.OnReorg(func(index types.ChainIndex) { + reorgs = append(reorgs, index.Height) + }) + // mine two chains chain1 := mine(cm.TipState(), 5) chain2 := mine(cm.TipState(), 7) @@ -78,25 +65,42 @@ func TestManager(t *testing.T) { if err := cm.AddBlocks(chain2); err != nil { t.Fatal(err) } + if !reflect.DeepEqual(reorgs, []uint64{5, 7}) { + t.Error("wrong reorg history:", reorgs) + } - // subscriber history should show the reorg - if !reflect.DeepEqual(hs.revertHistory, []uint64{4, 3, 2, 1, 0}) { - t.Error("lighter chain should have been reverted:", hs.revertHistory) - } else if !reflect.DeepEqual(hs.applyHistory, []uint64{1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6, 7}) { - t.Error("both chains should have been applied:", hs.applyHistory) + // get full update history + rus, aus, err := cm.UpdatesSince(types.ChainIndex{}, 100) + if err != nil { + t.Fatal(err) + } else if len(rus) != 0 && len(aus) != 8 { + t.Fatal("wrong number of updates:", len(rus), len(aus)) + } + var path []uint64 + for _, au := range aus { + path = append(path, au.State.Index.Height) + } + if !reflect.DeepEqual(path, []uint64{0, 1, 2, 3, 4, 5, 6, 7}) { + t.Error("wrong update path:", path) } - // add a subscriber whose tip is in the middle of the lighter chain - subTip := types.ChainIndex{Height: 3, ID: chain1[3].ParentID} - var hs2 historySubscriber - if err := cm.AddSubscriber(&hs2, subTip); err != nil { + // get update history from the middle of the lighter chain + rus, aus, err = cm.UpdatesSince(types.ChainIndex{Height: 3, ID: chain1[3].ParentID}, 100) + if err != nil { t.Fatal(err) + } else if len(rus) != 3 && len(aus) != 7 { + t.Fatal("wrong number of updates:", len(rus), len(aus)) + } + path = nil + for _, ru := range rus { + path = append(path, ru.State.Index.Height) + fmt.Println(path) + } + for _, au := range aus { + path = append(path, au.State.Index.Height) } - // check that the subscriber was properly synced - if !reflect.DeepEqual(hs2.revertHistory, []uint64{2, 1, 0}) { - t.Fatal("3 blocks should have been reverted:", hs2.revertHistory) - } else if !reflect.DeepEqual(hs2.applyHistory, []uint64{1, 2, 3, 4, 5, 6, 7}) { - t.Fatal("7 blocks should have been applied:", hs2.applyHistory) + if !reflect.DeepEqual(path, []uint64{2, 1, 0, 1, 2, 3, 4, 5, 6, 7}) { + t.Error("wrong update path:", path) } } diff --git a/miner_test.go b/miner_test.go index c25b4f8..2bb4c73 100644 --- a/miner_test.go +++ b/miner_test.go @@ -26,7 +26,6 @@ func TestMiner(t *testing.T) { if err != nil { t.Fatal(err) } - defer store.Close() cm := chain.NewManager(store, tipState) // create a transaction diff --git a/testutil/network.go b/testutil/network.go index 5071725..450ec68 100644 --- a/testutil/network.go +++ b/testutil/network.go @@ -17,8 +17,8 @@ func Network() (*consensus.Network, types.Block) { n.HardforkOak.Height = 1 n.HardforkASIC.Height = 1 n.HardforkFoundation.Height = 1 - n.HardforkV2.AllowHeight = 100 - n.HardforkV2.RequireHeight = 150 + n.HardforkV2.AllowHeight = 200 // comfortably above MaturityHeight + n.HardforkV2.RequireHeight = 250 return n, genesisBlock } diff --git a/testutil/wallet.go b/testutil/wallet.go index 75b40e3..6783ad6 100644 --- a/testutil/wallet.go +++ b/testutil/wallet.go @@ -140,36 +140,22 @@ func (es *EphemeralWalletStore) Tip() (types.ChainIndex, error) { } // ProcessChainApplyUpdate implements chain.Subscriber. -func (es *EphemeralWalletStore) ProcessChainApplyUpdate(cau *chain.ApplyUpdate, mayCommit bool) error { +func (es *EphemeralWalletStore) ProcessChainApplyUpdate(cau chain.ApplyUpdate) error { es.mu.Lock() defer es.mu.Unlock() - - es.uncommitted = append(es.uncommitted, cau) - if !mayCommit { - return nil - } - address := types.StandardUnlockHash(es.privateKey.PublicKey()) - ephemeralWalletUpdateTxn := &ephemeralWalletUpdateTxn{store: es} - - if err := wallet.ApplyChainUpdates(ephemeralWalletUpdateTxn, address, es.uncommitted); err != nil { + if err := wallet.ApplyChainUpdates(&ephemeralWalletUpdateTxn{store: es}, address, []chain.ApplyUpdate{cau}); err != nil { return err } es.tip = cau.State.Index - es.uncommitted = nil return nil } // ProcessChainRevertUpdate implements chain.Subscriber. -func (es *EphemeralWalletStore) ProcessChainRevertUpdate(cru *chain.RevertUpdate) error { +func (es *EphemeralWalletStore) ProcessChainRevertUpdate(cru chain.RevertUpdate) error { es.mu.Lock() defer es.mu.Unlock() - if len(es.uncommitted) > 0 && es.uncommitted[len(es.uncommitted)-1].State.Index == cru.State.Index { - es.uncommitted = es.uncommitted[:len(es.uncommitted)-1] - return nil - } - address := types.StandardUnlockHash(es.privateKey.PublicKey()) return wallet.RevertChainUpdate(&ephemeralWalletUpdateTxn{store: es}, address, cru) } diff --git a/wallet/update.go b/wallet/update.go index 3f3c6ed..4a21a20 100644 --- a/wallet/update.go +++ b/wallet/update.go @@ -50,7 +50,7 @@ type ( // addressPayoutEvents is a helper to add all payout transactions from an // apply update to a slice of transactions. -func addressPayoutEvents(addr types.Address, cau *chain.ApplyUpdate) (events []Event) { +func addressPayoutEvents(addr types.Address, cau chain.ApplyUpdate) (events []Event) { index := cau.State.Index state := cau.State block := cau.Block @@ -144,7 +144,7 @@ func addressPayoutEvents(addr types.Address, cau *chain.ApplyUpdate) (events []E } // ApplyChainUpdates atomically applies a batch of wallet updates -func ApplyChainUpdates(tx ApplyTx, address types.Address, updates []*chain.ApplyUpdate) error { +func ApplyChainUpdates(tx ApplyTx, address types.Address, updates []chain.ApplyUpdate) error { stateElements, err := tx.WalletStateElements() if err != nil { return fmt.Errorf("failed to get state elements: %w", err) @@ -243,7 +243,7 @@ func ApplyChainUpdates(tx ApplyTx, address types.Address, updates []*chain.Apply } // RevertChainUpdate atomically reverts a chain update from a wallet -func RevertChainUpdate(tx RevertTx, address types.Address, cru *chain.RevertUpdate) error { +func RevertChainUpdate(tx RevertTx, address types.Address, cru chain.RevertUpdate) error { stateElements, err := tx.WalletStateElements() if err != nil { return fmt.Errorf("failed to get state elements: %w", err) diff --git a/wallet/wallet.go b/wallet/wallet.go index 148074b..e308320 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -9,7 +9,6 @@ import ( "go.sia.tech/core/consensus" "go.sia.tech/core/types" - "go.sia.tech/coreutils/chain" "go.uber.org/zap" ) @@ -76,18 +75,13 @@ type ( ChainManager interface { TipState() consensus.State BestIndex(height uint64) (types.ChainIndex, bool) - PoolTransactions() []types.Transaction - - AddSubscriber(chain.Subscriber, types.ChainIndex) error - RemoveSubscriber(chain.Subscriber) + OnReorg(func(types.ChainIndex)) func() } // A SingleAddressStore stores the state of a single-address wallet. // Implementations are assumed to be thread safe. SingleAddressStore interface { - chain.Subscriber - // Tip returns the consensus change ID and block height of // the last wallet change. Tip() (types.ChainIndex, error) @@ -151,7 +145,7 @@ func (t *Event) DecodeFrom(d *types.Decoder) { // Close closes the wallet func (sw *SingleAddressWallet) Close() error { - sw.cm.RemoveSubscriber(sw.store) + // TODO: remove subscription?? return nil } @@ -571,7 +565,9 @@ func (sw *SingleAddressWallet) Redistribute(outputs int, amount, feePerByte type } // set the miner fee - txn.MinerFees = []types.Currency{fee} + if !fee.IsZero() { + txn.MinerFees = []types.Currency{fee} + } // add the change output change := SumOutputs(inputs).Sub(want.Add(fee)) diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 5b73be2..ce1e64d 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -2,6 +2,7 @@ package wallet_test import ( "errors" + "fmt" "testing" "go.sia.tech/core/types" @@ -14,7 +15,7 @@ import ( type testWallet struct { t *testing.T cm *chain.Manager - store wallet.SingleAddressStore + store *testutil.EphemeralWalletStore *wallet.SingleAddressWallet } @@ -33,11 +34,6 @@ func newTestWallet(t *testing.T, funded bool) *testWallet { // create chain manager and subscribe the wallet cm := chain.NewManager(cs, tipState) - err = cm.AddSubscriber(ws, types.ChainIndex{}) - if err != nil { - t.Fatal(err) - } - // create wallet l := zaptest.NewLogger(t) w, err := wallet.NewSingleAddressWallet(pk, cm, ws, wallet.WithLogger(l.Named("wallet"))) @@ -45,49 +41,55 @@ func newTestWallet(t *testing.T, funded bool) *testWallet { t.Fatal(err) } + tw := &testWallet{t, cm, ws, w} + if funded { // mine a block to fund the wallet - b := testutil.MineBlock(cm, w.Address()) - if err := cm.AddBlocks([]types.Block{b}); err != nil { + if err := tw.mineBlock(w.Address()); err != nil { t.Fatal(err) } - // mine until the payout matures - tip := cm.TipState() - target := tip.MaturityHeight() + 1 - for i := tip.Index.Height; i < target; i++ { - b := testutil.MineBlock(cm, types.VoidAddress) - if err := cm.AddBlocks([]types.Block{b}); err != nil { + maturityHeight := cm.TipState().MaturityHeight() + for cm.Tip().Height != maturityHeight { + if err := tw.mineBlock(types.VoidAddress); err != nil { t.Fatal(err) } } } + return tw +} - return &testWallet{t, cm, ws, w} +func (w *testWallet) mineBlock(addr types.Address) error { + tip := w.cm.Tip() + if err := w.cm.AddBlocks([]types.Block{testutil.MineBlock(w.cm, addr)}); err != nil { + return err + } + _, caus, err := w.cm.UpdatesSince(tip, 1) + if err != nil { + return err + } + if err := w.store.ProcessChainApplyUpdate(caus[0]); err != nil { + return fmt.Errorf("failed to process apply update: %w", err) + } + return nil } // redistribute creates a transaction that redistributes the wallet's balance // into n outputs of amount, and mines a block to confirm the transaction. func (w *testWallet) redistribute(n int, amount types.Currency) error { // redistribute & sign - txns, toSign, err := w.Redistribute(n, amount, types.NewCurrency64(1)) + txns, toSign, err := w.Redistribute(n, amount, types.ZeroCurrency) if err != nil { return err - } else { - for i := 0; i < len(txns); i++ { - w.SignTransaction(&txns[i], toSign, types.CoveredFields{WholeTransaction: true}) - } } - - // add txn to the pool - _, err = w.cm.AddPoolTransactions(txns) - if err != nil { + for i := 0; i < len(txns); i++ { + w.SignTransaction(&txns[i], toSign, types.CoveredFields{WholeTransaction: true}) + } + // mine txn + if _, err := w.cm.AddPoolTransactions(txns); err != nil { return err } - - // mine a block - b := testutil.MineBlock(w.cm, w.Address()) - return w.cm.AddBlocks([]types.Block{b}) + return w.mineBlock(types.VoidAddress) } // assertBalance compares the wallet's balance to the expected values. @@ -108,6 +110,7 @@ func (w *testWallet) assertBalance(spendable, confirmed, immature, unconfirmed t // assertOutputs checks that the wallet has the expected number of outputs with given value. func (w *testWallet) assertOutputs(n int, amount types.Currency) { + w.t.Helper() // assert outputs utxos, err := w.store.UnspentSiacoinElements() if err != nil { @@ -116,7 +119,7 @@ func (w *testWallet) assertOutputs(n int, amount types.Currency) { var cnt int for _, utxo := range utxos { if utxo.SiacoinOutput.Value.Equals(amount) { - n-- + cnt++ } } if cnt != n { @@ -136,11 +139,7 @@ func TestWallet(t *testing.T) { w.assertBalance(types.ZeroCurrency, types.ZeroCurrency, types.ZeroCurrency, types.ZeroCurrency) // mine a block to fund the wallet - b := testutil.MineBlock(cm, w.Address()) - if err := cm.AddBlocks([]types.Block{b}); err != nil { - t.Fatal(err) - } - + w.mineBlock(w.Address()) maturityHeight := cm.TipState().MaturityHeight() // check that the wallet has a single event if events, err := w.Events(0, 100); err != nil { @@ -178,10 +177,7 @@ func TestWallet(t *testing.T) { tip := cm.TipState() target := tip.MaturityHeight() + 1 for i := tip.Index.Height; i < target; i++ { - b := testutil.MineBlock(cm, types.VoidAddress) - if err := cm.AddBlocks([]types.Block{b}); err != nil { - t.Fatal(err) - } + w.mineBlock(types.VoidAddress) } // check that one payout has matured @@ -250,10 +246,7 @@ func TestWallet(t *testing.T) { w.assertBalance(types.ZeroCurrency, initialReward, types.ZeroCurrency, initialReward) // mine a block to confirm the transaction - b = testutil.MineBlock(cm, types.VoidAddress) - if err := cm.AddBlocks([]types.Block{b}); err != nil { - t.Fatal(err) - } + w.mineBlock(types.VoidAddress) // check that the balance was confirmed and the other values reset w.assertBalance(initialReward, initialReward, types.ZeroCurrency, types.ZeroCurrency) @@ -297,10 +290,7 @@ func TestWallet(t *testing.T) { t.Fatal(err) } - b = testutil.MineBlock(cm, types.VoidAddress) - if err := cm.AddBlocks([]types.Block{b}); err != nil { - t.Fatal(err) - } + w.mineBlock(types.VoidAddress) // check that the wallet now has 22 transactions, the initial payout // transaction, the split transaction, and 20 void transactions @@ -413,13 +403,13 @@ func TestWalletRedistribute(t *testing.T) { t.Fatalf("expected one output, got %v", len(utxos)) } - // redistribute the wallet into 3 outputs of 75KS + // redistribute the wallet into 4 outputs of 75KS amount := types.Siacoins(75e3) - err = w.redistribute(3, amount) + err = w.redistribute(4, amount) if err != nil { t.Fatal(err) } - w.assertOutputs(3, amount) + w.assertOutputs(4, amount) // redistribute the wallet into 4 outputs of 50KS amount = types.Siacoins(50e3) @@ -429,14 +419,14 @@ func TestWalletRedistribute(t *testing.T) { } w.assertOutputs(4, amount) - // redistribute the wallet into 3 outputs of 100KS - expect ErrNotEnoughFunds - err = w.redistribute(3, types.Siacoins(100e3)) + // redistribute the wallet into 3 outputs of 101KS - expect ErrNotEnoughFunds + err = w.redistribute(3, types.Siacoins(101e3)) if !errors.Is(err, wallet.ErrNotEnoughFunds) { t.Fatal(err) } // redistribute the wallet into 3 outputs of 50KS - assert this is a no-op - txns, toSign, err := w.Redistribute(3, amount, types.NewCurrency64(1)) + txns, toSign, err := w.Redistribute(3, amount, types.ZeroCurrency) if err != nil { t.Fatal(err) } else if len(txns) != 0 {