Skip to content

Commit

Permalink
track immature balances in database
Browse files Browse the repository at this point in the history
  • Loading branch information
chris124567 committed Mar 8, 2024
1 parent 212e766 commit d05912d
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 59 deletions.
5 changes: 3 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type AddressUTXOsResponse struct {

// AddressBalanceResponse is the response for /addresses/:address/balance.
type AddressBalanceResponse struct {
UnspentSiacoins types.Currency `json:"unspentSiacoins"`
UnspentSiafunds uint64 `json:"unspentSiafunds"`
UnspentSiacoins types.Currency `json:"unspentSiacoins"`
ImmatureSiacoins types.Currency `json:"immatureSiacoins"`
UnspentSiafunds uint64 `json:"unspentSiafunds"`
}
9 changes: 5 additions & 4 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type (
Block(id types.BlockID) (explorer.Block, error)
BestTip(height uint64) (types.ChainIndex, error)
Transactions(ids []types.TransactionID) ([]explorer.Transaction, error)
Balance(address types.Address) (sc types.Currency, sf uint64, err error)
Balance(address types.Address) (sc types.Currency, immatureSC types.Currency, sf uint64, err error)
UnspentSiacoinOutputs(address types.Address, limit, offset uint64) ([]explorer.SiacoinOutput, error)
UnspentSiafundOutputs(address types.Address, limit, offset uint64) ([]explorer.SiafundOutput, error)
Contracts(ids []types.FileContractID) (result []explorer.FileContract, err error)
Expand Down Expand Up @@ -228,14 +228,15 @@ func (s *server) explorerAddressessAddressBalanceHandler(jc jape.Context) {
return
}

sc, sf, err := s.e.Balance(address)
sc, immatureSC, sf, err := s.e.Balance(address)
if jc.Check("failed to get balance", err) != nil {
return
}

jc.Encode(AddressBalanceResponse{
UnspentSiacoins: sc,
UnspentSiafunds: sf,
UnspentSiacoins: sc,
ImmatureSiacoins: immatureSC,
UnspentSiafunds: sf,
})
}

Expand Down
4 changes: 2 additions & 2 deletions explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Store interface {
Transactions(ids []types.TransactionID) ([]Transaction, error)
UnspentSiacoinOutputs(address types.Address, limit, offset uint64) ([]SiacoinOutput, error)
UnspentSiafundOutputs(address types.Address, limit, offset uint64) ([]SiafundOutput, error)
Balance(address types.Address) (sc types.Currency, sf uint64, err error)
Balance(address types.Address) (sc types.Currency, immatureSC types.Currency, sf uint64, err error)
Contracts(ids []types.FileContractID) (result []FileContract, err error)

MerkleProof(leafIndex uint64) ([]types.Hash256, error)
Expand Down Expand Up @@ -70,7 +70,7 @@ func (e *Explorer) UnspentSiafundOutputs(address types.Address, limit, offset ui
}

// Balance returns the balance of an address.
func (e *Explorer) Balance(address types.Address) (sc types.Currency, sf uint64, err error) {
func (e *Explorer) Balance(address types.Address) (sc types.Currency, immatureSC types.Currency, sf uint64, err error) {
return e.s.Balance(address)
}

Expand Down
102 changes: 53 additions & 49 deletions persist/sqlite/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,13 @@ type consensusUpdate interface {
ForEachFileContractElement(fn func(fce types.FileContractElement, rev *types.FileContractElement, resolved, valid bool))
}

func (s *Store) updateBalances(dbTxn txn, update consensusUpdate) error {
type balance struct {
sc types.Currency
sf uint64
}
type balance struct {
sc types.Currency
immatureSC types.Currency
sf uint64
}

func (s *Store) updateBalances(dbTxn txn, update consensusUpdate, height uint64) error {
addresses := make(map[types.Address]balance)
update.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) {
addresses[sce.SiacoinOutput.Address] = balance{}
Expand All @@ -279,7 +280,7 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate) error {
addressList = append(addressList, dbEncode(address))
}

rows, err := dbTxn.Query(`SELECT address, siacoin_balance, siafund_balance
rows, err := dbTxn.Query(`SELECT address, siacoin_balance, immature_siacoin_balance, siafund_balance
FROM address_balance
WHERE address IN (`+queryPlaceHolders(len(addressList))+`)`, addressList...)
if err != nil {
Expand All @@ -288,41 +289,42 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate) error {
defer rows.Close()

for rows.Next() {
var bal balance
var address types.Address
var sc types.Currency
var sf uint64
if err := rows.Scan(dbDecode(&address), dbDecode(&sc), dbDecode(&sf)); err != nil {
if err := rows.Scan(dbDecode(&address), dbDecode(&bal.sc), dbDecode(&bal.immatureSC), dbDecode(&bal.sf)); err != nil {
return err
}
addresses[address] = balance{
sc: sc,
sf: sf,
}
addresses[address] = bal
}

// log.Println("New block")
update.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) {
bal := addresses[sce.SiacoinOutput.Address]
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
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)
}
} else {
if !spent {
bal.immatureSC = bal.immatureSC.Add(sce.SiacoinOutput.Value)
}
} else if sce.MaturityHeight == 0 {
// Outputs that mature later are handled in updateMaturedBalances
// log.Println("Gain:", sce.SiacoinOutput.Address, sce.SiacoinOutput.Value)
bal.sc = bal.sc.Add(sce.SiacoinOutput.Value)
}
addresses[sce.SiacoinOutput.Address] = bal
})
Expand All @@ -340,17 +342,17 @@ func (s *Store) updateBalances(dbTxn txn, update consensusUpdate) error {
addresses[sfe.SiafundOutput.Address] = bal
})

stmt, err := dbTxn.Prepare(`INSERT INTO address_balance(address, siacoin_balance, siafund_balance)
VALUES (?, ?, ?)
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 = ?, siafund_balance = ?`)
DO UPDATE set siacoin_balance = ?, immature_siacoin_balance = ?, siafund_balance = ?`)
if err != nil {
return fmt.Errorf("updateBalances: failed to prepare statement: %w", err)
}
defer stmt.Close()

for addr, bal := range addresses {
if _, err := stmt.Exec(dbEncode(addr), dbEncode(bal.sc), dbEncode(bal.sf), dbEncode(bal.sc), dbEncode(bal.sf)); err != nil {
if _, err := stmt.Exec(dbEncode(addr), dbEncode(bal.sc), dbEncode(bal.immatureSC), dbEncode(bal.sf), dbEncode(bal.sc), dbEncode(bal.immatureSC), dbEncode(bal.sf)); err != nil {
return fmt.Errorf("updateBalances: failed to exec statement: %w", err)
}
// log.Println(addr, "=", bal.sc)
Expand Down Expand Up @@ -390,48 +392,50 @@ func (s *Store) updateMaturedBalances(dbTxn txn, update consensusUpdate, height
addressList = append(addressList, dbEncode(sco.Address))
}

balanceRows, err := dbTxn.Query(`SELECT address, siacoin_balance
balanceRows, err := dbTxn.Query(`SELECT address, siacoin_balance, immature_siacoin_balance
FROM address_balance
WHERE address IN (`+queryPlaceHolders(len(addressList))+`)`, addressList...)
if err != nil {
return fmt.Errorf("updateMaturedBalances: failed to query address_balance: %w", err)
}
defer balanceRows.Close()

addresses := make(map[types.Address]types.Currency)
addresses := make(map[types.Address]balance)
for balanceRows.Next() {
var address types.Address
var sc types.Currency
if err := balanceRows.Scan(dbDecode(&address), dbDecode(&sc)); err != nil {
var bal balance
if err := balanceRows.Scan(dbDecode(&address), dbDecode(&bal.sc), dbDecode(&bal.immatureSC)); err != nil {
return fmt.Errorf("updateMaturedBalances: failed to scan balance: %w", err)
}
addresses[address] = sc
addresses[address] = bal
}

// If the update is an apply update then we add the amounts.
// If we are reverting then we subtract them.
for _, sco := range scos {
bal := addresses[sco.Address]
if isRevert {
bal = bal.Sub(sco.Value)
bal.sc = bal.sc.Sub(sco.Value)
bal.immatureSC = bal.immatureSC.Add(sco.Value)
} else {
bal = bal.Add(sco.Value)
bal.sc = bal.sc.Add(sco.Value)
bal.immatureSC = bal.immatureSC.Sub(sco.Value)
}
addresses[sco.Address] = bal
}

stmt, err := dbTxn.Prepare(`INSERT INTO address_balance(address, siacoin_balance, siafund_balance)
VALUES (?, ?, ?)
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 = ?`)
DO UPDATE set siacoin_balance = ?, immature_siacoin_balance = ?`)
if err != nil {
return fmt.Errorf("updateMaturedBalances: failed to prepare statement: %w", err)
}
defer stmt.Close()

initialSF := dbEncode(uint64(0))
for addr, bal := range addresses {
if _, err := stmt.Exec(dbEncode(addr), dbEncode(bal), initialSF, dbEncode(bal)); err != nil {
if _, err := stmt.Exec(dbEncode(addr), dbEncode(bal.sc), dbEncode(bal.immatureSC), initialSF, dbEncode(bal.sc), dbEncode(bal.immatureSC)); err != nil {
return fmt.Errorf("updateMaturedBalances: failed to exec statement: %w", err)
}
}
Expand Down Expand Up @@ -604,7 +608,7 @@ func (s *Store) applyUpdates() error {
if err != nil {
return fmt.Errorf("applyUpdates: failed to add siafund outputs: %w", err)
}
if err := s.updateBalances(dbTxn, update); err != nil {
if err := s.updateBalances(dbTxn, update, update.State.Index.Height); 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)
Expand Down Expand Up @@ -640,7 +644,7 @@ func (s *Store) revertUpdate(cru *chain.RevertUpdate) error {
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); err != nil {
} 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 {
return fmt.Errorf("revertUpdate: failed to update matured balances: %w", err)
Expand Down
1 change: 1 addition & 0 deletions persist/sqlite/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ CREATE INDEX blocks_height_index ON blocks(height);
CREATE TABLE address_balance (
address BLOB PRIMARY KEY NOT NULL,
siacoin_balance BLOB NOT NULL,
immature_siacoin_balance BLOB NOT NULL,
siafund_balance BLOB NOT NULL
);

Expand Down
4 changes: 2 additions & 2 deletions persist/sqlite/outputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ func (s *Store) UnspentSiafundOutputs(address types.Address, limit, offset uint6
}

// Balance implements explorer.Store.
func (s *Store) Balance(address types.Address) (sc types.Currency, sf uint64, err error) {
func (s *Store) Balance(address types.Address) (sc types.Currency, immatureSC types.Currency, sf uint64, err error) {
err = s.transaction(func(tx txn) error {
err = tx.QueryRow(`SELECT siacoin_balance, siafund_balance FROM address_balance WHERE address = ?`, dbEncode(address)).Scan(dbDecode(&sc), dbDecode(&sf))
err = tx.QueryRow(`SELECT siacoin_balance, immature_siacoin_balance, siafund_balance FROM address_balance WHERE address = ?`, dbEncode(address)).Scan(dbDecode(&sc), dbDecode(&immatureSC), dbDecode(&sf))
if err != nil {
return fmt.Errorf("failed to query balances: %w", err)
}
Expand Down

0 comments on commit d05912d

Please sign in to comment.