diff --git a/api/api_test.go b/api/api_test.go index d8f3191..fbf5039 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -801,11 +801,11 @@ func TestV2(t *testing.T) { Value: sce.SiacoinOutput.Value, }}, Signatures: []types.TransactionSignature{{ - ParentID: sce.ID, + ParentID: types.Hash256(sce.ID), CoveredFields: types.CoveredFields{WholeTransaction: true}, }}, } - sig := key.SignHash(cm.TipState().WholeSigHash(txn, sce.ID, 0, 0, nil)) + sig := key.SignHash(cm.TipState().WholeSigHash(txn, types.Hash256(sce.ID), 0, 0, nil)) txn.Signatures[0].Signature = sig[:] if err := addBlock([]types.Transaction{txn}, nil); err != nil { return err @@ -1104,7 +1104,7 @@ func TestP2P(t *testing.T) { Value: sce.SiacoinOutput.Value, }}, Signatures: []types.TransactionSignature{{ - ParentID: sce.ID, + ParentID: types.Hash256(sce.ID), CoveredFields: types.CoveredFields{WholeTransaction: true}, }}, } @@ -1112,7 +1112,7 @@ func TestP2P(t *testing.T) { if err != nil { return err } - sig := key.SignHash(cs.WholeSigHash(txn, sce.ID, 0, 0, nil)) + sig := key.SignHash(cs.WholeSigHash(txn, types.Hash256(sce.ID), 0, 0, nil)) txn.Signatures[0].Signature = sig[:] if err := c.TxpoolBroadcast([]types.Transaction{txn}, nil); err != nil { return err diff --git a/go.mod b/go.mod index 71822d4..919ea9f 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ toolchain go1.23.2 require ( github.com/mattn/go-sqlite3 v1.14.24 - go.sia.tech/core v0.5.0 - go.sia.tech/coreutils v0.5.0 + go.sia.tech/core v0.6.1 + go.sia.tech/coreutils v0.6.0 go.sia.tech/jape v0.12.1 go.sia.tech/web/walletd v0.23.1 go.uber.org/zap v1.27.0 diff --git a/go.sum b/go.sum index 9992d92..01d1b5b 100644 --- a/go.sum +++ b/go.sum @@ -10,10 +10,10 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.sia.tech/core v0.5.0 h1:feLC7DSCF+PhU157s/94106hFKyiGrGQ9HC3/dF/l7E= -go.sia.tech/core v0.5.0/go.mod h1:P3C1BWa/7J4XgdzWuaYHBvLo2RzZ0UBaJM4TG1GWB2g= -go.sia.tech/coreutils v0.5.0 h1:/xKxdw83iZy0jjLzI2NGHyG4azyjK5DJscxpkr6nIGQ= -go.sia.tech/coreutils v0.5.0/go.mod h1:VYM4FcmlhVrpDGvglLHjRW+gitoaxPNLvp5mL2quilo= +go.sia.tech/core v0.6.1 h1:eaExM2E2eNr43su2XDkY5J24E3F54YGS7hcC3WtVjVk= +go.sia.tech/core v0.6.1/go.mod h1:P3C1BWa/7J4XgdzWuaYHBvLo2RzZ0UBaJM4TG1GWB2g= +go.sia.tech/coreutils v0.6.0 h1:r0IZt+aVdGG2uIHl7OtaWRYdVx4NQ7ezRoSGa0Ej8GY= +go.sia.tech/coreutils v0.6.0/go.mod h1:XlsnogeYU/Tdjzp/HUNAj5T7tZCdmeBHIBjymbPC+uQ= go.sia.tech/jape v0.12.1 h1:xr+o9V8FO8ScRqbSaqYf9bjj1UJ2eipZuNcI1nYousU= go.sia.tech/jape v0.12.1/go.mod h1:wU+h6Wh5olDjkPXjF0tbZ1GDgoZ6VTi4naFw91yyWC4= go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c= diff --git a/persist/sqlite/addresses.go b/persist/sqlite/addresses.go index 64e3d74..526e08e 100644 --- a/persist/sqlite/addresses.go +++ b/persist/sqlite/addresses.go @@ -103,14 +103,14 @@ func (s *Store) AddressSiacoinOutputs(address types.Address, index types.ChainIn if s.indexMode == wallet.IndexModeFull { indices := make([]uint64, len(siacoins)) for i, se := range siacoins { - indices[i] = se.LeafIndex + indices[i] = se.StateElement.LeafIndex } proofs, err := fillElementProofs(tx, indices) if err != nil { return fmt.Errorf("failed to fill element proofs: %w", err) } for i, proof := range proofs { - siacoins[i].MerkleProof = proof + siacoins[i].StateElement.MerkleProof = proof } } return nil @@ -148,14 +148,14 @@ func (s *Store) AddressSiafundOutputs(address types.Address, offset, limit int) if s.indexMode == wallet.IndexModeFull { indices := make([]uint64, len(siafunds)) for i, se := range siafunds { - indices[i] = se.LeafIndex + indices[i] = se.StateElement.LeafIndex } proofs, err := fillElementProofs(tx, indices) if err != nil { return fmt.Errorf("failed to fill element proofs: %w", err) } for i, proof := range proofs { - siafunds[i].MerkleProof = proof + siafunds[i].StateElement.MerkleProof = proof } } return nil @@ -244,13 +244,13 @@ func (s *Store) AnnotateV1Events(index types.ChainIndex, timestamp time.Time, v1 for i, output := range txn.SiacoinOutputs { sce := types.SiacoinElement{ + ID: txn.SiacoinOutputID(i), StateElement: types.StateElement{ - ID: types.Hash256(txn.SiacoinOutputID(i)), LeafIndex: types.UnassignedLeafIndex, }, SiacoinOutput: output, } - siacoinElementCache[types.SiacoinOutputID(sce.StateElement.ID)] = sce + siacoinElementCache[sce.ID] = sce relevant = true } @@ -268,13 +268,13 @@ func (s *Store) AnnotateV1Events(index types.ChainIndex, timestamp time.Time, v1 for i, output := range txn.SiafundOutputs { sfe := types.SiafundElement{ + ID: txn.SiafundOutputID(i), StateElement: types.StateElement{ - ID: types.Hash256(txn.SiafundOutputID(i)), LeafIndex: types.UnassignedLeafIndex, }, SiafundOutput: output, } - siafundElementCache[types.SiafundOutputID(sfe.StateElement.ID)] = sfe + siafundElementCache[sfe.ID] = sfe relevant = true } diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index fa6ea33..2129eab 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -3,7 +3,6 @@ package sqlite import ( "bytes" "database/sql" - "encoding/json" "errors" "fmt" @@ -26,96 +25,36 @@ type addressRef struct { Balance wallet.Balance } -func (ut *updateTx) SiacoinStateElements() ([]types.StateElement, error) { - if ut.indexMode == wallet.IndexModeFull { - panic("SiacoinStateElements called in full index mode") - } - - const query = `SELECT id, leaf_index, merkle_proof FROM siacoin_elements` - rows, err := ut.tx.Query(query) - if err != nil { - return nil, fmt.Errorf("failed to query siacoin elements: %w", err) - } - defer rows.Close() - - var elements []types.StateElement - for rows.Next() { - se, err := scanStateElement(rows) - if err != nil { - return nil, fmt.Errorf("failed to scan state element: %w", err) - } - elements = append(elements, se) - } - return elements, rows.Err() +type stateElement struct { + ID types.Hash256 + types.StateElement } -func (ut *updateTx) UpdateSiacoinStateElements(elements []types.StateElement) error { +func (ut *updateTx) UpdateStateElementProofs(update wallet.ProofUpdater) error { if ut.indexMode == wallet.IndexModeFull { - panic("UpdateSiacoinStateElements called in full index mode") + panic("UpdateStateElementProofs called in full index mode") } - log := ut.tx.log.Named("UpdateSiacoinStateElements") - log.Debug("updating siacoin state elements", zap.Int("count", len(elements))) - - const query = `UPDATE siacoin_elements SET merkle_proof=$1, leaf_index=$2 WHERE id=$3 RETURNING id` - stmt, err := ut.tx.Prepare(query) + se, err := getSiacoinStateElements(ut.tx) if err != nil { - return fmt.Errorf("failed to prepare statement: %w", err) + return fmt.Errorf("failed to get siacoin state elements: %w", err) } - defer stmt.Close() - - for _, se := range elements { - var dummy types.Hash256 - err := stmt.QueryRow(encode(se.MerkleProof), se.LeafIndex, encode(se.ID)).Scan(decode(&dummy)) - if err != nil { - return fmt.Errorf("failed to execute statement: %w", err) - } - log.Debug("updated element proof", zap.Stringer("id", se.ID), zap.Uint64("leafIndex", se.LeafIndex)) + for i := range se { + update.UpdateElementProof(&se[i].StateElement) } - return nil -} - -func (ut *updateTx) SiafundStateElements() ([]types.StateElement, error) { - if ut.indexMode == wallet.IndexModeFull { - panic("SiafundStateElements called in full index mode") + if err := updateSiacoinStateElements(ut.tx, se); err != nil { + return fmt.Errorf("failed to update siacoin state elements: %w", err) } - const query = `SELECT id, leaf_index, merkle_proof FROM siafund_elements` - rows, err := ut.tx.Query(query) + sfe, err := getSiafundStateElements(ut.tx) if err != nil { - return nil, fmt.Errorf("failed to query siacoin elements: %w", err) + return fmt.Errorf("failed to get siafund state elements: %w", err) } - defer rows.Close() - - var elements []types.StateElement - for rows.Next() { - se, err := scanStateElement(rows) - if err != nil { - return nil, fmt.Errorf("failed to scan state element: %w", err) - } - elements = append(elements, se) - } - return elements, rows.Err() -} - -func (ut *updateTx) UpdateSiafundStateElements(elements []types.StateElement) error { - if ut.indexMode == wallet.IndexModeFull { - panic("UpdateSiafundStateElements called in full index mode") - } - - const query = `UPDATE siafund_elements SET merkle_proof=$1, leaf_index=$2 WHERE id=$3 RETURNING id` - stmt, err := ut.tx.Prepare(query) - if err != nil { - return fmt.Errorf("failed to prepare statement: %w", err) + for i := range sfe { + update.UpdateElementProof(&sfe[i].StateElement) } - defer stmt.Close() - - for _, se := range elements { - var dummy types.Hash256 - err := stmt.QueryRow(encode(se.MerkleProof), se.LeafIndex, encode(se.ID)).Scan(decode(&dummy)) - if err != nil { - return fmt.Errorf("failed to execute statement: %w", err) - } + if err := updateSiafundStateElements(ut.tx, sfe); err != nil { + return fmt.Errorf("failed to update siafund state elements: %w", err) } return nil } @@ -319,9 +258,90 @@ func (s *Store) SetIndexMode(mode wallet.IndexMode) error { }) } -func scanStateElement(s scanner) (se types.StateElement, err error) { - err = s.Scan(decode(&se.ID), &se.LeafIndex, decode(&se.MerkleProof)) - return +func getSiacoinStateElements(tx *txn) ([]stateElement, error) { + const query = `SELECT id, leaf_index, merkle_proof FROM siacoin_elements` + rows, err := tx.Query(query) + if err != nil { + return nil, fmt.Errorf("failed to query siacoin elements: %w", err) + } + defer rows.Close() + + var elements []stateElement + for rows.Next() { + var se stateElement + if err := rows.Scan(decode(&se.ID), &se.LeafIndex, decode(&se.MerkleProof)); err != nil { + return nil, fmt.Errorf("failed to scan siacoin element: %w", err) + } + elements = append(elements, se) + } + return elements, rows.Err() +} + +func getSiafundStateElements(tx *txn) ([]stateElement, error) { + const query = `SELECT id, leaf_index, merkle_proof FROM siafund_elements` + rows, err := tx.Query(query) + if err != nil { + return nil, fmt.Errorf("failed to query siafund elements: %w", err) + } + defer rows.Close() + + var elements []stateElement + for rows.Next() { + var se stateElement + if err := rows.Scan(decode(&se.ID), &se.LeafIndex, decode(&se.MerkleProof)); err != nil { + return nil, fmt.Errorf("failed to scan siacoin element: %w", err) + } + elements = append(elements, se) + } + return elements, rows.Err() +} + +func updateSiafundStateElements(tx *txn, elements []stateElement) error { + if len(elements) == 0 { + return nil + } + const query = `UPDATE siafund_elements SET merkle_proof=$1, leaf_index=$2 WHERE id=$3` + stmt, err := tx.Prepare(query) + if err != nil { + return fmt.Errorf("failed to prepare statement: %w", err) + } + defer stmt.Close() + + for _, se := range elements { + res, err := stmt.Exec(encode(se.MerkleProof), se.LeafIndex, encode(se.ID)) + if err != nil { + return fmt.Errorf("failed to execute statement: %w", err) + } else if n, err := res.RowsAffected(); err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } else if n != 1 { + return fmt.Errorf("expected 1 row affected, got %v", n) + } + } + return nil +} + +func updateSiacoinStateElements(tx *txn, elements []stateElement) error { + if len(elements) == 0 { + return nil + } + const query = `UPDATE siacoin_elements SET merkle_proof=$1, leaf_index=$2 WHERE id=$3` + stmt, err := tx.Prepare(query) + if err != nil { + return fmt.Errorf("failed to prepare statement: %w", err) + } + defer stmt.Close() + + for _, se := range elements { + res, err := stmt.Exec(encode(se.MerkleProof), se.LeafIndex, encode(se.ID)) + if err != nil { + return fmt.Errorf("failed to execute statement: %w", err) + } else if n, err := res.RowsAffected(); err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } else if n != 1 { + return fmt.Errorf("expected 1 row affected, got %v", n) + } + } + return nil } func scanAddress(s scanner) (ab addressRef, err error) { @@ -530,10 +550,10 @@ func addSiacoinElements(tx *txn, elements []types.SiacoinElement, indexID int64, // in full index mode, Merkle proofs are stored in the state tree table // rather than per element. if indexMode == wallet.IndexModeFull { - se.MerkleProof = nil + se.StateElement.MerkleProof = nil } - _, err = insertStmt.Exec(encode(se.ID), encode(se.SiacoinOutput.Value), encode(se.MerkleProof), se.LeafIndex, se.MaturityHeight, addrRef.ID, se.MaturityHeight == 0, indexID) + _, err = insertStmt.Exec(encode(se.ID), encode(se.SiacoinOutput.Value), encode(se.StateElement.MerkleProof), se.StateElement.LeafIndex, se.MaturityHeight, addrRef.ID, se.MaturityHeight == 0, indexID) if err != nil { return fmt.Errorf("failed to execute statement: %w", err) } @@ -804,10 +824,10 @@ func addSiafundElements(tx *txn, elements []types.SiafundElement, indexID int64, // in full index mode, Merkle proofs are stored in the state tree table // rather than per element. if indexMode == wallet.IndexModeFull { - se.MerkleProof = nil + se.StateElement.MerkleProof = nil } - _, err = insertStmt.Exec(encode(se.ID), se.SiafundOutput.Value, encode(se.MerkleProof), se.LeafIndex, encode(se.ClaimStart), addrRef.ID, indexID) + _, err = insertStmt.Exec(encode(se.ID), se.SiafundOutput.Value, encode(se.StateElement.MerkleProof), se.StateElement.LeafIndex, encode(se.ClaimStart), addrRef.ID, indexID) if err != nil { return fmt.Errorf("failed to execute statement: %w", err) } else if exists { @@ -1052,15 +1072,18 @@ func addEvents(tx *txn, events []wallet.Event, indexID int64) error { defer relevantAddrStmt.Close() var buf bytes.Buffer - enc := json.NewEncoder(&buf) + enc := types.NewEncoder(&buf) for _, event := range events { buf.Reset() - if err := enc.Encode(event.Data); err != nil { - return fmt.Errorf("failed to encode event: %w", err) + ev, ok := event.Data.(types.EncoderTo) + if !ok { + panic("event data does not implement types.EncoderTo") // developer error } + ev.EncodeTo(enc) + enc.Flush() var eventID int64 - err = insertEventStmt.QueryRow(encode(event.ID), event.MaturityHeight, encode(event.Timestamp), event.Type, buf.String(), indexID).Scan(&eventID) + err = insertEventStmt.QueryRow(encode(event.ID), event.MaturityHeight, encode(event.Timestamp), event.Type, buf.Bytes(), indexID).Scan(&eventID) if errors.Is(err, sql.ErrNoRows) { continue // skip if the event already exists } else if err != nil { diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index cc77c4f..107bace 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -152,10 +152,10 @@ func TestPruneSiacoins(t *testing.T) { }, } - sigHash := cm.TipState().WholeSigHash(txn, utxos[0].ID, 0, 0, nil) + sigHash := cm.TipState().WholeSigHash(txn, types.Hash256(utxos[0].ID), 0, 0, nil) sig := pk.SignHash(sigHash) txn.Signatures = append(txn.Signatures, types.TransactionSignature{ - ParentID: utxos[0].ID, + ParentID: types.Hash256(utxos[0].ID), CoveredFields: types.CoveredFields{WholeTransaction: true}, PublicKeyIndex: 0, Timelock: 0, @@ -277,10 +277,10 @@ func TestPruneSiafunds(t *testing.T) { }, } - sigHash := cm.TipState().WholeSigHash(txn, utxos[0].ID, 0, 0, nil) + sigHash := cm.TipState().WholeSigHash(txn, types.Hash256(utxos[0].ID), 0, 0, nil) sig := pk.SignHash(sigHash) txn.Signatures = append(txn.Signatures, types.TransactionSignature{ - ParentID: utxos[0].ID, + ParentID: types.Hash256(utxos[0].ID), CoveredFields: types.CoveredFields{WholeTransaction: true}, PublicKeyIndex: 0, Timelock: 0, diff --git a/persist/sqlite/events.go b/persist/sqlite/events.go index 69c62da..1ecd94f 100644 --- a/persist/sqlite/events.go +++ b/persist/sqlite/events.go @@ -2,7 +2,6 @@ package sqlite import ( "database/sql" - "encoding/json" "errors" "fmt" @@ -62,47 +61,44 @@ WHERE ev.event_id = $1` return } +func decodeEventData[T wallet.EventPayout | + wallet.EventV1Transaction | + wallet.EventV2Transaction | + wallet.EventV1ContractResolution | + wallet.EventV2ContractResolution, TP interface { + *T + types.DecoderFrom +}](dec *types.Decoder) T { + v := new(T) + TP(v).DecodeFrom(dec) + return *v +} + func scanEvent(s scanner) (ev wallet.Event, eventID int64, err error) { var eventBuf []byte - err = s.Scan(&eventID, decode(&ev.ID), &ev.MaturityHeight, decode(&ev.Timestamp), &ev.Index.Height, decode(&ev.Index.ID), &ev.Confirmations, &ev.Type, &eventBuf) if err != nil { return } + dec := types.NewBufDecoder(eventBuf) switch ev.Type { case wallet.EventTypeV1Transaction: - var tx wallet.EventV1Transaction - if err = json.Unmarshal(eventBuf, &tx); err != nil { - return wallet.Event{}, 0, fmt.Errorf("failed to unmarshal transaction event: %w", err) - } - ev.Data = tx + ev.Data = decodeEventData[wallet.EventV1Transaction](dec) case wallet.EventTypeV2Transaction: - var tx wallet.EventV2Transaction - if err = json.Unmarshal(eventBuf, &tx); err != nil { - return wallet.Event{}, 0, fmt.Errorf("failed to unmarshal transaction event: %w", err) - } - ev.Data = tx + ev.Data = decodeEventData[wallet.EventV2Transaction](dec) case wallet.EventTypeV1ContractResolution: - var r wallet.EventV1ContractResolution - if err = json.Unmarshal(eventBuf, &r); err != nil { - return wallet.Event{}, 0, fmt.Errorf("failed to unmarshal missed file contract event: %w", err) - } - ev.Data = r + ev.Data = decodeEventData[wallet.EventV1ContractResolution](dec) case wallet.EventTypeV2ContractResolution: - var r wallet.EventV2ContractResolution - if err = json.Unmarshal(eventBuf, &r); err != nil { - return wallet.Event{}, 0, fmt.Errorf("failed to unmarshal file contract event: %w", err) - } - ev.Data = r + ev.Data = decodeEventData[wallet.EventV2ContractResolution](dec) case wallet.EventTypeSiafundClaim, wallet.EventTypeMinerPayout, wallet.EventTypeFoundationSubsidy: - var p wallet.EventPayout - if err = json.Unmarshal(eventBuf, &p); err != nil { - return wallet.Event{}, 0, fmt.Errorf("failed to unmarshal event %q (%q): %w", ev.ID, ev.Type, err) - } - ev.Data = p + ev.Data = decodeEventData[wallet.EventPayout](dec) default: return wallet.Event{}, 0, fmt.Errorf("unknown event type: %q", ev.Type) } + if err := dec.Err(); err != nil { + return wallet.Event{}, 0, fmt.Errorf("failed to decode event data: %w", err) + } + return } diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index 37eba72..cf6a94b 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -17,7 +17,7 @@ CREATE TABLE siacoin_elements ( id BLOB PRIMARY KEY, siacoin_value BLOB NOT NULL, merkle_proof BLOB NOT NULL, - leaf_index INTEGER NOT NULL, + leaf_index INTEGER UNIQUE NOT NULL, maturity_height INTEGER NOT NULL, /* stored as int64 for easier querying */ address_id INTEGER NOT NULL REFERENCES sia_addresses (id), matured BOOLEAN NOT NULL, /* tracks whether the value has been added to the address balance */ @@ -34,7 +34,7 @@ CREATE TABLE siafund_elements ( id BLOB PRIMARY KEY, claim_start BLOB NOT NULL, merkle_proof BLOB NOT NULL, - leaf_index INTEGER NOT NULL, + leaf_index INTEGER UNIQUE NOT NULL, siafund_value INTEGER NOT NULL, address_id INTEGER NOT NULL REFERENCES sia_addresses (id), chain_index_id INTEGER NOT NULL REFERENCES chain_indices (id), diff --git a/persist/sqlite/migrations.go b/persist/sqlite/migrations.go index 8429fe4..bce12b8 100644 --- a/persist/sqlite/migrations.go +++ b/persist/sqlite/migrations.go @@ -7,6 +7,77 @@ import ( "go.uber.org/zap" ) +// migrateVersion5 resets the database to trigger a full resync to switch +// events from JSON to Sia encoding +func migrateVersion5(tx *txn, _ *zap.Logger) error { + if _, err := tx.Exec(`DELETE FROM siacoin_elements;`); err != nil { + return fmt.Errorf("failed to delete siacoin_elements: %w", err) + } else if _, err := tx.Exec(`DELETE FROM siafund_elements;`); err != nil { + return fmt.Errorf("failed to delete siafund_elements: %w", err) + } else if _, err := tx.Exec(`DELETE FROM state_tree;`); err != nil { + return fmt.Errorf("failed to delete state_tree: %w", err) + } else if _, err := tx.Exec(`DELETE FROM event_addresses;`); err != nil { + return fmt.Errorf("failed to delete event_addresses: %w", err) + } else if _, err := tx.Exec(`DELETE FROM events;`); err != nil { + return fmt.Errorf("failed to delete events: %w", err) + } else if _, err := tx.Exec(`DELETE FROM chain_indices;`); err != nil { + return fmt.Errorf("failed to delete chain_indices: %w", err) + } else if _, err := tx.Exec(`DROP TABLE siacoin_elements;`); err != nil { + return fmt.Errorf("failed to drop siacoin_elements: %w", err) + } else if _, err := tx.Exec(`DROP TABLE siafund_elements;`); err != nil { + return fmt.Errorf("failed to drop siafund_elements: %w", err) + } + + _, err := tx.Exec(`UPDATE global_settings SET last_indexed_height=0, last_indexed_id=$1, element_num_leaves=0`, encode(types.ChainIndex{})) + if err != nil { + return fmt.Errorf("failed to reset global_settings: %w", err) + } + + _, err = tx.Exec(`UPDATE sia_addresses SET siacoin_balance=$1, immature_siacoin_balance=$1, siafund_balance=0;`, encode(types.ZeroCurrency)) + if err != nil { + return fmt.Errorf("failed to reset sia_addresses: %w", err) + } + + _, err = tx.Exec(`CREATE TABLE siacoin_elements ( + id BLOB PRIMARY KEY, + siacoin_value BLOB NOT NULL, + merkle_proof BLOB NOT NULL, + leaf_index INTEGER UNIQUE NOT NULL, + maturity_height INTEGER NOT NULL, /* stored as int64 for easier querying */ + address_id INTEGER NOT NULL REFERENCES sia_addresses (id), + matured BOOLEAN NOT NULL, /* tracks whether the value has been added to the address balance */ + chain_index_id INTEGER NOT NULL REFERENCES chain_indices (id), + spent_index_id INTEGER REFERENCES chain_indices (id) /* soft delete */ +); +CREATE INDEX siacoin_elements_address_id_idx ON siacoin_elements (address_id); +CREATE INDEX siacoin_elements_maturity_height_matured_idx ON siacoin_elements (maturity_height, matured); +CREATE INDEX siacoin_elements_chain_index_id_idx ON siacoin_elements (chain_index_id); +CREATE INDEX siacoin_elements_spent_index_id_idx ON siacoin_elements (spent_index_id); +CREATE INDEX siacoin_elements_address_id_spent_index_id_idx ON siacoin_elements(address_id, spent_index_id);`) + if err != nil { + return fmt.Errorf("failed to create siacoin_elements: %w", err) + } + + _, err = tx.Exec(`CREATE TABLE siafund_elements ( + id BLOB PRIMARY KEY, + claim_start BLOB NOT NULL, + merkle_proof BLOB NOT NULL, + leaf_index INTEGER UNIQUE NOT NULL, + siafund_value INTEGER NOT NULL, + address_id INTEGER NOT NULL REFERENCES sia_addresses (id), + chain_index_id INTEGER NOT NULL REFERENCES chain_indices (id), + spent_index_id INTEGER REFERENCES chain_indices (id) /* soft delete */ +); +CREATE INDEX siafund_elements_address_id_idx ON siafund_elements (address_id); +CREATE INDEX siafund_elements_chain_index_id_idx ON siafund_elements (chain_index_id); +CREATE INDEX siafund_elements_spent_index_id_idx ON siafund_elements (spent_index_id); +CREATE INDEX siafund_elements_address_id_spent_index_id_idx ON siafund_elements(address_id, spent_index_id);`) + if err != nil { + return fmt.Errorf("failed to create siafund_elements: %w", err) + } + return nil +} + // migrateVersion4 splits the height and ID of the last indexed tip into two // separate columns for easier querying. func migrateVersion4(tx *txn, _ *zap.Logger) error { diff --git a/persist/sqlite/utxo.go b/persist/sqlite/utxo.go index 5bcf9bf..881d700 100644 --- a/persist/sqlite/utxo.go +++ b/persist/sqlite/utxo.go @@ -24,13 +24,13 @@ WHERE se.id=$1 AND spent_index_id IS NULL` // retrieve the merkle proofs for the siacoin element if s.indexMode == wallet.IndexModeFull { - proof, err := fillElementProofs(tx, []uint64{ele.LeafIndex}) + proof, err := fillElementProofs(tx, []uint64{ele.StateElement.LeafIndex}) if err != nil { return fmt.Errorf("failed to fill element proofs: %w", err) } else if len(proof) != 1 { panic("expected exactly one proof") // should never happen } - ele.MerkleProof = proof[0] + ele.StateElement.MerkleProof = proof[0] } return nil }) @@ -55,13 +55,13 @@ WHERE se.id=$1 AND spent_index_id IS NULL` // retrieve the merkle proofs for the siafund element if s.indexMode == wallet.IndexModeFull { - proof, err := fillElementProofs(tx, []uint64{ele.LeafIndex}) + proof, err := fillElementProofs(tx, []uint64{ele.StateElement.LeafIndex}) if err != nil { return fmt.Errorf("failed to fill element proofs: %w", err) } else if len(proof) != 1 { panic("expected exactly one proof") // should never happen } - ele.MerkleProof = proof[0] + ele.StateElement.MerkleProof = proof[0] } return nil }) diff --git a/persist/sqlite/wallet.go b/persist/sqlite/wallet.go index 05f8f76..45f572f 100644 --- a/persist/sqlite/wallet.go +++ b/persist/sqlite/wallet.go @@ -258,14 +258,14 @@ func (s *Store) WalletSiacoinOutputs(id wallet.ID, index types.ChainIndex, offse if s.indexMode == wallet.IndexModeFull { indices := make([]uint64, len(siacoins)) for i, se := range siacoins { - indices[i] = se.LeafIndex + indices[i] = se.StateElement.LeafIndex } proofs, err := fillElementProofs(tx, indices) if err != nil { return fmt.Errorf("failed to fill element proofs: %w", err) } for i, proof := range proofs { - siacoins[i].MerkleProof = proof + siacoins[i].StateElement.MerkleProof = proof } } return nil @@ -307,14 +307,14 @@ func (s *Store) WalletSiafundOutputs(id wallet.ID, offset, limit int) (siafunds if s.indexMode == wallet.IndexModeFull { indices := make([]uint64, len(siafunds)) for i, se := range siafunds { - indices[i] = se.LeafIndex + indices[i] = se.StateElement.LeafIndex } proofs, err := fillElementProofs(tx, indices) if err != nil { return fmt.Errorf("failed to fill element proofs: %w", err) } for i, proof := range proofs { - siafunds[i].MerkleProof = proof + siafunds[i].StateElement.MerkleProof = proof } } return nil @@ -489,13 +489,13 @@ func (s *Store) WalletUnconfirmedEvents(id wallet.ID, index types.ChainIndex, ti } sce := types.SiacoinElement{ + ID: txn.SiacoinOutputID(i), StateElement: types.StateElement{ - ID: types.Hash256(txn.SiacoinOutputID(i)), LeafIndex: types.UnassignedLeafIndex, }, SiacoinOutput: output, } - siacoinElementCache[types.SiacoinOutputID(sce.StateElement.ID)] = sce + siacoinElementCache[sce.ID] = sce } for _, input := range txn.SiafundInputs { @@ -528,13 +528,13 @@ func (s *Store) WalletUnconfirmedEvents(id wallet.ID, index types.ChainIndex, ti } sfe := types.SiafundElement{ + ID: txn.SiafundOutputID(i), StateElement: types.StateElement{ - ID: types.Hash256(txn.SiafundOutputID(i)), LeafIndex: types.UnassignedLeafIndex, }, SiafundOutput: output, } - siafundElementCache[types.SiafundOutputID(sfe.StateElement.ID)] = sfe + siafundElementCache[sfe.ID] = sfe } if len(relevant) == 0 { @@ -593,12 +593,12 @@ func (s *Store) WalletUnconfirmedEvents(id wallet.ID, index types.ChainIndex, ti } func scanSiacoinElement(s scanner) (se types.SiacoinElement, err error) { - err = s.Scan(decode(&se.ID), decode(&se.SiacoinOutput.Value), decode(&se.MerkleProof), &se.LeafIndex, &se.MaturityHeight, decode(&se.SiacoinOutput.Address)) + err = s.Scan(decode(&se.ID), decode(&se.SiacoinOutput.Value), decode(&se.StateElement.MerkleProof), &se.StateElement.LeafIndex, &se.MaturityHeight, decode(&se.SiacoinOutput.Address)) return } func scanSiafundElement(s scanner) (se types.SiafundElement, err error) { - err = s.Scan(decode(&se.ID), &se.LeafIndex, decode(&se.MerkleProof), &se.SiafundOutput.Value, decode(&se.ClaimStart), decode(&se.SiafundOutput.Address)) + err = s.Scan(decode(&se.ID), &se.StateElement.LeafIndex, decode(&se.StateElement.MerkleProof), &se.SiafundOutput.Value, decode(&se.ClaimStart), decode(&se.SiafundOutput.Address)) return } diff --git a/wallet/update.go b/wallet/update.go index 4f458dd..9392c7a 100644 --- a/wallet/update.go +++ b/wallet/update.go @@ -12,10 +12,15 @@ type ( // A stateTreeUpdater is an interface for applying and reverting // Merkle tree updates. stateTreeUpdater interface { - UpdateElementProof(e *types.StateElement) + UpdateElementProof(*types.StateElement) ForEachTreeNode(fn func(row uint64, col uint64, h types.Hash256)) } + // A ProofUpdater is an interface for updating Merkle proofs. + ProofUpdater interface { + UpdateElementProof(*types.StateElement) + } + // AddressBalance pairs an address with its balance. AddressBalance struct { Address types.Address `json:"address"` @@ -53,12 +58,7 @@ type ( // An UpdateTx atomically updates the state of a store. UpdateTx interface { - SiacoinStateElements() ([]types.StateElement, error) - UpdateSiacoinStateElements([]types.StateElement) error - - SiafundStateElements() ([]types.StateElement, error) - UpdateSiafundStateElements([]types.StateElement) error - + UpdateStateElementProofs(ProofUpdater) error UpdateStateTree([]TreeNodeUpdate) error AddressRelevant(types.Address) (bool, error) @@ -82,31 +82,7 @@ func updateStateElements(tx UpdateTx, update stateTreeUpdater, indexMode IndexMo }) return tx.UpdateStateTree(updates) } else { - // fetch all siacoin and siafund state elements - siacoinStateElements, err := tx.SiacoinStateElements() - if err != nil { - return fmt.Errorf("failed to get siacoin state elements: %w", err) - } - - // update siacoin element proofs - for i := range siacoinStateElements { - update.UpdateElementProof(&siacoinStateElements[i]) - } - - if err := tx.UpdateSiacoinStateElements(siacoinStateElements); err != nil { - return fmt.Errorf("failed to update siacoin state elements: %w", err) - } - - siafundStateElements, err := tx.SiafundStateElements() - if err != nil { - return fmt.Errorf("failed to get siafund state elements: %w", err) - } - - // update siafund element proofs - for i := range siafundStateElements { - update.UpdateElementProof(&siafundStateElements[i]) - } - return tx.UpdateSiafundStateElements(siafundStateElements) + return tx.UpdateStateElementProofs(update) } } diff --git a/wallet/wallet.go b/wallet/wallet.go index c968e56..93bc107 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -174,11 +174,11 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f sces := make(map[types.SiacoinOutputID]types.SiacoinElement) sfes := make(map[types.SiafundOutputID]types.SiafundElement) cu.ForEachSiacoinElement(func(sce types.SiacoinElement, _, _ bool) { - sce.MerkleProof = nil + sce.StateElement.MerkleProof = nil sces[types.SiacoinOutputID(sce.ID)] = sce }) cu.ForEachSiafundElement(func(sfe types.SiafundElement, _, _ bool) { - sfe.MerkleProof = nil + sfe.StateElement.MerkleProof = nil sfes[types.SiafundOutputID(sfe.ID)] = sfe }) @@ -221,7 +221,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f sce, ok := sces[sfi.ParentID.ClaimOutputID()] if ok && relevant(sce.SiacoinOutput.Address) { - addEvent(sce.ID, sce.MaturityHeight, EventTypeSiafundClaim, wallet.EventPayout{ + addEvent(types.Hash256(sce.ID), sce.MaturityHeight, EventTypeSiafundClaim, wallet.EventPayout{ SiacoinElement: sce, }, []types.Address{sfi.ClaimAddress}) } @@ -268,7 +268,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f sce, ok := sces[types.SiafundOutputID(sfi.Parent.ID).V2ClaimOutputID()] if ok && relevant(sfi.ClaimAddress) { - addEvent(sce.ID, sce.MaturityHeight, EventTypeSiafundClaim, wallet.EventPayout{ + addEvent(types.Hash256(sce.ID), sce.MaturityHeight, EventTypeSiafundClaim, wallet.EventPayout{ SiacoinElement: sce, }, []types.Address{sfi.ClaimAddress}) } @@ -299,7 +299,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f return } - fce.MerkleProof = nil + fce.StateElement.MerkleProof = nil if valid { for i := range fce.FileContract.ValidProofOutputs { @@ -309,7 +309,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f } element := sces[types.FileContractID(fce.ID).ValidOutputID(i)] - addEvent(element.ID, element.MaturityHeight, EventTypeV1ContractResolution, wallet.EventV1ContractResolution{ + addEvent(types.Hash256(element.ID), element.MaturityHeight, EventTypeV1ContractResolution, wallet.EventV1ContractResolution{ Parent: fce, SiacoinElement: element, Missed: false, @@ -323,7 +323,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f } element := sces[types.FileContractID(fce.ID).MissedOutputID(i)] - addEvent(element.ID, element.MaturityHeight, EventTypeV1ContractResolution, wallet.EventV1ContractResolution{ + addEvent(types.Hash256(element.ID), element.MaturityHeight, EventTypeV1ContractResolution, wallet.EventV1ContractResolution{ Parent: fce, SiacoinElement: element, Missed: true, @@ -337,7 +337,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f return } - fce.MerkleProof = nil + fce.StateElement.MerkleProof = nil var missed bool if _, ok := res.(*types.V2FileContractExpiration); ok { @@ -346,7 +346,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f if relevant(fce.V2FileContract.HostOutput.Address) { element := sces[types.FileContractID(fce.ID).V2HostOutputID()] - addEvent(element.ID, element.MaturityHeight, EventTypeV2ContractResolution, wallet.EventV2ContractResolution{ + addEvent(types.Hash256(element.ID), element.MaturityHeight, EventTypeV2ContractResolution, wallet.EventV2ContractResolution{ Resolution: types.V2FileContractResolution{ Parent: fce, Resolution: res, @@ -358,7 +358,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f if relevant(fce.V2FileContract.RenterOutput.Address) { element := sces[types.FileContractID(fce.ID).V2RenterOutputID()] - addEvent(element.ID, element.MaturityHeight, EventTypeV2ContractResolution, wallet.EventV2ContractResolution{ + addEvent(types.Hash256(element.ID), element.MaturityHeight, EventTypeV2ContractResolution, wallet.EventV2ContractResolution{ Resolution: types.V2FileContractResolution{ Parent: fce, Resolution: res, @@ -373,7 +373,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f for i := range b.MinerPayouts { if relevant(b.MinerPayouts[i].Address) { element := sces[cs.Index.ID.MinerOutputID(i)] - addEvent(element.ID, element.MaturityHeight, EventTypeMinerPayout, wallet.EventPayout{ + addEvent(types.Hash256(element.ID), element.MaturityHeight, EventTypeMinerPayout, wallet.EventPayout{ SiacoinElement: element, }, []types.Address{b.MinerPayouts[i].Address}) } @@ -383,7 +383,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate, relevant f if relevant(cs.FoundationPrimaryAddress) { element, ok := sces[cs.Index.ID.FoundationOutputID()] if ok { - addEvent(element.ID, element.MaturityHeight, EventTypeFoundationSubsidy, wallet.EventPayout{ + addEvent(types.Hash256(element.ID), element.MaturityHeight, EventTypeFoundationSubsidy, wallet.EventPayout{ SiacoinElement: element, }, []types.Address{element.SiacoinOutput.Address}) } diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 4fd7d5e..dab8c1f 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -409,13 +409,13 @@ func TestEphemeralBalance(t *testing.T) { }, Signatures: []types.TransactionSignature{ { - ParentID: utxos[0].ID, + ParentID: types.Hash256(utxos[0].ID), PublicKeyIndex: 0, CoveredFields: types.CoveredFields{WholeTransaction: true}, }, }, } - parentSigHash := cm.TipState().WholeSigHash(parentTxn, utxos[0].ID, 0, 0, nil) + parentSigHash := cm.TipState().WholeSigHash(parentTxn, types.Hash256(utxos[0].ID), 0, 0, nil) parentSig := pk.SignHash(parentSigHash) parentTxn.Signatures[0].Signature = parentSig[:] @@ -1071,13 +1071,13 @@ func TestOrphans(t *testing.T) { }, Signatures: []types.TransactionSignature{ { - ParentID: utxos[0].ID, + ParentID: types.Hash256(utxos[0].ID), PublicKeyIndex: 0, CoveredFields: types.CoveredFields{WholeTransaction: true}, }, }, } - sigHash := cm.TipState().WholeSigHash(txn, utxos[0].ID, 0, 0, nil) + sigHash := cm.TipState().WholeSigHash(txn, types.Hash256(utxos[0].ID), 0, 0, nil) sig := pk.SignHash(sigHash) txn.Signatures[0].Signature = sig[:] @@ -1727,13 +1727,13 @@ func TestWalletUnconfirmedEvents(t *testing.T) { }, Signatures: []types.TransactionSignature{ { - ParentID: utxos[0].ID, + ParentID: types.Hash256(utxos[0].ID), PublicKeyIndex: 0, CoveredFields: types.CoveredFields{WholeTransaction: true}, }, }, } - sigHash := cm.TipState().WholeSigHash(txn, utxos[0].ID, 0, 0, nil) + sigHash := cm.TipState().WholeSigHash(txn, types.Hash256(utxos[0].ID), 0, 0, nil) sig := pk.SignHash(sigHash) txn.Signatures[0].Signature = sig[:] @@ -1824,7 +1824,7 @@ func TestWalletUnconfirmedEvents(t *testing.T) { } txnData = events[1].Data.(wallet.EventV1Transaction) - if txnData.SpentSiacoinElements[0].ID != types.Hash256(ephemeralOutputID) { + if txnData.SpentSiacoinElements[0].ID != ephemeralOutputID { t.Fatalf("expected siacoin output %v, got %v", ephemeralOutputID, txnData.SpentSiacoinElements[0].ID) } else if txnData.SpentSiacoinElements[0].SiacoinOutput.Value != txn.SiacoinOutputs[0].Value { t.Fatalf("expected siacoin value %v, got %v", utxos[0].SiacoinOutput.Value, txnData.SpentSiacoinElements[0].SiacoinOutput.Value) @@ -1935,13 +1935,13 @@ func TestAddressUnconfirmedEvents(t *testing.T) { }, Signatures: []types.TransactionSignature{ { - ParentID: utxos[0].ID, + ParentID: types.Hash256(utxos[0].ID), PublicKeyIndex: 0, CoveredFields: types.CoveredFields{WholeTransaction: true}, }, }, } - sigHash := cm.TipState().WholeSigHash(txn, utxos[0].ID, 0, 0, nil) + sigHash := cm.TipState().WholeSigHash(txn, types.Hash256(utxos[0].ID), 0, 0, nil) sig := pk.SignHash(sigHash) txn.Signatures[0].Signature = sig[:] @@ -2041,7 +2041,7 @@ func TestAddressUnconfirmedEvents(t *testing.T) { } txnData = events[1].Data.(wallet.EventV1Transaction) - if txnData.SpentSiacoinElements[0].ID != types.Hash256(ephemeralOutputID) { + if txnData.SpentSiacoinElements[0].ID != ephemeralOutputID { t.Fatalf("expected siacoin output %v, got %v", ephemeralOutputID, txnData.SpentSiacoinElements[0].ID) } else if txnData.SpentSiacoinElements[0].SiacoinOutput.Value != txn.SiacoinOutputs[0].Value { t.Fatalf("expected siacoin value %v, got %v", utxos[0].SiacoinOutput.Value, txnData.SpentSiacoinElements[0].SiacoinOutput.Value) @@ -3043,7 +3043,7 @@ func TestEventTypes(t *testing.T) { }, Signatures: []types.TransactionSignature{ { - ParentID: sce[0].ID, + ParentID: types.Hash256(sce[0].ID), PublicKeyIndex: 0, Timelock: 0, CoveredFields: types.CoveredFields{WholeTransaction: true}, @@ -3052,7 +3052,7 @@ func TestEventTypes(t *testing.T) { } // sign the transaction - sigHash := cm.TipState().WholeSigHash(txn, sce[0].ID, 0, 0, nil) + sigHash := cm.TipState().WholeSigHash(txn, types.Hash256(sce[0].ID), 0, 0, nil) sig := pk.SignHash(sigHash) txn.Signatures[0].Signature = sig[:] @@ -3097,14 +3097,14 @@ func TestEventTypes(t *testing.T) { FileContracts: []types.FileContract{fc}, Signatures: []types.TransactionSignature{ { - ParentID: sce[0].ID, + ParentID: types.Hash256(sce[0].ID), PublicKeyIndex: 0, Timelock: 0, CoveredFields: types.CoveredFields{WholeTransaction: true}, }, }, } - sigHash := cm.TipState().WholeSigHash(txn, sce[0].ID, 0, 0, nil) + sigHash := cm.TipState().WholeSigHash(txn, types.Hash256(sce[0].ID), 0, 0, nil) sig := pk.SignHash(sigHash) txn.Signatures[0].Signature = sig[:]