diff --git a/explorer/types.go b/explorer/types.go index ec57004e..4f3c4917 100644 --- a/explorer/types.go +++ b/explorer/types.go @@ -18,6 +18,12 @@ const ( SourceMinerPayout // SourceTransaction means the source of the output is a transaction. SourceTransaction + // SourceValidProofOutput me ans the source of the output is a valid proof + // output. + SourceValidProofOutput + // SourceMissedProofOutput me ans the source of the output is a missed + // proof output. + SourceMissedProofOutput ) // MarshalJSON implements json.Marshaler. @@ -48,6 +54,10 @@ type SiafundOutput types.SiafundElement // internally. type FileContract struct { types.StateElement + + Resolved bool `json:"resolved"` + Valid bool `json:"valid"` + Filesize uint64 `json:"filesize"` FileMerkleRoot types.Hash256 `json:"fileMerkleRoot"` WindowStart uint64 `json:"windowStart"` @@ -80,7 +90,7 @@ type Transaction struct { SiafundInputs []types.SiafundInput `json:"siafundInputs,omitempty"` SiafundOutputs []SiafundOutput `json:"siafundOutputs,omitempty"` FileContracts []FileContract `json:"fileContracts,omitempty"` - FileContractRevisions []FileContractRevision `json:"fileContracts,omitempty"` + FileContractRevisions []FileContractRevision `json:"fileContractRevisions,omitempty"` ArbitraryData [][]byte `json:"arbitraryData,omitempty"` } diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 8b13c680..f2db2230 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -121,22 +121,56 @@ func (s *Store) addSiafundOutputs(dbTxn txn, id int64, txn types.Transaction, db return nil } -func (s *Store) addFileContracts(dbTxn txn, id int64, txn types.Transaction, dbIDs map[fileContract]int64) error { +func (s *Store) addFileContracts(dbTxn txn, id int64, txn types.Transaction, scDBIds map[types.SiacoinOutputID]int64, fcDBIds map[fileContract]int64) error { stmt, err := dbTxn.Prepare(`INSERT INTO transaction_file_contracts(transaction_id, transaction_order, contract_id) VALUES (?, ?, ?)`) if err != nil { return fmt.Errorf("addFileContracts: failed to prepare statement: %w", err) } defer stmt.Close() + // validOutputsStmt, err := dbTxn.Prepare(`INSERT INTO file_contract_valid_proof_outputs(contract_id, contract_order, output_id) VALUES (?, ?, ?)`) + // if err != nil { + // return fmt.Errorf("addFileContracts: failed to prepare valid proof outputs statement: %w", err) + // } + // defer validOutputsStmt.Close() + + // missedOutputsStmt, err := dbTxn.Prepare(`INSERT INTO file_contract_missed_proof_outputs(contract_id, contract_order, output_id) VALUES (?, ?, ?)`) + // if err != nil { + // return fmt.Errorf("addFileContracts: failed to prepare missed proof outputs statement: %w", err) + // } + // defer missedOutputsStmt.Close() + for i := range txn.FileContracts { - dbID, ok := dbIDs[fileContract{txn.FileContractID(i), 0}] + dbID, ok := fcDBIds[fileContract{txn.FileContractID(i), 0}] if !ok { - return errors.New("addFileContracts: dbID not in map") + return errors.New("addFileContracts: fcDbID not in map") } if _, err := stmt.Exec(id, i, dbID); err != nil { - return fmt.Errorf("addFileContracts: failed to execute statement: %w", err) + return fmt.Errorf("addFileContracts: failed to execute transaction_file_contracts statement: %w", err) } + + // for j := range txn.FileContracts[i].ValidProofOutputs { + // scDBId, ok := scDBIds[txn.FileContractID(i).ValidOutputID(j)] + // if !ok { + // return errors.New("addFileContracts: valid scDBId not in map") + // } + + // if _, err := validOutputsStmt.Exec(dbID, j, scDBId); err != nil { + // return fmt.Errorf("addFileContracts: failed to execute valid proof outputs statement: %w", err) + // } + // } + + // for j := range txn.FileContracts[i].MissedProofOutputs { + // scDBId, ok := scDBIds[txn.FileContractID(i).MissedOutputID(j)] + // if !ok { + // return errors.New("addFileContracts: missed scDBId not in map") + // } + + // if _, err := missedOutputsStmt.Exec(dbID, j, scDBId); err != nil { + // return fmt.Errorf("addFileContracts: failed to execute missed proof outputs statement: %w", err) + // } + // } } return nil } @@ -196,10 +230,10 @@ func (s *Store) addTransactions(dbTxn txn, bid types.BlockID, txns []types.Trans return fmt.Errorf("failed to add siafund inputs: %w", err) } else if err := s.addSiafundOutputs(dbTxn, txnID, txn, sfDBIds); err != nil { return fmt.Errorf("failed to add siafund outputs: %w", err) - } else if err := s.addFileContracts(dbTxn, txnID, txn, fcDBIds); err != nil { + } else if err := s.addFileContracts(dbTxn, txnID, txn, scDBIds, fcDBIds); err != nil { return fmt.Errorf("failed to add file contract: %w", err) } else if err := s.addFileContractRevisions(dbTxn, txnID, txn, fcDBIds); err != nil { - return fmt.Errorf("failed to add file contract: %w", err) + return fmt.Errorf("failed to add file contract revisions: %w", err) } } return nil @@ -316,17 +350,27 @@ func (s *Store) addSiacoinElements(dbTxn txn, bid types.BlockID, update consensu for i := range block.MinerPayouts { sources[bid.MinerOutputID(i)] = explorer.SourceMinerPayout } + for _, txn := range block.Transactions { for i := range txn.SiacoinOutputs { sources[txn.SiacoinOutputID(i)] = explorer.SourceTransaction } - // TODO: contract valid/missed outputs + + for i := range txn.FileContracts { + fcid := txn.FileContractID(i) + for j := range txn.FileContracts[i].ValidProofOutputs { + sources[fcid.ValidOutputID(j)] = explorer.SourceValidProofOutput + } + for j := range txn.FileContracts[i].MissedProofOutputs { + sources[fcid.MissedOutputID(j)] = explorer.SourceMissedProofOutput + } + } } } 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(output_id) + ON CONFLICT DO UPDATE SET spent = ?`) if err != nil { return nil, fmt.Errorf("addSiacoinElements: failed to prepare siacoin_elements statement: %w", err) @@ -360,7 +404,7 @@ func (s *Store) addSiacoinElements(dbTxn txn, bid types.BlockID, update consensu func (s *Store) addSiafundElements(dbTxn txn, bid types.BlockID, update consensusUpdate) (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(output_id) + ON CONFLICT DO UPDATE SET spent = ?`) if err != nil { return nil, fmt.Errorf("addSiafundElements: failed to prepare siafund_elements statement: %w", err) @@ -397,8 +441,8 @@ type fileContract struct { } func (s *Store) addFileContractElements(dbTxn txn, bid types.BlockID, update consensusUpdate) (map[fileContract]int64, error) { - stmt, err := dbTxn.Prepare(`INSERT INTO file_contract_elements(block_id, contract_id, leaf_index, merkle_proof, resolved, valid, filesize, file_merkle_root, window_start, window_end, valid_proof_outputs, missed_proof_outputs, payout, unlock_hash, revision_number) - VALUES (?, ?, ?, ?, FALSE, TRUE, ?, ?, ?, ?, ?, ?, ?, ?, ?) + stmt, err := dbTxn.Prepare(`INSERT INTO file_contract_elements(block_id, contract_id, leaf_index, merkle_proof, resolved, valid, filesize, file_merkle_root, window_start, window_end, payout, unlock_hash, revision_number) + VALUES (?, ?, ?, ?, FALSE, TRUE, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (contract_id, revision_number) DO UPDATE SET resolved = ? AND valid = ?`) if err != nil { @@ -418,7 +462,7 @@ func (s *Store) addFileContractElements(dbTxn txn, bid types.BlockID, update con fc = &rev.FileContract } - result, err := stmt.Exec(dbEncode(bid), dbEncode(fce.StateElement.ID), dbEncode(fce.StateElement.LeafIndex), dbEncode(fce.StateElement.MerkleProof), fc.Filesize, dbEncode(fc.FileMerkleRoot), fc.WindowStart, fc.WindowEnd, len(fc.ValidProofOutputs), len(fc.MissedProofOutputs), dbEncode(fc.Payout), dbEncode(fc.UnlockHash), fc.RevisionNumber, resolved, valid) + result, err := stmt.Exec(dbEncode(bid), dbEncode(fce.StateElement.ID), dbEncode(fce.StateElement.LeafIndex), dbEncode(fce.StateElement.MerkleProof), fc.Filesize, dbEncode(fc.FileMerkleRoot), fc.WindowStart, fc.WindowEnd, dbEncode(fc.Payout), dbEncode(fc.UnlockHash), fc.RevisionNumber, resolved, valid) if err != nil { updateErr = fmt.Errorf("addFileContractElements: failed to execute file_contract_elements statement: %w", err) return diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index 96229c67..88ab9dfe 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -69,14 +69,30 @@ CREATE TABLE file_contract_elements ( file_merkle_root BLOB NOT NULL, window_start INTEGER NOT NULL, window_end INTEGER NOT NULL, - valid_proof_outputs INTEGER NOT NULL, - missed_proof_outputs INTEGER NOT NULL, payout BLOB NOT NULL, unlock_hash BLOB NOT NULL, revision_number INTEGER NOT NULL, UNIQUE(contract_id, revision_number) ); +CREATE TABLE file_contract_valid_proof_outputs ( + contract_id INTEGER REFERENCES file_contract_elements(id) ON DELETE CASCADE NOT NULL, + contract_order INTEGER NOT NULL, + output_id INTEGER REFERENCES siacoin_elements(id) ON DELETE CASCADE NOT NULL, + UNIQUE(contract_id, contract_order) +); + +CREATE INDEX file_contract_valid_proof_outputs_contract_id_index ON file_contract_valid_proof_outputs(contract_id); + +CREATE TABLE file_contract_missed_proof_outputs ( + contract_id INTEGER REFERENCES file_contract_elements(id) ON DELETE CASCADE NOT NULL, + contract_order INTEGER NOT NULL, + output_id INTEGER REFERENCES siacoin_elements(id) ON DELETE CASCADE NOT NULL, + UNIQUE(contract_id, contract_order) +); + +CREATE INDEX file_contract_missed_proof_outputs_contract_id_index ON file_contract_missed_proof_outputs(contract_id); + CREATE TABLE miner_payouts ( block_id BLOB REFERENCES blocks(id) ON DELETE CASCADE NOT NULL, block_order INTEGER NOT NULL, diff --git a/persist/sqlite/transactions.go b/persist/sqlite/transactions.go index 66dac92d..18975e46 100644 --- a/persist/sqlite/transactions.go +++ b/persist/sqlite/transactions.go @@ -139,6 +139,58 @@ ORDER BY ts.transaction_order DESC` return result, nil } +// transactionFileContracts returns the file contracts for each transaction. +func transactionFileContracts(tx txn, txnIDs []int64) (map[int64][]explorer.FileContract, error) { + query := `SELECT ts.transaction_id, fc.contract_id, fc.leaf_index, fc.merkle_proof, fc.resolved, fc.valid, fc.filesize, fc.file_merkle_root, fc.window_start, fc.window_end, fc.payout, fc.unlock_hash, fc.revision_number +FROM file_contract_elements fc +INNER JOIN transaction_file_contracts ts ON (ts.contract_id = fc.id) +WHERE ts.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) +ORDER BY ts.transaction_order DESC` + rows, err := tx.Query(query, queryArgs(txnIDs)...) + if err != nil { + return nil, fmt.Errorf("failed to query contract output ids: %w", err) + } + defer rows.Close() + + // map transaction ID to contract list + result := make(map[int64][]explorer.FileContract) + for rows.Next() { + var txnID int64 + var fc explorer.FileContract + if err := rows.Scan(&txnID, dbDecode(&fc.StateElement.ID), dbDecode(&fc.StateElement.LeafIndex), dbDecode(&fc.StateElement.MerkleProof), &fc.Resolved, &fc.Valid, &fc.Filesize, dbDecode(&fc.FileMerkleRoot), &fc.WindowStart, &fc.WindowEnd, dbDecode(&fc.Payout), dbDecode(&fc.UnlockHash), &fc.RevisionNumber); err != nil { + return nil, fmt.Errorf("failed to scan file contract: %w", err) + } + result[txnID] = append(result[txnID], fc) + } + return result, nil +} + +// transactionFileContracts returns the file contract revisions for each transaction. +func transactionFileContractRevisions(tx txn, txnIDs []int64) (map[int64][]explorer.FileContractRevision, error) { + query := `SELECT ts.transaction_id, ts.parent_id, ts.unlock_conditions, fc.contract_id, fc.leaf_index, fc.merkle_proof, fc.resolved, fc.valid, fc.filesize, fc.file_merkle_root, fc.window_start, fc.window_end, fc.payout, fc.unlock_hash, fc.revision_number +FROM file_contract_elements fc +INNER JOIN transaction_file_contracts ts ON (ts.contract_id = fc.id) +WHERE ts.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) +ORDER BY ts.transaction_order DESC` + rows, err := tx.Query(query, queryArgs(txnIDs)...) + if err != nil { + return nil, fmt.Errorf("failed to query contract output ids: %w", err) + } + defer rows.Close() + + // map transaction ID to contract list + result := make(map[int64][]explorer.FileContractRevision) + for rows.Next() { + var txnID int64 + var fc explorer.FileContractRevision + if err := rows.Scan(&txnID, dbDecode(&fc.ParentID), dbDecode(&fc.UnlockConditions), dbDecode(&fc.StateElement.ID), dbDecode(&fc.StateElement.LeafIndex), dbDecode(&fc.StateElement.MerkleProof), &fc.Resolved, &fc.Valid, &fc.Filesize, dbDecode(&fc.FileMerkleRoot), &fc.WindowStart, &fc.WindowEnd, dbDecode(&fc.Payout), dbDecode(&fc.UnlockHash), &fc.RevisionNumber); err != nil { + return nil, fmt.Errorf("failed to scan file contract: %w", err) + } + result[txnID] = append(result[txnID], fc) + } + 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) { @@ -235,19 +287,29 @@ func (s *Store) getTransactions(tx txn, dbIDs []int64) ([]explorer.Transaction, return nil, fmt.Errorf("getTransactions: failed to get siafund outputs: %w", err) } - // TODO: file contracts - // TODO: file contract revisions + txnFileContracts, err := transactionFileContracts(tx, dbIDs) + if err != nil { + return nil, fmt.Errorf("getTransactions: failed to get file contracts: %w", err) + } + + txnFileContractRevisions, err := transactionFileContractRevisions(tx, dbIDs) + if err != nil { + return nil, fmt.Errorf("getTransactions: failed to get file contract revisions: %w", err) + } + // TODO: storage proofs // TODO: signatures var results []explorer.Transaction for _, dbID := range dbIDs { txn := explorer.Transaction{ - ArbitraryData: txnArbitraryData[dbID], - SiacoinInputs: txnSiacoinInputs[dbID], - SiacoinOutputs: txnSiacoinOutputs[dbID], - SiafundInputs: txnSiafundInputs[dbID], - SiafundOutputs: txnSiafundOutputs[dbID], + ArbitraryData: txnArbitraryData[dbID], + SiacoinInputs: txnSiacoinInputs[dbID], + SiacoinOutputs: txnSiacoinOutputs[dbID], + SiafundInputs: txnSiafundInputs[dbID], + SiafundOutputs: txnSiafundOutputs[dbID], + FileContracts: txnFileContracts[dbID], + FileContractRevisions: txnFileContractRevisions[dbID], } results = append(results, txn) }