diff --git a/cmd/explored/node.go b/cmd/explored/node.go index 90f05889..190f06a7 100644 --- a/cmd/explored/node.go +++ b/cmd/explored/node.go @@ -160,11 +160,7 @@ func newNode(addr, dir string, chainNetwork string, useUPNP bool, logger *zap.Lo return nil, err } - genesisIndex := types.ChainIndex{ - ID: genesisBlock.ID(), - Height: 0, - } - e, err := explorer.NewExplorer(cm, store, genesisIndex, logger.Named("explorer")) + e, err := explorer.NewExplorer(cm, store, logger.Named("explorer")) if err != nil { return nil, err } diff --git a/explorer/explorer.go b/explorer/explorer.go index b08a1e19..1160b1d2 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -72,12 +72,12 @@ func syncStore(store Store, cm ChainManager, index types.ChainIndex) error { } // NewExplorer returns a Sia explorer. -func NewExplorer(cm ChainManager, store Store, genesisIndex types.ChainIndex, log *zap.Logger) (*Explorer, error) { +func NewExplorer(cm ChainManager, store Store, log *zap.Logger) (*Explorer, error) { e := &Explorer{s: store} tip, err := store.Tip() if errors.Is(err, ErrNoTip) { - tip = genesisIndex + tip = types.ChainIndex{} } else if err != nil { return nil, fmt.Errorf("failed to get tip: %w", err) } @@ -91,7 +91,7 @@ func NewExplorer(cm ChainManager, store Store, genesisIndex types.ChainIndex, lo e.mu.Lock() lastTip, err := store.Tip() if errors.Is(err, ErrNoTip) { - lastTip = genesisIndex + lastTip = types.ChainIndex{} } else if err != nil { log.Error("failed to get tip", zap.Error(err)) } diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 783c6ce5..bb74204a 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -266,14 +266,20 @@ type balance struct { sf uint64 } -func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) error { +func (s *Store) updateBalances(dbTxn txn, height uint64, spentSiacoinElements, newSiacoinElements []types.SiacoinElement, spentSiafundElements, newSiafundElements []types.SiafundElement) error { addresses := make(map[types.Address]balance) - update.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) { + for _, sce := range spentSiacoinElements { addresses[sce.SiacoinOutput.Address] = balance{} - }) - update.ForEachSiafundElement(func(sfe types.SiafundElement, spent bool) { + } + for _, sce := range newSiacoinElements { + addresses[sce.SiacoinOutput.Address] = balance{} + } + for _, sfe := range spentSiafundElements { addresses[sfe.SiafundOutput.Address] = balance{} - }) + } + for _, sfe := range newSiafundElements { + addresses[sfe.SiafundOutput.Address] = balance{} + } var addressList []any for address := range addresses { @@ -281,8 +287,8 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) } rows, err := dbTxn.Query(`SELECT address, siacoin_balance, immature_siacoin_balance, siafund_balance - FROM address_balance - WHERE address IN (`+queryPlaceHolders(len(addressList))+`)`, addressList...) + FROM address_balance + WHERE address IN (`+queryPlaceHolders(len(addressList))+`)`, addressList...) if err != nil { return fmt.Errorf("updateBalances: failed to query address_balance: %w", err) } @@ -297,55 +303,43 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) addresses[address] = bal } - // log.Println("New block") - update.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) { + for _, sce := range newSiacoinElements { + bal := addresses[sce.SiacoinOutput.Address] + if sce.MaturityHeight <= height { + bal.sc = bal.sc.Add(sce.SiacoinOutput.Value) + } else { + bal.immatureSC = bal.immatureSC.Add(sce.SiacoinOutput.Value) + } + addresses[sce.SiacoinOutput.Address] = bal + } + for _, sce := range spentSiacoinElements { bal := addresses[sce.SiacoinOutput.Address] if sce.MaturityHeight < height { - if spent { - // If within the same block, an address A receives SC in one - // transaction and sends it to another address in a later - // transaction, the chain update will not contain the unspent - // siacoin element that was temporarily A's. This can then result - // in underflow when we subtract the element for A as being spent. - // So we catch underflow here because this causes crashes even - // though there is no net balance change for A. - // Example: https://siascan.com/block/506 - - // log.Println("Spend:", sce.SiacoinOutput.Address, sce.SiacoinOutput.Value) - underflow := false - bal.sc, underflow = bal.sc.SubWithUnderflow(sce.SiacoinOutput.Value) - if underflow { - return - } - } else { - // log.Println("Gain:", sce.SiacoinOutput.Address, sce.SiacoinOutput.Value) - bal.sc = bal.sc.Add(sce.SiacoinOutput.Value) - } + bal.sc = bal.sc.Sub(sce.SiacoinOutput.Value) } else { - if !spent { - bal.immatureSC = bal.immatureSC.Add(sce.SiacoinOutput.Value) - } + bal.immatureSC = bal.immatureSC.Sub(sce.SiacoinOutput.Value) } addresses[sce.SiacoinOutput.Address] = bal - }) - update.ForEachSiafundElement(func(sfe types.SiafundElement, spent bool) { + } + + for _, sfe := range newSiafundElements { bal := addresses[sfe.SiafundOutput.Address] - if spent { - underflow := (bal.sf - sfe.SiafundOutput.Value) > bal.sf - if underflow { - return - } - bal.sf -= sfe.SiafundOutput.Value - } else { - bal.sf += sfe.SiafundOutput.Value + bal.sf += sfe.SiafundOutput.Value + addresses[sfe.SiafundOutput.Address] = bal + } + for _, sfe := range spentSiafundElements { + bal := addresses[sfe.SiafundOutput.Address] + if bal.sf < sfe.SiafundOutput.Value { + panic("sf underflow") } + bal.sf -= sfe.SiafundOutput.Value addresses[sfe.SiafundOutput.Address] = bal - }) + } stmt, err := dbTxn.Prepare(`INSERT INTO address_balance(address, siacoin_balance, immature_siacoin_balance, siafund_balance) - VALUES (?, ?, ?, ?) - ON CONFLICT(address) - DO UPDATE set siacoin_balance = ?, immature_siacoin_balance = ?, siafund_balance = ?`) + VALUES (?, ?, ?, ?) + ON CONFLICT(address) + DO UPDATE set siacoin_balance = ?, immature_siacoin_balance = ?, siafund_balance = ?`) if err != nil { return fmt.Errorf("updateBalances: failed to prepare statement: %w", err) } @@ -361,18 +355,13 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) return nil } -func (s *Store) updateMaturedBalances(dbTxn txn, update consensusUpdate, height uint64) error { +func (s *Store) updateMaturedBalances(dbTxn txn, revert bool, height uint64) error { // Prevent double counting - outputs with a maturity height of 0 are // handled in updateBalances if height == 0 { return nil } - _, isRevert := update.(chain.RevertUpdate) - if isRevert { - height++ - } - rows, err := dbTxn.Query(`SELECT address, value FROM siacoin_elements WHERE maturity_height = ?`, height) @@ -414,7 +403,7 @@ func (s *Store) updateMaturedBalances(dbTxn txn, update consensusUpdate, height // If we are reverting then we subtract them. for _, sco := range scos { bal := addresses[sco.Address] - if isRevert { + if revert { bal.sc = bal.sc.Sub(sco.Value) bal.immatureSC = bal.immatureSC.Add(sco.Value) } else { @@ -443,7 +432,7 @@ func (s *Store) updateMaturedBalances(dbTxn txn, update consensusUpdate, height return nil } -func (s *Store) addSiacoinElements(dbTxn txn, bid types.BlockID, update consensusUpdate) (map[types.SiacoinOutputID]int64, error) { +func (s *Store) addSiacoinElements(dbTxn txn, bid types.BlockID, update consensusUpdate, spentElements, newElements []types.SiacoinElement) (map[types.SiacoinOutputID]int64, error) { sources := make(map[types.SiacoinOutputID]explorer.Source) if applyUpdate, ok := update.(chain.ApplyUpdate); ok { block := applyUpdate.Block @@ -470,38 +459,45 @@ func (s *Store) addSiacoinElements(dbTxn txn, bid types.BlockID, update consensu stmt, err := dbTxn.Prepare(`INSERT INTO siacoin_elements(output_id, block_id, leaf_index, merkle_proof, spent, source, maturity_height, address, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT + ON CONFLICT (output_id) DO UPDATE SET spent = ?`) if err != nil { return nil, fmt.Errorf("addSiacoinElements: failed to prepare siacoin_elements statement: %w", err) } defer stmt.Close() - var updateErr error scDBIds := make(map[types.SiacoinOutputID]int64) - update.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) { - if updateErr != nil { - return + for _, sce := range newElements { + result, err := stmt.Exec(dbEncode(sce.StateElement.ID), dbEncode(bid), dbEncode(sce.StateElement.LeafIndex), dbEncode(sce.StateElement.MerkleProof), false, int(sources[types.SiacoinOutputID(sce.StateElement.ID)]), sce.MaturityHeight, dbEncode(sce.SiacoinOutput.Address), dbEncode(sce.SiacoinOutput.Value), false) + if err != nil { + return nil, fmt.Errorf("addSiacoinElements: failed to execute siacoin_elements statement: %w", err) } - result, err := stmt.Exec(dbEncode(sce.StateElement.ID), dbEncode(bid), dbEncode(sce.StateElement.LeafIndex), dbEncode(sce.StateElement.MerkleProof), spent, int(sources[types.SiacoinOutputID(sce.StateElement.ID)]), sce.MaturityHeight, dbEncode(sce.SiacoinOutput.Address), dbEncode(sce.SiacoinOutput.Value), spent) + dbID, err := result.LastInsertId() if err != nil { - updateErr = fmt.Errorf("addSiacoinElements: failed to execute siacoin_elements statement: %w", err) - return + return nil, fmt.Errorf("addSiacoinElements: failed to get last insert ID: %w", err) + } + + scDBIds[types.SiacoinOutputID(sce.StateElement.ID)] = dbID + } + for _, sce := range spentElements { + result, err := stmt.Exec(dbEncode(sce.StateElement.ID), dbEncode(bid), dbEncode(sce.StateElement.LeafIndex), dbEncode(sce.StateElement.MerkleProof), true, int(sources[types.SiacoinOutputID(sce.StateElement.ID)]), sce.MaturityHeight, dbEncode(sce.SiacoinOutput.Address), dbEncode(sce.SiacoinOutput.Value), true) + if err != nil { + return nil, fmt.Errorf("addSiacoinElements: failed to execute siacoin_elements statement: %w", err) } dbID, err := result.LastInsertId() if err != nil { - updateErr = fmt.Errorf("addSiacoinElements: failed to get last insert ID: %w", err) - return + return nil, fmt.Errorf("addSiacoinElements: failed to get last insert ID: %w", err) } scDBIds[types.SiacoinOutputID(sce.StateElement.ID)] = dbID - }) - return scDBIds, updateErr + } + + return scDBIds, nil } -func (s *Store) addSiafundElements(dbTxn txn, bid types.BlockID, update consensusUpdate) (map[types.SiafundOutputID]int64, error) { +func (s *Store) addSiafundElements(dbTxn txn, bid types.BlockID, spentElements, newElements []types.SiafundElement) (map[types.SiafundOutputID]int64, error) { stmt, err := dbTxn.Prepare(`INSERT INTO siafund_elements(output_id, block_id, leaf_index, merkle_proof, spent, claim_start, address, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT @@ -511,28 +507,35 @@ func (s *Store) addSiafundElements(dbTxn txn, bid types.BlockID, update consensu } defer stmt.Close() - var updateErr error sfDBIds := make(map[types.SiafundOutputID]int64) - update.ForEachSiafundElement(func(sfe types.SiafundElement, spent bool) { - if updateErr != nil { - return + for _, sfe := range newElements { + result, err := stmt.Exec(dbEncode(sfe.StateElement.ID), dbEncode(bid), dbEncode(sfe.StateElement.LeafIndex), dbEncode(sfe.StateElement.MerkleProof), false, dbEncode(sfe.ClaimStart), dbEncode(sfe.SiafundOutput.Address), dbEncode(sfe.SiafundOutput.Value), false) + if err != nil { + return nil, fmt.Errorf("addSiafundElements: failed to execute siafund_elements statement: %w", err) } - result, err := stmt.Exec(dbEncode(sfe.StateElement.ID), dbEncode(bid), dbEncode(sfe.StateElement.LeafIndex), dbEncode(sfe.StateElement.MerkleProof), spent, dbEncode(sfe.ClaimStart), dbEncode(sfe.SiafundOutput.Address), dbEncode(sfe.SiafundOutput.Value), spent) + dbID, err := result.LastInsertId() if err != nil { - updateErr = fmt.Errorf("addSiafundElements: failed to execute siafund_elements statement: %w", err) - return + return nil, fmt.Errorf("addSiafundElements: failed to get last insert ID: %w", err) + } + + sfDBIds[types.SiafundOutputID(sfe.StateElement.ID)] = dbID + } + for _, sfe := range spentElements { + result, err := stmt.Exec(dbEncode(sfe.StateElement.ID), dbEncode(bid), dbEncode(sfe.StateElement.LeafIndex), dbEncode(sfe.StateElement.MerkleProof), true, dbEncode(sfe.ClaimStart), dbEncode(sfe.SiafundOutput.Address), dbEncode(sfe.SiafundOutput.Value), true) + if err != nil { + return nil, fmt.Errorf("addSiafundElements: failed to execute siafund_elements statement: %w", err) } dbID, err := result.LastInsertId() if err != nil { - updateErr = fmt.Errorf("addSiafundElements: failed to get last insert ID: %w", err) - return + return nil, fmt.Errorf("addSiafundElements: failed to get last insert ID: %w", err) } sfDBIds[types.SiafundOutputID(sfe.StateElement.ID)] = dbID - }) - return sfDBIds, updateErr + } + + return sfDBIds, nil } type fileContract struct { @@ -597,52 +600,175 @@ func (s *Store) deleteBlock(dbTxn txn, bid types.BlockID) error { func (s *Store) ProcessChainUpdates(crus []chain.RevertUpdate, caus []chain.ApplyUpdate) error { return s.transaction(func(dbTxn txn) error { for _, cru := range crus { - if err := s.deleteBlock(dbTxn, cru.Block.ID()); err != nil { - return fmt.Errorf("revertUpdate: failed to delete block: %w", err) - } else if _, err := s.addSiacoinElements(dbTxn, cru.Block.ID(), cru); err != nil { + if err := s.updateMaturedBalances(dbTxn, true, cru.State.Index.Height+1); err != nil { + return fmt.Errorf("revertUpdate: failed to update matured balances: %w", err) + } + + created := make(map[types.Hash256]bool) + ephemeral := make(map[types.Hash256]bool) + for _, txn := range cru.Block.Transactions { + for i := range txn.SiacoinOutputs { + created[types.Hash256(txn.SiacoinOutputID(i))] = true + } + for _, input := range txn.SiacoinInputs { + ephemeral[types.Hash256(input.ParentID)] = created[types.Hash256(input.ParentID)] + } + for i := range txn.SiafundOutputs { + created[types.Hash256(txn.SiafundOutputID(i))] = true + } + for _, input := range txn.SiafundInputs { + ephemeral[types.Hash256(input.ParentID)] = created[types.Hash256(input.ParentID)] + } + } + + // add new siacoin elements to the store + var newSiacoinElements, spentSiacoinElements []types.SiacoinElement + var ephemeralSiacoinElements []types.SiacoinElement + cru.ForEachSiacoinElement(func(se types.SiacoinElement, spent bool) { + if ephemeral[se.ID] { + ephemeralSiacoinElements = append(ephemeralSiacoinElements, se) + return + } + + if spent { + newSiacoinElements = append(newSiacoinElements, se) + } else { + spentSiacoinElements = append(spentSiacoinElements, se) + } + }) + + var newSiafundElements, spentSiafundElements []types.SiafundElement + var ephemeralSiafundElements []types.SiafundElement + cru.ForEachSiafundElement(func(se types.SiafundElement, spent bool) { + if ephemeral[se.ID] { + ephemeralSiafundElements = append(ephemeralSiafundElements, se) + return + } + + if spent { + newSiafundElements = append(newSiafundElements, se) + } else { + spentSiafundElements = append(spentSiafundElements, se) + } + }) + + // log.Println("REVERT!") + if _, err := s.addSiacoinElements( + dbTxn, + cru.Block.ID(), + cru, + spentSiacoinElements, + append(newSiacoinElements, ephemeralSiacoinElements...), + ); err != nil { return fmt.Errorf("revertUpdate: failed to update siacoin output state: %w", err) - } else if _, err := s.addSiafundElements(dbTxn, cru.Block.ID(), cru); err != nil { + } else if _, err := s.addSiafundElements( + dbTxn, + cru.Block.ID(), + spentSiafundElements, + append(newSiafundElements, ephemeralSiafundElements...), + ); err != nil { return fmt.Errorf("revertUpdate: failed to update siafund output state: %w", err) - } else if err := s.updateBalances(dbTxn, cru, cru.State.Index.Height); err != nil { + } else if err := s.updateBalances(dbTxn, cru.State.Index.Height+1, spentSiacoinElements, newSiacoinElements, spentSiafundElements, newSiafundElements); err != nil { return fmt.Errorf("revertUpdate: failed to update balances: %w", err) - } else if err := s.updateMaturedBalances(dbTxn, cru, cru.State.Index.Height); err != nil { - return fmt.Errorf("revertUpdate: failed to update matured balances: %w", err) } else if _, err := s.addFileContractElements(dbTxn, cru.Block.ID(), cru); err != nil { return fmt.Errorf("revertUpdate: failed to update file contract state: %w", err) } else if err := s.updateLeaves(dbTxn, cru); err != nil { return fmt.Errorf("revertUpdate: failed to update leaves: %w", err) + } else if err := s.deleteBlock(dbTxn, cru.Block.ID()); err != nil { + return fmt.Errorf("revertUpdate: failed to delete block: %w", err) } } - for _, update := range caus { - scDBIds, err := s.addSiacoinElements(dbTxn, update.Block.ID(), update) + for _, cau := range caus { + if err := s.addBlock(dbTxn, cau.Block, cau.State.Index.Height); err != nil { + return fmt.Errorf("applyUpdates: failed to add block: %w", err) + } else if err := s.updateMaturedBalances(dbTxn, false, cau.State.Index.Height); err != nil { + return fmt.Errorf("applyUpdates: failed to update matured balances: %w", err) + } + + created := make(map[types.Hash256]bool) + ephemeral := make(map[types.Hash256]bool) + for _, txn := range cau.Block.Transactions { + for i := range txn.SiacoinOutputs { + created[types.Hash256(txn.SiacoinOutputID(i))] = true + } + for _, input := range txn.SiacoinInputs { + ephemeral[types.Hash256(input.ParentID)] = created[types.Hash256(input.ParentID)] + } + for i := range txn.SiafundOutputs { + created[types.Hash256(txn.SiafundOutputID(i))] = true + } + for _, input := range txn.SiafundInputs { + ephemeral[types.Hash256(input.ParentID)] = created[types.Hash256(input.ParentID)] + } + } + + // add new siacoin elements to the store + var newSiacoinElements, spentSiacoinElements []types.SiacoinElement + var ephemeralSiacoinElements []types.SiacoinElement + cau.ForEachSiacoinElement(func(se types.SiacoinElement, spent bool) { + if ephemeral[se.ID] { + ephemeralSiacoinElements = append(ephemeralSiacoinElements, se) + return + } + + if spent { + spentSiacoinElements = append(spentSiacoinElements, se) + } else { + newSiacoinElements = append(newSiacoinElements, se) + } + }) + + var newSiafundElements, spentSiafundElements []types.SiafundElement + var ephemeralSiafundElements []types.SiafundElement + cau.ForEachSiafundElement(func(se types.SiafundElement, spent bool) { + if ephemeral[se.ID] { + ephemeralSiafundElements = append(ephemeralSiafundElements, se) + return + } + + if spent { + spentSiafundElements = append(spentSiafundElements, se) + } else { + newSiafundElements = append(newSiafundElements, se) + } + }) + + scDBIds, err := s.addSiacoinElements( + dbTxn, + cau.Block.ID(), + cau, + append(spentSiacoinElements, ephemeralSiacoinElements...), + newSiacoinElements, + ) if err != nil { return fmt.Errorf("applyUpdates: failed to add siacoin outputs: %w", err) } - sfDBIds, err := s.addSiafundElements(dbTxn, update.Block.ID(), update) + sfDBIds, err := s.addSiafundElements( + dbTxn, + cau.Block.ID(), + append(spentSiafundElements, ephemeralSiafundElements...), + newSiafundElements, + ) if err != nil { return fmt.Errorf("applyUpdates: failed to add siafund outputs: %w", err) } - if err := s.updateBalances(dbTxn, update, update.State.Index.Height); err != nil { + if err := s.updateBalances(dbTxn, cau.State.Index.Height, spentSiacoinElements, newSiacoinElements, spentSiafundElements, newSiafundElements); err != nil { return fmt.Errorf("applyUpdates: failed to update balances: %w", err) - } else if err := s.updateMaturedBalances(dbTxn, update, update.State.Index.Height); err != nil { - return fmt.Errorf("applyUpdates: failed to update matured balances: %w", err) } - fcDBIds, err := s.addFileContractElements(dbTxn, update.Block.ID(), update) + fcDBIds, err := s.addFileContractElements(dbTxn, cau.Block.ID(), cau) if err != nil { return fmt.Errorf("applyUpdates: failed to add file contracts: %w", err) } - if err := s.addBlock(dbTxn, update.Block, update.State.Index.Height); err != nil { - return fmt.Errorf("applyUpdates: failed to add block: %w", err) - } else if err := s.addMinerPayouts(dbTxn, update.Block.ID(), update.State.Index.Height, update.Block.MinerPayouts, scDBIds); err != nil { + if err := s.addMinerPayouts(dbTxn, cau.Block.ID(), cau.State.Index.Height, cau.Block.MinerPayouts, scDBIds); err != nil { return fmt.Errorf("applyUpdates: failed to add miner payouts: %w", err) - } else if err := s.addTransactions(dbTxn, update.Block.ID(), update.Block.Transactions, scDBIds, sfDBIds, fcDBIds); err != nil { + } else if err := s.addTransactions(dbTxn, cau.Block.ID(), cau.Block.Transactions, scDBIds, sfDBIds, fcDBIds); err != nil { return fmt.Errorf("applyUpdates: failed to add transactions: addTransactions: %w", err) } - if err := s.updateLeaves(dbTxn, update); err != nil { + if err := s.updateLeaves(dbTxn, cau); err != nil { return err } } diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 2879606a..8c3391e7 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -253,6 +253,94 @@ func TestBalance(t *testing.T) { checkBalance(addr3, types.Siacoins(100), types.ZeroCurrency, 0) } +func TestSiafundBalance(t *testing.T) { + log := zaptest.NewLogger(t) + dir := t.TempDir() + db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) + if err != nil { + t.Fatal(err) + } + defer bdb.Close() + + // Generate three addresses: addr1, addr2, addr3 + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + + pk2 := types.GeneratePrivateKey() + addr2 := types.StandardUnlockHash(pk2.PublicKey()) + + pk3 := types.GeneratePrivateKey() + addr3 := types.StandardUnlockHash(pk3.PublicKey()) + + const giftSF = 10000 + network, genesisBlock := testV1Network(addr1, types.ZeroCurrency, giftSF) + + store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) + if err != nil { + t.Fatal(err) + } + + cm := chain.NewManager(store, genesisState) + + // checkBalance checks that an address has the balances we expect + checkBalance := func(addr types.Address, expectSC, expectImmatureSC types.Currency, expectSF uint64) { + sc, immatureSC, sf, err := db.Balance(addr) + if err != nil { + t.Fatal(err) + } + check(t, "siacoins", expectSC, sc) + check(t, "immature siacoins", expectImmatureSC, immatureSC) + check(t, "siafunds", expectSF, sf) + } + + // Send all of the payout except 100 SF to addr2 + unlockConditions := types.StandardUnlockConditions(pk1.PublicKey()) + parentTxn := types.Transaction{ + SiafundInputs: []types.SiafundInput{ + { + ParentID: types.SiafundOutputID(genesisBlock.Transactions[0].SiafundOutputID(0)), + UnlockConditions: unlockConditions, + }, + }, + SiafundOutputs: []types.SiafundOutput{ + {Address: addr1, Value: 100}, + {Address: addr2, Value: genesisBlock.Transactions[0].SiafundOutputs[0].Value - 100}, + }, + } + signTxn(cm.TipState(), pk1, &parentTxn) + + // In the same block, have addr1 send the 100 SF it still has left to + // addr3 + outputID := parentTxn.SiafundOutputID(0) + txn := types.Transaction{ + SiafundInputs: []types.SiafundInput{ + { + ParentID: outputID, + UnlockConditions: unlockConditions, + }, + }, + SiafundOutputs: []types.SiafundOutput{ + {Address: addr3, Value: 100}, + }, + } + signTxn(cm.TipState(), pk1, &txn) + + if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), []types.Transaction{parentTxn, txn}, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + checkBalance(addr1, types.ZeroCurrency, types.ZeroCurrency, 0) + checkBalance(addr2, types.ZeroCurrency, types.ZeroCurrency, giftSF-100) + checkBalance(addr3, types.ZeroCurrency, types.ZeroCurrency, 100) +} + func TestSendTransactions(t *testing.T) { log := zaptest.NewLogger(t) dir := t.TempDir() @@ -772,3 +860,623 @@ func TestFileContract(t *testing.T) { checkFC(true, false, fc, dbFCs[0]) } } + +func TestRevertTip(t *testing.T) { + log := zaptest.NewLogger(t) + dir := t.TempDir() + db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) + if err != nil { + t.Fatal(err) + } + defer bdb.Close() + + network, genesisBlock := testV1Network(types.VoidAddress, types.ZeroCurrency, 0) + + store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) + if err != nil { + t.Fatal(err) + } + + cm := chain.NewManager(store, genesisState) + + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + + pk2 := types.GeneratePrivateKey() + addr2 := types.StandardUnlockHash(pk2.PublicKey()) + + const n = 100 + for i := cm.Tip().Height; i < n; i++ { + if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), nil, addr1)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + tip, err := db.Tip() + if err != nil { + t.Fatal(err) + } + check(t, "tip", cm.Tip(), tip) + } + + { + // mine to trigger a reorg + var blocks []types.Block + state := genesisState + for i := uint64(0); i < n+5; i++ { + blocks = append(blocks, mineBlock(state, nil, addr2)) + state.Index.ID = blocks[len(blocks)-1].ID() + state.Index.Height++ + } + if err := cm.AddBlocks(blocks); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + tip, err := db.Tip() + if err != nil { + t.Fatal(err) + } + check(t, "tip", cm.Tip(), tip) + } + + for i := 0; i < n; i++ { + best, err := db.BestTip(uint64(i)) + if err != nil { + t.Fatal(err) + } + if cmBest, ok := cm.BestIndex(uint64(i)); !ok || cmBest != best { + t.Fatal("best tip mismatch") + } + } +} + +func TestRevertBalance(t *testing.T) { + log := zaptest.NewLogger(t) + dir := t.TempDir() + db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) + if err != nil { + t.Fatal(err) + } + defer bdb.Close() + + network, genesisBlock := testV1Network(types.VoidAddress, types.ZeroCurrency, 0) + + store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) + if err != nil { + t.Fatal(err) + } + + cm := chain.NewManager(store, genesisState) + + // checkBalance checks that an address has the balances we expect + checkBalance := func(addr types.Address, expectSC, expectImmatureSC types.Currency, expectSF uint64) { + sc, immatureSC, sf, err := db.Balance(addr) + if err != nil { + t.Fatal(err) + } + check(t, "siacoins", expectSC, sc) + check(t, "immature siacoins", expectImmatureSC, immatureSC) + check(t, "siafunds", expectSF, sf) + } + + // Generate three addresses: addr1, addr2, addr3 + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + + pk2 := types.GeneratePrivateKey() + addr2 := types.StandardUnlockHash(pk2.PublicKey()) + + pk3 := types.GeneratePrivateKey() + addr3 := types.StandardUnlockHash(pk3.PublicKey()) + + // t.Log("addr1:", addr1) + // t.Log("addr2:", addr2) + // t.Log("addr3:", addr3) + + expectedPayout := cm.TipState().BlockReward() + maturityHeight := cm.TipState().MaturityHeight() + + // Mine a block sending the payout to addr1 + if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), nil, addr1)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + // Check that addr1 has the miner payout output + utxos, err := db.UnspentSiacoinOutputs(addr1, 100, 0) + if err != nil { + t.Fatal(err) + } + check(t, "utxos", 1, len(utxos)) + check(t, "value", expectedPayout, utxos[0].SiacoinOutput.Value) + check(t, "source", explorer.SourceMinerPayout, utxos[0].Source) + + { + // Mine to trigger a reorg + // Send payout to addr2 instead of addr1 for these blocks + var blocks []types.Block + state := genesisState + for i := uint64(0); i < 2; i++ { + blocks = append(blocks, mineBlock(state, nil, addr2)) + state.Index.ID = blocks[len(blocks)-1].ID() + state.Index.Height++ + } + if err := cm.AddBlocks(blocks); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + } + + // Mine until the payout matures + for i := cm.Tip().Height; i < maturityHeight; i++ { + checkBalance(addr1, types.ZeroCurrency, types.ZeroCurrency, 0) + checkBalance(addr2, types.ZeroCurrency, expectedPayout.Mul64(2), 0) + if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), nil, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + } + checkBalance(addr1, types.ZeroCurrency, types.ZeroCurrency, 0) + checkBalance(addr2, expectedPayout.Mul64(1), expectedPayout.Mul64(1), 0) + + utxos1, err := db.UnspentSiacoinOutputs(addr1, 100, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr1 utxos", 0, len(utxos1)) + + utxos2, err := db.UnspentSiacoinOutputs(addr2, 100, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr2 utxos", 2, len(utxos2)) + for _, utxo := range utxos2 { + check(t, "value", expectedPayout, utxo.SiacoinOutput.Value) + check(t, "source", explorer.SourceMinerPayout, utxo.Source) + } + + // Send all of the payout except 100 SC to addr3 + hundredSC := types.Siacoins(100) + unlockConditions := types.StandardUnlockConditions(pk2.PublicKey()) + parentTxn := types.Transaction{ + SiacoinInputs: []types.SiacoinInput{ + { + ParentID: types.SiacoinOutputID(utxos2[0].ID), + UnlockConditions: unlockConditions, + }, + }, + SiacoinOutputs: []types.SiacoinOutput{ + {Address: addr2, Value: hundredSC}, + {Address: addr3, Value: utxos2[0].SiacoinOutput.Value.Sub(hundredSC)}, + }, + } + signTxn(cm.TipState(), pk2, &parentTxn) + + // In the same block, have addr2 send the 100 SC it still has left to + // addr1 + outputID := parentTxn.SiacoinOutputID(0) + txn := types.Transaction{ + SiacoinInputs: []types.SiacoinInput{ + { + ParentID: outputID, + UnlockConditions: unlockConditions, + }, + }, + SiacoinOutputs: []types.SiacoinOutput{ + {Address: addr1, Value: hundredSC}, + }, + } + signTxn(cm.TipState(), pk2, &txn) + + if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), []types.Transaction{parentTxn, txn}, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + checkBalance(addr1, hundredSC, types.ZeroCurrency, 0) + // second block added in reorg has now matured + checkBalance(addr2, utxos2[1].SiacoinOutput.Value, types.ZeroCurrency, 0) + checkBalance(addr3, utxos2[0].SiacoinOutput.Value.Sub(hundredSC), types.ZeroCurrency, 0) + + { + // Reorg everything from before + // Send payout to void instead of addr2 for these blocks except for + // the first block where the payout goes to addr1, and the second block + // where the payout goes to addr2. + var blocks []types.Block + state := genesisState + for i := uint64(0); i < maturityHeight+10; i++ { + addr := types.VoidAddress + if i == 0 { + addr = addr1 + } else if i == 1 { + addr = addr2 + } + blocks = append(blocks, mineBlock(state, nil, addr)) + state.Index.ID = blocks[len(blocks)-1].ID() + state.Index.Height++ + } + if err := cm.AddBlocks(blocks); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + } + + checkBalance(addr1, expectedPayout, types.ZeroCurrency, 0) + checkBalance(addr2, expectedPayout, types.ZeroCurrency, 0) + checkBalance(addr3, types.ZeroCurrency, types.ZeroCurrency, 0) + + utxos1, err = db.UnspentSiacoinOutputs(addr1, 100, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr1 utxos", 1, len(utxos1)) + for _, utxo := range utxos1 { + check(t, "value", expectedPayout, utxo.SiacoinOutput.Value) + check(t, "source", explorer.SourceMinerPayout, utxo.Source) + } + + utxos2, err = db.UnspentSiacoinOutputs(addr2, 100, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr2 utxos", 1, len(utxos2)) + for _, utxo := range utxos2 { + check(t, "value", expectedPayout, utxo.SiacoinOutput.Value) + check(t, "source", explorer.SourceMinerPayout, utxo.Source) + } + + utxos3, err := db.UnspentSiacoinOutputs(addr3, 100, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr3 utxos", 0, len(utxos3)) +} + +func TestRevertSendTransactions(t *testing.T) { + log := zaptest.NewLogger(t) + dir := t.TempDir() + db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) + if err != nil { + t.Fatal(err) + } + defer bdb.Close() + + // Generate three addresses: addr1, addr2, addr3 + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + + pk2 := types.GeneratePrivateKey() + addr2 := types.StandardUnlockHash(pk2.PublicKey()) + + pk3 := types.GeneratePrivateKey() + addr3 := types.StandardUnlockHash(pk3.PublicKey()) + + // t.Log("addr1:", addr1) + // t.Log("addr2:", addr2) + // t.Log("addr3:", addr3) + + const giftSF = 10000 + network, genesisBlock := testV1Network(addr1, types.ZeroCurrency, giftSF) + + store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) + if err != nil { + t.Fatal(err) + } + + cm := chain.NewManager(store, genesisState) + + // checkBalance checks that an address has the balances we expect + checkBalance := func(addr types.Address, expectSC, expectImmatureSC types.Currency, expectSF uint64) { + sc, immatureSC, sf, err := db.Balance(addr) + if err != nil { + t.Fatal(err) + } + check(t, "siacoins", expectSC, sc) + check(t, "immature siacoins", expectImmatureSC, immatureSC) + check(t, "siafunds", expectSF, sf) + } + + checkTransaction := func(expectTxn types.Transaction, gotTxn explorer.Transaction) { + check(t, "siacoin inputs", len(expectTxn.SiacoinInputs), len(gotTxn.SiacoinInputs)) + check(t, "siacoin outputs", len(expectTxn.SiacoinOutputs), len(gotTxn.SiacoinOutputs)) + check(t, "siafund inputs", len(expectTxn.SiafundInputs), len(gotTxn.SiafundInputs)) + check(t, "siafund outputs", len(expectTxn.SiafundOutputs), len(gotTxn.SiafundOutputs)) + + for i := range expectTxn.SiacoinInputs { + expectSci := expectTxn.SiacoinInputs[i] + gotSci := gotTxn.SiacoinInputs[i] + + check(t, "parent ID", expectSci.ParentID, gotSci.ParentID) + check(t, "unlock conditions", expectSci.UnlockConditions, gotSci.UnlockConditions) + } + for i := range expectTxn.SiacoinOutputs { + expectSco := expectTxn.SiacoinOutputs[i] + gotSco := gotTxn.SiacoinOutputs[i].SiacoinOutput + + check(t, "address", expectSco.Address, gotSco.Address) + check(t, "value", expectSco.Value, gotSco.Value) + check(t, "source", explorer.SourceTransaction, gotTxn.SiacoinOutputs[i].Source) + } + for i := range expectTxn.SiafundInputs { + expectSfi := expectTxn.SiafundInputs[i] + gotSfi := gotTxn.SiafundInputs[i] + + check(t, "parent ID", expectSfi.ParentID, gotSfi.ParentID) + check(t, "claim address", expectSfi.ClaimAddress, gotSfi.ClaimAddress) + check(t, "unlock conditions", expectSfi.UnlockConditions, gotSfi.UnlockConditions) + } + for i := range expectTxn.SiafundOutputs { + expectSfo := expectTxn.SiafundOutputs[i] + gotSfo := gotTxn.SiafundOutputs[i].SiafundOutput + + check(t, "address", expectSfo.Address, gotSfo.Address) + check(t, "value", expectSfo.Value, gotSfo.Value) + } + } + + expectedPayout := cm.TipState().BlockReward() + maturityHeight := cm.TipState().MaturityHeight() + + var blocks []types.Block + b1 := mineBlock(cm.TipState(), nil, addr1) + // Mine a block sending the payout to the addr1 + if err := cm.AddBlocks([]types.Block{b1}); err != nil { + t.Fatal(err) + } + blocks = append(blocks, b1) + syncDB(t, db, cm) + + // Mine until the payout matures + for i := cm.Tip().Height; i < maturityHeight; i++ { + b := mineBlock(cm.TipState(), nil, types.VoidAddress) + if err := cm.AddBlocks([]types.Block{b}); err != nil { + t.Fatal(err) + } + blocks = append(blocks, b) + syncDB(t, db, cm) + } + + checkBalance(addr1, expectedPayout, types.ZeroCurrency, giftSF) + checkBalance(addr2, types.ZeroCurrency, types.ZeroCurrency, 0) + checkBalance(addr3, types.ZeroCurrency, types.ZeroCurrency, 0) + + const n = 26 + + // Check that addr1 has the miner payout output + utxos, err := db.UnspentSiacoinOutputs(addr1, n, 0) + if err != nil { + t.Fatal(err) + } + check(t, "utxos", 1, len(utxos)) + check(t, "value", expectedPayout, utxos[0].SiacoinOutput.Value) + check(t, "source", explorer.SourceMinerPayout, utxos[0].Source) + + sfOutputID := genesisBlock.Transactions[0].SiafundOutputID(0) + scOutputID := utxos[0].ID + unlockConditions := types.StandardUnlockConditions(pk1.PublicKey()) + // Send 1 SC to addr2 and 2 SC to addr3 100 times in consecutive blocks + for i := 0; i < n; i++ { + addr1SCs := expectedPayout.Sub(types.Siacoins(1 + 2).Mul64(uint64(i + 1))) + addr1SFs := giftSF - (1+2)*uint64(i+1) + + parentTxn := types.Transaction{ + SiacoinInputs: []types.SiacoinInput{ + { + ParentID: types.SiacoinOutputID(scOutputID), + UnlockConditions: unlockConditions, + }, + }, + SiafundInputs: []types.SiafundInput{ + { + ParentID: sfOutputID, + UnlockConditions: unlockConditions, + }, + }, + SiacoinOutputs: []types.SiacoinOutput{ + {Address: addr2, Value: types.Siacoins(1)}, + {Address: addr3, Value: types.Siacoins(2)}, + {Address: addr1, Value: addr1SCs}, + }, + SiafundOutputs: []types.SiafundOutput{ + {Address: addr2, Value: 1}, + {Address: addr3, Value: 2}, + {Address: addr1, Value: addr1SFs}, + }, + } + + signTxn(cm.TipState(), pk1, &parentTxn) + scOutputID = types.Hash256(parentTxn.SiacoinOutputID(2)) + sfOutputID = parentTxn.SiafundOutputID(2) + + // Mine a block with the above transaction + b := mineBlock(cm.TipState(), []types.Transaction{parentTxn}, types.VoidAddress) + if err := cm.AddBlocks([]types.Block{b}); err != nil { + t.Fatal(err) + } + blocks = append(blocks, b) + syncDB(t, db, cm) + + checkBalance(addr1, addr1SCs, types.ZeroCurrency, addr1SFs) + checkBalance(addr2, types.Siacoins(1).Mul64(uint64(i+1)), types.ZeroCurrency, 1*uint64(i+1)) + checkBalance(addr3, types.Siacoins(2).Mul64(uint64(i+1)), types.ZeroCurrency, 2*uint64(i+1)) + + // Ensure the block we retrieved from the database is the same as the + // actual block + block, err := db.Block(b.ID()) + if err != nil { + t.Fatal(err) + } + check(t, "transactions", len(b.Transactions), len(block.Transactions)) + check(t, "miner payouts", len(b.MinerPayouts), len(block.MinerPayouts)) + check(t, "nonce", b.Nonce, block.Nonce) + check(t, "timestamp", b.Timestamp, block.Timestamp) + + // Ensure the miner payouts in the block match + for i := range b.MinerPayouts { + check(t, "address", b.MinerPayouts[i].Address, b.MinerPayouts[i].Address) + check(t, "value", b.MinerPayouts[i].Value, b.MinerPayouts[i].Value) + } + + // Ensure the transactions in the block and retrieved separately match + // with the actual transactions + for i := range b.Transactions { + checkTransaction(b.Transactions[i], block.Transactions[i]) + + txns, err := db.Transactions([]types.TransactionID{b.Transactions[i].ID()}) + if err != nil { + t.Fatal(err) + } + check(t, "transactions", 1, len(txns)) + checkTransaction(b.Transactions[i], txns[0]) + } + + type expectedUTXOs struct { + addr types.Address + + sc int + scValue types.Currency + + sf int + sfValue uint64 + } + expected := []expectedUTXOs{ + {addr1, 1, addr1SCs, 1, addr1SFs}, + {addr2, i + 1, types.Siacoins(1), i + 1, 1}, + {addr3, i + 1, types.Siacoins(2), i + 1, 2}, + } + for _, e := range expected { + sc, err := db.UnspentSiacoinOutputs(e.addr, n, 0) + if err != nil { + t.Fatal(err) + } + sf, err := db.UnspentSiafundOutputs(e.addr, n, 0) + if err != nil { + t.Fatal(err) + } + + check(t, "sc utxos", e.sc, len(sc)) + check(t, "sf utxos", e.sf, len(sf)) + + for _, sco := range sc { + check(t, "address", e.addr, sco.SiacoinOutput.Address) + check(t, "value", e.scValue, sco.SiacoinOutput.Value) + check(t, "source", explorer.SourceTransaction, sco.Source) + } + for _, sfo := range sf { + check(t, "address", e.addr, sfo.SiafundOutput.Address) + check(t, "value", e.sfValue, sfo.SiafundOutput.Value) + } + } + } + + { + // take 3 blocks off the top + // revertBlocks := blocks[len(blocks)-3:] + newBlocks := blocks[:len(blocks)-3] + + state, ok := store.State(newBlocks[len(newBlocks)-1].ID()) + if !ok { + t.Fatal("no such block") + } + for i := 0; i < 3+1; i++ { + newBlocks = append(newBlocks, mineBlock(state, nil, types.VoidAddress)) + state.Index.ID = newBlocks[len(newBlocks)-1].ID() + state.Index.Height++ + } + + if err := cm.AddBlocks(newBlocks); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + addr1SCs := expectedPayout.Sub(types.Siacoins(1 + 2).Mul64(uint64(n - 3))) + addr1SFs := giftSF - (1+2)*uint64(n-3) + + checkBalance(addr1, addr1SCs, types.ZeroCurrency, addr1SFs) + checkBalance(addr2, types.Siacoins(1).Mul64(uint64(n-3)), types.ZeroCurrency, 1*uint64(n-3)) + checkBalance(addr3, types.Siacoins(2).Mul64(uint64(n-3)), types.ZeroCurrency, 2*uint64(n-3)) + + scUtxos1, err := db.UnspentSiacoinOutputs(addr1, n, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr1 sc utxos", 1, len(scUtxos1)) + for _, sce := range scUtxos1 { + check(t, "address", addr1, sce.SiacoinOutput.Address) + check(t, "value", addr1SCs, sce.SiacoinOutput.Value) + check(t, "source", explorer.SourceTransaction, sce.Source) + } + + scUtxos2, err := db.UnspentSiacoinOutputs(addr2, n, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr2 sc utxos", n-3, len(scUtxos2)) + for _, sce := range scUtxos2 { + check(t, "address", addr2, sce.SiacoinOutput.Address) + check(t, "value", types.Siacoins(1), sce.SiacoinOutput.Value) + check(t, "source", explorer.SourceTransaction, sce.Source) + } + + scUtxos3, err := db.UnspentSiacoinOutputs(addr3, n, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr3 sc utxos", n-3, len(scUtxos3)) + for _, sce := range scUtxos3 { + check(t, "address", addr3, sce.SiacoinOutput.Address) + check(t, "value", types.Siacoins(2), sce.SiacoinOutput.Value) + check(t, "source", explorer.SourceTransaction, sce.Source) + } + + sfUtxos1, err := db.UnspentSiafundOutputs(addr1, n, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr1 sf utxos", 1, len(sfUtxos1)) + for _, sfe := range sfUtxos1 { + check(t, "address", addr1, sfe.SiafundOutput.Address) + check(t, "value", addr1SFs, sfe.SiafundOutput.Value) + } + + sfUtxos2, err := db.UnspentSiafundOutputs(addr2, n, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr2 sf utxos", n-3, len(sfUtxos2)) + for _, sfe := range sfUtxos2 { + check(t, "address", addr2, sfe.SiafundOutput.Address) + check(t, "value", uint64(1), sfe.SiafundOutput.Value) + } + + sfUtxos3, err := db.UnspentSiafundOutputs(addr3, n, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr3 sf utxos", n-3, len(sfUtxos3)) + for _, sfe := range sfUtxos3 { + check(t, "address", addr3, sfe.SiafundOutput.Address) + check(t, "value", uint64(2), sfe.SiafundOutput.Value) + } + } +} diff --git a/persist/sqlite/init.go b/persist/sqlite/init.go index 93fb9f96..cda2ddb9 100644 --- a/persist/sqlite/init.go +++ b/persist/sqlite/init.go @@ -64,10 +64,6 @@ func (s *Store) upgradeDatabase(current, target int64) error { func (s *Store) init() error { // calculate the expected final database version target := int64(len(migrations) + 1) - // disable foreign key constraints during migration - if _, err := s.db.Exec("PRAGMA foreign_keys = OFF"); err != nil { - return fmt.Errorf("failed to disable foreign key constraints: %w", err) - } // error is ignored -- the database may not have been initialized yet. s.db.QueryRow("SELECT COUNT(*) FROM merkle_proofs WHERE i = 0").Scan(&s.numLeaves) diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index d253cd3c..79f092ee 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -25,8 +25,8 @@ CREATE TABLE siacoin_elements ( block_id BLOB REFERENCES blocks(id) ON DELETE CASCADE NOT NULL, output_id BLOB UNIQUE NOT NULL, - leaf_index BLOB UNIQUE NOT NULL, - merkle_proof BLOB UNIQUE NOT NULL, + leaf_index BLOB NOT NULL, + merkle_proof BLOB NOT NULL, spent INTEGER NOT NULL, source INTEGER NOT NULL, @@ -43,8 +43,8 @@ CREATE TABLE siafund_elements ( block_id BLOB REFERENCES blocks(id) ON DELETE CASCADE NOT NULL, output_id BLOB UNIQUE NOT NULL, - leaf_index BLOB UNIQUE NOT NULL, - merkle_proof BLOB UNIQUE NOT NULL, + leaf_index BLOB NOT NULL, + merkle_proof BLOB NOT NULL, spent INTEGER NOT NULL, claim_start BLOB NOT NULL,