From e59f00b99105e70613075d644d4a899ed06b6a61 Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Thu, 4 Apr 2024 17:58:22 -0400 Subject: [PATCH 1/9] add first revert test --- persist/sqlite/consensus_test.go | 77 ++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 2879606a..14ffa123 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -772,3 +772,80 @@ 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) + t.Log(cm.Tip()) + + 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") + } + } +} From ec38c7178e05029bab5666f7b1e152c590e72091 Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Fri, 5 Apr 2024 17:33:54 -0400 Subject: [PATCH 2/9] balance calculation fixes when reverting, fix bug that was preventing foreign key constraints from being checked, make revert balance test more complicated --- persist/sqlite/consensus.go | 41 ++++++--- persist/sqlite/consensus_test.go | 152 ++++++++++++++++++++++++++++++- persist/sqlite/init.go | 4 - persist/sqlite/init.sql | 8 +- 4 files changed, 185 insertions(+), 20 deletions(-) diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 783c6ce5..979f6ff6 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -267,6 +267,11 @@ type balance struct { } func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) error { + _, isRevert := update.(chain.RevertUpdate) + if isRevert { + height++ + } + addresses := make(map[types.Address]balance) update.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) { addresses[sce.SiacoinOutput.Address] = balance{} @@ -300,6 +305,7 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) // log.Println("New block") update.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) { bal := addresses[sce.SiacoinOutput.Address] + // log.Printf("%v (spent: %v): %s siacoins, %s immature siacoins, %d siafunds", sce.SiacoinOutput.Address, spent, bal.sc, bal.immatureSC, bal.sf) if sce.MaturityHeight < height { if spent { // If within the same block, an address A receives SC in one @@ -322,10 +328,13 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) bal.sc = bal.sc.Add(sce.SiacoinOutput.Value) } } else { - if !spent { + if isRevert { + bal.immatureSC = bal.immatureSC.Sub(sce.SiacoinOutput.Value) + } else { bal.immatureSC = bal.immatureSC.Add(sce.SiacoinOutput.Value) } } + // log.Printf("%v (spent: %v): %s siacoins, %s immature siacoins, %d siafunds", sce.SiacoinOutput.Address, spent, bal.sc, bal.immatureSC, bal.sf) addresses[sce.SiacoinOutput.Address] = bal }) update.ForEachSiafundElement(func(sfe types.SiafundElement, spent bool) { @@ -413,13 +422,20 @@ func (s *Store) updateMaturedBalances(dbTxn txn, update consensusUpdate, height // If the update is an apply update then we add the amounts. // If we are reverting then we subtract them. for _, sco := range scos { + var underflow bool bal := addresses[sco.Address] if isRevert { - bal.sc = bal.sc.Sub(sco.Value) + bal.sc, underflow = bal.sc.SubWithUnderflow(sco.Value) + if underflow { + continue + } bal.immatureSC = bal.immatureSC.Add(sco.Value) } else { bal.sc = bal.sc.Add(sco.Value) - bal.immatureSC = bal.immatureSC.Sub(sco.Value) + bal.immatureSC, underflow = bal.immatureSC.SubWithUnderflow(sco.Value) + if underflow { + continue + } } addresses[sco.Address] = bal } @@ -470,7 +486,7 @@ 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) @@ -483,6 +499,7 @@ func (s *Store) addSiacoinElements(dbTxn txn, bid types.BlockID, update consensu if updateErr != nil { return } + // log.Printf("Adding siacoin element: %+v", sce) 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) if err != nil { @@ -599,10 +616,10 @@ func (s *Store) ProcessChainUpdates(crus []chain.RevertUpdate, caus []chain.Appl 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 { - return fmt.Errorf("revertUpdate: failed to update siacoin output state: %w", err) - } else if _, err := s.addSiafundElements(dbTxn, cru.Block.ID(), cru); err != nil { - return fmt.Errorf("revertUpdate: failed to update siafund output state: %w", err) + // } else if _, err := s.addSiacoinElements(dbTxn, cru.Block.ID(), cru); 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 { + // 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 { return fmt.Errorf("revertUpdate: failed to update balances: %w", err) } else if err := s.updateMaturedBalances(dbTxn, cru, cru.State.Index.Height); err != nil { @@ -615,6 +632,10 @@ func (s *Store) ProcessChainUpdates(crus []chain.RevertUpdate, caus []chain.Appl } for _, update := range caus { + if err := s.addBlock(dbTxn, update.Block, update.State.Index.Height); err != nil { + return fmt.Errorf("applyUpdates: failed to add block: %w", err) + } + scDBIds, err := s.addSiacoinElements(dbTxn, update.Block.ID(), update) if err != nil { return fmt.Errorf("applyUpdates: failed to add siacoin outputs: %w", err) @@ -634,9 +655,7 @@ func (s *Store) ProcessChainUpdates(crus []chain.RevertUpdate, caus []chain.Appl 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, update.Block.ID(), update.State.Index.Height, update.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 { return fmt.Errorf("applyUpdates: failed to add transactions: addTransactions: %w", err) diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 14ffa123..cf7f5782 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -830,7 +830,6 @@ func TestRevertTip(t *testing.T) { t.Fatal(err) } syncDB(t, db, cm) - t.Log(cm.Tip()) tip, err := db.Tip() if err != nil { @@ -849,3 +848,154 @@ func TestRevertTip(t *testing.T) { } } } + +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()) + + 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 i := 0; i < len(utxos2); i++ { + check(t, "value", expectedPayout, utxos2[i].SiacoinOutput.Value) + check(t, "source", explorer.SourceMinerPayout, utxos2[i].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) +} 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, From 2b8f58a80a62f2685801feeca0cf77bb6b279d6b Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Mon, 8 Apr 2024 17:04:09 -0400 Subject: [PATCH 3/9] fix underflow issues --- persist/sqlite/consensus.go | 24 ++++++++++++++---------- persist/sqlite/consensus_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 979f6ff6..4e9ab5ad 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -304,10 +304,11 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) // log.Println("New block") update.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) { + var underflow bool bal := addresses[sce.SiacoinOutput.Address] - // log.Printf("%v (spent: %v): %s siacoins, %s immature siacoins, %d siafunds", sce.SiacoinOutput.Address, spent, bal.sc, bal.immatureSC, bal.sf) + // log.Printf("\n\n%v (revert: %v, spent: %v): %s siacoins, %s immature siacoins, %d siafunds (current height: %d, maturity height: %d): change %v", sce.SiacoinOutput.Address, isRevert, spent, bal.sc, bal.immatureSC, bal.sf, height, sce.MaturityHeight, sce.SiacoinOutput.Value) if sce.MaturityHeight < height { - if spent { + if spent || isRevert { // 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 @@ -318,10 +319,9 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) // 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 + bal.sc = types.ZeroCurrency } } else { // log.Println("Gain:", sce.SiacoinOutput.Address, sce.SiacoinOutput.Value) @@ -329,12 +329,15 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) } } else { if isRevert { - bal.immatureSC = bal.immatureSC.Sub(sce.SiacoinOutput.Value) + bal.immatureSC, underflow = bal.immatureSC.SubWithUnderflow(sce.SiacoinOutput.Value) + if underflow { + bal.sc = types.ZeroCurrency + } } else { bal.immatureSC = bal.immatureSC.Add(sce.SiacoinOutput.Value) } } - // log.Printf("%v (spent: %v): %s siacoins, %s immature siacoins, %d siafunds", sce.SiacoinOutput.Address, spent, bal.sc, bal.immatureSC, bal.sf) + // log.Printf("%v (revert: %v, spent: %v): %s siacoins, %s immature siacoins, %d siafunds (current height: %d, maturity height: %d): change %v", sce.SiacoinOutput.Address, isRevert, spent, bal.sc, bal.immatureSC, bal.sf, height, sce.MaturityHeight, sce.SiacoinOutput.Value) addresses[sce.SiacoinOutput.Address] = bal }) update.ForEachSiafundElement(func(sfe types.SiafundElement, spent bool) { @@ -342,9 +345,10 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) if spent { underflow := (bal.sf - sfe.SiafundOutput.Value) > bal.sf if underflow { - return + bal.sf = 0 + } else { + bal.sf -= sfe.SiafundOutput.Value } - bal.sf -= sfe.SiafundOutput.Value } else { bal.sf += sfe.SiafundOutput.Value } @@ -427,14 +431,14 @@ func (s *Store) updateMaturedBalances(dbTxn txn, update consensusUpdate, height if isRevert { bal.sc, underflow = bal.sc.SubWithUnderflow(sco.Value) if underflow { - continue + bal.sc = types.ZeroCurrency } bal.immatureSC = bal.immatureSC.Add(sco.Value) } else { bal.sc = bal.sc.Add(sco.Value) bal.immatureSC, underflow = bal.immatureSC.SubWithUnderflow(sco.Value) if underflow { - continue + bal.immatureSC = types.ZeroCurrency } } addresses[sco.Address] = bal diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index cf7f5782..1a72095f 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -894,6 +894,10 @@ func TestRevertBalance(t *testing.T) { 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() @@ -998,4 +1002,26 @@ func TestRevertBalance(t *testing.T) { // 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) + + t.Log(cm.Tip()) + { + // Reorg everything from before + // Send payout to void instead of addr2 for these blocks + var blocks []types.Block + state := genesisState + for i := uint64(0); i < maturityHeight+10; i++ { + blocks = append(blocks, mineBlock(state, nil, types.VoidAddress)) + 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) + } + t.Log(cm.Tip()) + + checkBalance(addr1, types.ZeroCurrency, types.ZeroCurrency, 0) + checkBalance(addr2, types.ZeroCurrency, types.ZeroCurrency, 0) + checkBalance(addr3, types.ZeroCurrency, types.ZeroCurrency, 0) } From c2410bac81b10bfebf6eaecec42c06e12b6f0f7c Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Mon, 8 Apr 2024 17:45:59 -0400 Subject: [PATCH 4/9] check that there are no more utxos after revert --- persist/sqlite/consensus_test.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 1a72095f..23b52db6 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -1003,7 +1003,6 @@ func TestRevertBalance(t *testing.T) { checkBalance(addr2, utxos2[1].SiacoinOutput.Value, types.ZeroCurrency, 0) checkBalance(addr3, utxos2[0].SiacoinOutput.Value.Sub(hundredSC), types.ZeroCurrency, 0) - t.Log(cm.Tip()) { // Reorg everything from before // Send payout to void instead of addr2 for these blocks @@ -1019,9 +1018,26 @@ func TestRevertBalance(t *testing.T) { } syncDB(t, db, cm) } - t.Log(cm.Tip()) checkBalance(addr1, types.ZeroCurrency, types.ZeroCurrency, 0) checkBalance(addr2, types.ZeroCurrency, 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", 0, len(utxos1)) + + utxos2, err = db.UnspentSiacoinOutputs(addr2, 100, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr2 utxos", 0, len(utxos2)) + + utxos3, err := db.UnspentSiacoinOutputs(addr2, 100, 0) + if err != nil { + t.Fatal(err) + } + check(t, "addr3 utxos", 0, len(utxos3)) } From 13ff146eb4ac591ed74cc3606840010ca7db6eed Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Tue, 9 Apr 2024 16:54:42 -0400 Subject: [PATCH 5/9] add TestRevertSendTransactions --- persist/sqlite/consensus.go | 2 + persist/sqlite/consensus_test.go | 305 ++++++++++++++++++++++++++++++- 2 files changed, 297 insertions(+), 10 deletions(-) diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 4e9ab5ad..f03939b2 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -4,6 +4,7 @@ import ( "database/sql" "errors" "fmt" + "log" "go.sia.tech/core/types" "go.sia.tech/coreutils/chain" @@ -618,6 +619,7 @@ 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 { + log.Println("REVERT!") 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 { diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 23b52db6..4e46c057 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -955,9 +955,9 @@ func TestRevertBalance(t *testing.T) { t.Fatal(err) } check(t, "addr2 utxos", 2, len(utxos2)) - for i := 0; i < len(utxos2); i++ { - check(t, "value", expectedPayout, utxos2[i].SiacoinOutput.Value) - check(t, "source", explorer.SourceMinerPayout, utxos2[i].Source) + 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 @@ -1005,11 +1005,19 @@ func TestRevertBalance(t *testing.T) { { // Reorg everything from before - // Send payout to void instead of addr2 for these blocks + // 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++ { - blocks = append(blocks, mineBlock(state, nil, types.VoidAddress)) + 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++ } @@ -1019,25 +1027,302 @@ func TestRevertBalance(t *testing.T) { syncDB(t, db, cm) } - checkBalance(addr1, types.ZeroCurrency, types.ZeroCurrency, 0) - checkBalance(addr2, types.ZeroCurrency, types.ZeroCurrency, 0) + 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", 0, len(utxos1)) + 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", 0, len(utxos2)) + 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(addr2, 100, 0) + 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()) + + 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) + } + _ = sf + 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)) + } +} From 3b734028d93eac10079c7908886bfe19221e24fb Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Tue, 9 Apr 2024 16:57:01 -0400 Subject: [PATCH 6/9] fix siafund revert balance calculation --- persist/sqlite/consensus.go | 2 +- persist/sqlite/consensus_test.go | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index f03939b2..4e12e0e8 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -343,7 +343,7 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) }) update.ForEachSiafundElement(func(sfe types.SiafundElement, spent bool) { bal := addresses[sfe.SiafundOutput.Address] - if spent { + if spent || isRevert { underflow := (bal.sf - sfe.SiafundOutput.Value) > bal.sf if underflow { bal.sf = 0 diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 4e46c057..9092cb8a 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -1099,10 +1099,9 @@ func TestRevertSendTransactions(t *testing.T) { if err != nil { t.Fatal(err) } - _ = sf check(t, "siacoins", expectSC, sc) check(t, "immature siacoins", expectImmatureSC, immatureSC) - // check(t, "siafunds", expectSF, sf) + check(t, "siafunds", expectSF, sf) } checkTransaction := func(expectTxn types.Transaction, gotTxn explorer.Transaction) { @@ -1318,10 +1317,10 @@ func TestRevertSendTransactions(t *testing.T) { } syncDB(t, db, cm) - // addr1SCs := expectedPayout.Sub(types.Siacoins(1 + 2).Mul64(uint64(n - 3))) - // addr1SFs := giftSF - (1+2)*uint64(n-3) + 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(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)) } From 6d9a4fea629e267989d02c8319111241310a97e1 Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Thu, 11 Apr 2024 16:38:41 -0400 Subject: [PATCH 7/9] rework balance calculations to be more like walletd --- persist/sqlite/consensus.go | 336 ++++++++++++++++++++----------- persist/sqlite/consensus_test.go | 67 ++++++ 2 files changed, 288 insertions(+), 115 deletions(-) diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 4e12e0e8..2f652ab3 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -4,7 +4,6 @@ import ( "database/sql" "errors" "fmt" - "log" "go.sia.tech/core/types" "go.sia.tech/coreutils/chain" @@ -267,19 +266,20 @@ type balance struct { sf uint64 } -func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) error { - _, isRevert := update.(chain.RevertUpdate) - if isRevert { - height++ - } - +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 { @@ -287,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) } @@ -303,63 +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) { - var underflow 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] - // log.Printf("\n\n%v (revert: %v, spent: %v): %s siacoins, %s immature siacoins, %d siafunds (current height: %d, maturity height: %d): change %v", sce.SiacoinOutput.Address, isRevert, spent, bal.sc, bal.immatureSC, bal.sf, height, sce.MaturityHeight, sce.SiacoinOutput.Value) if sce.MaturityHeight < height { - if spent || isRevert { - // 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) - bal.sc, underflow = bal.sc.SubWithUnderflow(sce.SiacoinOutput.Value) - if underflow { - bal.sc = types.ZeroCurrency - } - } 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 isRevert { - bal.immatureSC, underflow = bal.immatureSC.SubWithUnderflow(sce.SiacoinOutput.Value) - if underflow { - bal.sc = types.ZeroCurrency - } - } else { - bal.immatureSC = bal.immatureSC.Add(sce.SiacoinOutput.Value) - } + bal.immatureSC = bal.immatureSC.Sub(sce.SiacoinOutput.Value) } - // log.Printf("%v (revert: %v, spent: %v): %s siacoins, %s immature siacoins, %d siafunds (current height: %d, maturity height: %d): change %v", sce.SiacoinOutput.Address, isRevert, spent, bal.sc, bal.immatureSC, bal.sf, height, sce.MaturityHeight, 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 || isRevert { - underflow := (bal.sf - sfe.SiafundOutput.Value) > bal.sf - if underflow { - bal.sf = 0 - } else { - 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 sfe.SiafundOutput.Value > bal.sf { + 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) } @@ -427,20 +407,13 @@ func (s *Store) updateMaturedBalances(dbTxn txn, update consensusUpdate, height // If the update is an apply update then we add the amounts. // If we are reverting then we subtract them. for _, sco := range scos { - var underflow bool bal := addresses[sco.Address] if isRevert { - bal.sc, underflow = bal.sc.SubWithUnderflow(sco.Value) - if underflow { - bal.sc = types.ZeroCurrency - } + bal.sc = bal.sc.Sub(sco.Value) bal.immatureSC = bal.immatureSC.Add(sco.Value) } else { bal.sc = bal.sc.Add(sco.Value) - bal.immatureSC, underflow = bal.immatureSC.SubWithUnderflow(sco.Value) - if underflow { - bal.immatureSC = types.ZeroCurrency - } + bal.immatureSC = bal.immatureSC.Sub(sco.Value) } addresses[sco.Address] = bal } @@ -464,7 +437,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 @@ -498,32 +471,38 @@ func (s *Store) addSiacoinElements(dbTxn txn, bid types.BlockID, update consensu } 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) } - // log.Printf("Adding siacoin element: %+v", sce) - 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 @@ -533,28 +512,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 { @@ -619,55 +605,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 { - log.Println("REVERT!") - 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 { - // return fmt.Errorf("revertUpdate: failed to update siacoin output state: %w", err) - // } else if _, err := s.addSiafundElements(dbTxn, cru.Block.ID(), cru); 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 { - return fmt.Errorf("revertUpdate: failed to update balances: %w", err) - } else if err := s.updateMaturedBalances(dbTxn, cru, cru.State.Index.Height); err != nil { + if err := s.updateMaturedBalances(dbTxn, cru, cru.State.Index.Height); 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, + append(spentSiacoinElements, ephemeralSiacoinElements...), + newSiacoinElements, + ); err != nil { + return fmt.Errorf("revertUpdate: failed to update siacoin output state: %w", err) + } else if _, err := s.addSiafundElements( + dbTxn, + cru.Block.ID(), + append(spentSiafundElements, ephemeralSiafundElements...), + newSiafundElements, + ); err != nil { + return fmt.Errorf("revertUpdate: failed to update siafund output state: %w", err) + } 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.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 { - if err := s.addBlock(dbTxn, update.Block, update.State.Index.Height); err != nil { + 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, cau, cau.State.Index.Height); err != nil { + return fmt.Errorf("applyUpdates: failed to update matured balances: %w", err) } - scDBIds, err := s.addSiacoinElements(dbTxn, update.Block.ID(), update) + 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.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 9092cb8a..0c4ab976 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -1083,6 +1083,10 @@ func TestRevertSendTransactions(t *testing.T) { 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) @@ -1323,5 +1327,68 @@ func TestRevertSendTransactions(t *testing.T) { 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) + } } } From 24596625b8c3f27b1cc8b80875d4df0c683130a8 Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Thu, 11 Apr 2024 17:37:00 -0400 Subject: [PATCH 8/9] add TestSiafundBalance --- persist/sqlite/consensus.go | 21 +++----- persist/sqlite/consensus_test.go | 88 ++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 13 deletions(-) diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 2f652ab3..0cc8fcf6 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -355,18 +355,13 @@ func (s *Store) updateBalances(dbTxn txn, height uint64, spentSiacoinElements, n 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) @@ -408,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 { @@ -605,7 +600,7 @@ 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.updateMaturedBalances(dbTxn, cru, cru.State.Index.Height); 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) } @@ -662,15 +657,15 @@ func (s *Store) ProcessChainUpdates(crus []chain.RevertUpdate, caus []chain.Appl dbTxn, cru.Block.ID(), cru, - append(spentSiacoinElements, ephemeralSiacoinElements...), - newSiacoinElements, + 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(), - append(spentSiafundElements, ephemeralSiafundElements...), - newSiafundElements, + 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.State.Index.Height+1, spentSiacoinElements, newSiacoinElements, spentSiafundElements, newSiafundElements); err != nil { @@ -687,7 +682,7 @@ func (s *Store) ProcessChainUpdates(crus []chain.RevertUpdate, caus []chain.Appl 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, cau, cau.State.Index.Height); err != nil { + } else if err := s.updateMaturedBalances(dbTxn, false, cau.State.Index.Height); err != nil { return fmt.Errorf("applyUpdates: failed to update matured balances: %w", err) } diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 0c4ab976..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() From 05bb04075b3d19b4ec2de979a36323f817a23519 Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Thu, 11 Apr 2024 18:20:24 -0400 Subject: [PATCH 9/9] make sure genesis block is synced --- cmd/explored/node.go | 6 +----- explorer/explorer.go | 6 +++--- persist/sqlite/consensus.go | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) 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 0cc8fcf6..bb74204a 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -329,7 +329,7 @@ func (s *Store) updateBalances(dbTxn txn, height uint64, spentSiacoinElements, n } for _, sfe := range spentSiafundElements { bal := addresses[sfe.SiafundOutput.Address] - if sfe.SiafundOutput.Value > bal.sf { + if bal.sf < sfe.SiafundOutput.Value { panic("sf underflow") } bal.sf -= sfe.SiafundOutput.Value