diff --git a/explorer/types.go b/explorer/types.go index 7db70b20..5d240345 100644 --- a/explorer/types.go +++ b/explorer/types.go @@ -91,6 +91,7 @@ type Transaction struct { SiafundOutputs []SiafundOutput `json:"siafundOutputs,omitempty"` FileContracts []FileContract `json:"fileContracts,omitempty"` FileContractRevisions []FileContractRevision `json:"fileContractRevisions,omitempty"` + StorageProofs []types.StorageProof `json:"storageProofs,omitempty"` MinerFees []types.Currency `json:"minerFees,omitempty"` ArbitraryData [][]byte `json:"arbitraryData,omitempty"` Signatures []types.TransactionSignature `json:"signatures,omitempty"` diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 2e5dfb28..43136ce7 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -246,6 +246,21 @@ func addFileContractRevisions(tx *txn, id int64, txn types.Transaction, dbIDs ma return nil } +func addStorageProofs(tx *txn, id int64, txn types.Transaction) error { + stmt, err := tx.Prepare(`INSERT INTO transaction_storage_proofs(transaction_id, transaction_order, parent_id, leaf, proof) VALUES (?, ?, ?, ?, ?)`) + if err != nil { + return fmt.Errorf("addStorageProofs: failed to prepare statement: %w", err) + } + defer stmt.Close() + + for i, proof := range txn.StorageProofs { + if _, err := stmt.Exec(id, i, encode(proof.ParentID), proof.Leaf[:], encode(proof.Proof)); err != nil { + return fmt.Errorf("addStorageProofs: failed to execute statement: %w", err) + } + } + return nil +} + func addTransactions(tx *txn, bid types.BlockID, txns []types.Transaction, scDBIds map[types.SiacoinOutputID]int64, sfDBIds map[types.SiafundOutputID]int64, fcDBIds map[explorer.DBFileContract]int64) error { checkTransactionStmt, err := tx.Prepare(`SELECT id FROM transactions WHERE transaction_id = ?`) if err != nil { @@ -310,6 +325,8 @@ func addTransactions(tx *txn, bid types.BlockID, txns []types.Transaction, scDBI return fmt.Errorf("failed to add file contract: %w", err) } else if err := addFileContractRevisions(tx, txnID, txn, fcDBIds); err != nil { return fmt.Errorf("failed to add file contract revisions: %w", err) + } else if err := addStorageProofs(tx, txnID, txn); err != nil { + return fmt.Errorf("failed to add storage proofs: %w", err) } } return nil diff --git a/persist/sqlite/encoding.go b/persist/sqlite/encoding.go index 7fe4d0e0..2588004d 100644 --- a/persist/sqlite/encoding.go +++ b/persist/sqlite/encoding.go @@ -25,6 +25,15 @@ func encode(obj any) any { obj.EncodeTo(e) e.Flush() return buf.Bytes() + case []types.Hash256: + var buf bytes.Buffer + e := types.NewEncoder(&buf) + e.WritePrefix(len(obj)) + for _, o := range obj { + o.EncodeTo(e) + } + e.Flush() + return buf.Bytes() case uint64: b := make([]byte, 8) binary.BigEndian.PutUint64(b, obj) @@ -58,7 +67,12 @@ func (d *decodable) Scan(src any) error { case types.DecoderFrom: dec := types.NewBufDecoder(src) v.DecodeFrom(dec) - return dec.Err() + case *[]types.Hash256: + dec := types.NewBufDecoder(src) + *v = make([]types.Hash256, dec.ReadPrefix()) + for i := range *v { + (*v)[i].DecodeFrom(dec) + } case *uint64: *v = binary.BigEndian.Uint64(src) default: diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index 9dc4071f..0afc52ac 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -156,6 +156,17 @@ CREATE TABLE transaction_signatures ( CREATE INDEX transaction_signatures_transaction_id_index ON transaction_signatures(transaction_id); +CREATE TABLE transaction_storage_proofs ( + transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE NOT NULL, + transaction_order INTEGER NOT NULL, + parent_id BLOB REFERENCES last_contract_revision(contract_id) ON DELETE CASCADE NOT NULL, + leaf BLOB NOT NULL, + proof BLOB NOT NULL, + UNIQUE(transaction_id, transaction_order) +); + +CREATE INDEX transaction_storage_proofs_transaction_id_index ON transaction_storage_proofs(transaction_id); + CREATE TABLE transaction_siacoin_inputs ( transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE NOT NULL, transaction_order INTEGER NOT NULL, diff --git a/persist/sqlite/transactions.go b/persist/sqlite/transactions.go index 5d79b9f5..5cdbc92d 100644 --- a/persist/sqlite/transactions.go +++ b/persist/sqlite/transactions.go @@ -325,6 +325,30 @@ ORDER BY ts.transaction_order ASC` return result, nil } +// transactionStorageProofs returns the storage proofs for each transaction. +func transactionStorageProofs(tx *txn, txnIDs []int64) (map[int64][]types.StorageProof, error) { + query := `SELECT transaction_id, parent_id, leaf, proof +FROM transaction_storage_proofs +WHERE transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) +ORDER BY transaction_order ASC` + rows, err := tx.Query(query, queryArgs(txnIDs)...) + if err != nil { + return nil, err + } + defer rows.Close() + + result := make(map[int64][]types.StorageProof) + for rows.Next() { + var txnID int64 + var proof types.StorageProof + if err := rows.Scan(&txnID, decode(&proof.ParentID), &proof.Leaf, decode(&proof.Proof)); err != nil { + return nil, fmt.Errorf("failed to scan arbitrary data: %w", err) + } + result[txnID] = append(result[txnID], proof) + } + return result, nil +} + // blockTransactionIDs returns the database ID for each transaction in the // block. func blockTransactionIDs(tx *txn, blockID types.BlockID) (dbIDs []int64, err error) { @@ -441,8 +465,10 @@ func (s *Store) getTransactions(tx *txn, dbIDs []int64) ([]explorer.Transaction, return nil, fmt.Errorf("getTransactions: failed to get file contract revisions: %w", err) } - // TODO: storage proofs - // TODO: signatures + txnStorageProofs, err := transactionStorageProofs(tx, dbIDs) + if err != nil { + return nil, fmt.Errorf("getTransactions: failed to get storage proofs: %w", err) + } var results []explorer.Transaction for _, dbID := range dbIDs { @@ -453,6 +479,7 @@ func (s *Store) getTransactions(tx *txn, dbIDs []int64) ([]explorer.Transaction, SiafundOutputs: txnSiafundOutputs[dbID], FileContracts: txnFileContracts[dbID], FileContractRevisions: txnFileContractRevisions[dbID], + StorageProofs: txnStorageProofs[dbID], MinerFees: txnMinerFees[dbID], ArbitraryData: txnArbitraryData[dbID], Signatures: txnSignatures[dbID],