diff --git a/explorer/types.go b/explorer/types.go index 0728025e..dc21d6db 100644 --- a/explorer/types.go +++ b/explorer/types.go @@ -181,7 +181,14 @@ type V2FileContract struct { types.V2FileContractElement } -// A V2Transaction is a v2 transaction that uses the wrapped types above. +// A V2FileContractRevision is a V2 file contract revision with the +// explorer V2FileContract type. +type V2FileContractRevision struct { + Parent V2FileContract `json:"parent"` + Revision V2FileContract `json:"revision"` +} + +// A V2Transaction is a V2 transaction that uses the wrapped types above. type V2Transaction struct { ID types.TransactionID `json:"id"` @@ -190,7 +197,8 @@ type V2Transaction struct { SiafundInputs []types.V2SiafundInput `json:"siafundInputs,omitempty"` SiafundOutputs []SiafundOutput `json:"siafundOutputs,omitempty"` - FileContracts []V2FileContract `json:"fileContracts,omitempty"` + FileContracts []V2FileContract `json:"fileContracts,omitempty"` + FileContractRevisions []V2FileContractRevision `json:"fileContractRevisions,omitempty"` Attestations []types.Attestation `json:"attestations,omitempty"` ArbitraryData []byte `json:"arbitraryData,omitempty"` diff --git a/internal/testutil/check.go b/internal/testutil/check.go index a64b1a39..5798c3f4 100644 --- a/internal/testutil/check.go +++ b/internal/testutil/check.go @@ -170,6 +170,17 @@ func CheckV2Transaction(t *testing.T, expectTxn types.V2Transaction, gotTxn expl CheckV2FC(t, expected, got) } + Equal(t, "file contract revision", len(expectTxn.FileContractRevisions), len(gotTxn.FileContractRevisions)) + for i := range expectTxn.FileContractRevisions { + expected := expectTxn.FileContractRevisions[i] + got := gotTxn.FileContractRevisions[i] + + Equal(t, "parent ID", expected.Parent.ID, got.Parent.ID) + Equal(t, "revision ID", expected.Parent.ID, got.Revision.ID) + CheckV2FC(t, expected.Parent.V2FileContract, got.Parent) + CheckV2FC(t, expected.Revision, got.Revision) + } + Equal(t, "attestations", len(expectTxn.Attestations), len(gotTxn.Attestations)) for i := range expectTxn.Attestations { expected := expectTxn.Attestations[i] diff --git a/persist/sqlite/addresses.go b/persist/sqlite/addresses.go index 5358469d..105b86cd 100644 --- a/persist/sqlite/addresses.go +++ b/persist/sqlite/addresses.go @@ -75,7 +75,7 @@ WHERE ev.event_id = ?`, eventID).Scan(decode(&m.SiacoinOutput.StateElement.ID), var m explorer.EventMinerPayout err = tx.QueryRow(`SELECT sc.output_id, sc.leaf_index, sc.maturity_height, sc.address, sc.value FROM siacoin_elements sc -INNER JOIN miner_payout_events ev ON (ev.output_id = sc.id) +INNER JOIN miner_payout_events ev ON ev.output_id = sc.id WHERE ev.event_id = ?`, eventID).Scan(decode(&m.SiacoinOutput.StateElement.ID), decode(&m.SiacoinOutput.StateElement.LeafIndex), decode(&m.SiacoinOutput.MaturityHeight), decode(&m.SiacoinOutput.SiacoinOutput.Address), decode(&m.SiacoinOutput.SiacoinOutput.Value)) if err != nil { return explorer.Event{}, 0, fmt.Errorf("failed to fetch miner payout event data: %w", err) @@ -85,7 +85,7 @@ WHERE ev.event_id = ?`, eventID).Scan(decode(&m.SiacoinOutput.StateElement.ID), var m explorer.EventFoundationSubsidy err = tx.QueryRow(`SELECT sc.output_id, sc.leaf_index, sc.maturity_height, sc.address, sc.value FROM siacoin_elements sc -INNER JOIN foundation_subsidy_events ev ON (ev.output_id = sc.id) +INNER JOIN foundation_subsidy_events ev ON ev.output_id = sc.id WHERE ev.event_id = ?`, eventID).Scan(decode(&m.SiacoinOutput.StateElement.ID), decode(&m.SiacoinOutput.StateElement.LeafIndex), decode(&m.SiacoinOutput.MaturityHeight), decode(&m.SiacoinOutput.SiacoinOutput.Address), decode(&m.SiacoinOutput.SiacoinOutput.Value)) ev.Data = &m default: @@ -153,8 +153,8 @@ func (s *Store) AddressEvents(address types.Address, offset, limit uint64) (even err = s.transaction(func(tx *txn) error { const query = `SELECT ev.id, ev.event_id, ev.maturity_height, ev.date_created, ev.height, ev.block_id, ev.event_type FROM events ev - INNER JOIN event_addresses ea ON (ev.id = ea.event_id) - INNER JOIN address_balance sa ON (ea.address_id = sa.id) + INNER JOIN event_addresses ea ON ev.id = ea.event_id + INNER JOIN address_balance sa ON ea.address_id = sa.id WHERE sa.address = $1 ORDER BY ev.maturity_height DESC, ev.id DESC LIMIT $2 OFFSET $3` diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 34649c04..5764de6f 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -928,6 +928,8 @@ func updateFileContractElements(tx *txn, revert bool, b types.Block, fces []expl } for _, txn := range b.Transactions { + // add in any contracts that are not the latest, i.e. contracts that + // were created and revised in the same block for j, fc := range txn.FileContracts { fcID := txn.FileContractID(j) dbFC := explorer.DBFileContract{ID: txn.FileContractID(j), RevisionNumber: fc.RevisionNumber} @@ -939,6 +941,8 @@ func updateFileContractElements(tx *txn, revert bool, b types.Block, fces []expl return nil, fmt.Errorf("updateFileContractElements: %w", err) } } + // add in any revisions that are not the latest, i.e. contracts that + // were revised multiple times in one block for _, fcr := range txn.FileContractRevisions { fc := fcr.FileContract dbFC := explorer.DBFileContract{ID: fcr.ParentID, RevisionNumber: fc.RevisionNumber} diff --git a/persist/sqlite/contracts.go b/persist/sqlite/contracts.go index 2a487c1e..ce5ad564 100644 --- a/persist/sqlite/contracts.go +++ b/persist/sqlite/contracts.go @@ -41,7 +41,7 @@ func (s *Store) Contracts(ids []types.FileContractID) (result []explorer.FileCon err = s.transaction(func(tx *txn) error { query := `SELECT fc1.id, fc1.contract_id, fc1.leaf_index, fc1.resolved, fc1.valid, fc1.transaction_id, rev.confirmation_index, rev.confirmation_transaction_id, rev.proof_index, rev.proof_transaction_id, fc1.filesize, fc1.file_merkle_root, fc1.window_start, fc1.window_end, fc1.payout, fc1.unlock_hash, fc1.revision_number FROM file_contract_elements fc1 - INNER JOIN last_contract_revision rev ON (rev.contract_element_id = fc1.id) + INNER JOIN last_contract_revision rev ON rev.contract_element_id = fc1.id WHERE rev.contract_id IN (` + queryPlaceHolders(len(ids)) + `)` rows, err := tx.Query(query, encodedIDs(ids)...) if err != nil { @@ -86,7 +86,7 @@ func (s *Store) ContractRevisions(id types.FileContractID) (revisions []explorer err = s.transaction(func(tx *txn) error { query := `SELECT fc.id, fc.contract_id, fc.leaf_index, fc.resolved, fc.valid, fc.transaction_id, rev.confirmation_index, rev.confirmation_transaction_id, rev.proof_index, rev.proof_transaction_id, 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 - JOIN last_contract_revision rev ON (rev.contract_id = fc.contract_id) + JOIN last_contract_revision rev ON rev.contract_id = fc.contract_id WHERE fc.contract_id = ? ORDER BY fc.revision_number ASC` rows, err := tx.Query(query, encode(id)) @@ -144,7 +144,7 @@ func (s *Store) ContractsKey(key types.PublicKey) (result []explorer.FileContrac err = s.transaction(func(tx *txn) error { query := `SELECT fc1.id, fc1.contract_id, fc1.leaf_index, fc1.resolved, fc1.valid, fc1.transaction_id, rev.confirmation_index, rev.confirmation_transaction_id, rev.proof_index, rev.proof_transaction_id, fc1.filesize, fc1.file_merkle_root, fc1.window_start, fc1.window_end, fc1.payout, fc1.unlock_hash, fc1.revision_number FROM file_contract_elements fc1 - INNER JOIN last_contract_revision rev ON (rev.contract_element_id = fc1.id) + INNER JOIN last_contract_revision rev ON rev.contract_element_id = fc1.id WHERE rev.ed25519_renter_key = ? OR rev.ed25519_host_key = ?` rows, err := tx.Query(query, encode(key), encode(key)) if err != nil { diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index 176af19a..183cdf02 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -314,7 +314,16 @@ CREATE TABLE v2_transaction_file_contracts ( contract_id INTEGER REFERENCES v2_file_contract_elements(id) ON DELETE CASCADE NOT NULL, -- add an index to all foreign keys UNIQUE(transaction_id, transaction_order) ); -CREATE INDEX v2_transaction_file_contracts_transaction_id_index ON v2_transaction_file_contracts(transaction_id); +CREATE INDEX v2_transaction_file_contracts_transaction_id_index ON v2_transaction_file_contracts(transaction_id); + +CREATE TABLE v2_transaction_file_contract_revisions ( + transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL, + transaction_order INTEGER NOT NULL, + parent_contract_id INTEGER REFERENCES v2_file_contract_elements(id) ON DELETE CASCADE NOT NULL, -- add an index to all foreign keys + revision_contract_id INTEGER REFERENCES v2_file_contract_elements(id) ON DELETE CASCADE NOT NULL, -- add an index to all foreign keys + UNIQUE(transaction_id, transaction_order) +); +CREATE INDEX v2_transaction_file_contract_revisions_transaction_id_index ON v2_transaction_file_contract_revisions(transaction_id); CREATE TABLE v2_transaction_attestations ( transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL, diff --git a/persist/sqlite/transactions.go b/persist/sqlite/transactions.go index d605cc30..fdee7bcb 100644 --- a/persist/sqlite/transactions.go +++ b/persist/sqlite/transactions.go @@ -14,8 +14,8 @@ import ( func (s *Store) TransactionChainIndices(txnID types.TransactionID, offset, limit uint64) (indices []types.ChainIndex, err error) { err = s.transaction(func(tx *txn) error { rows, err := tx.Query(`SELECT DISTINCT b.id, b.height FROM blocks b -INNER JOIN block_transactions bt ON (bt.block_id = b.id) -INNER JOIN transactions t ON (t.id = bt.transaction_id) +INNER JOIN block_transactions bt ON bt.block_id = b.id +INNER JOIN transactions t ON t.id = bt.transaction_id WHERE t.transaction_id = ? ORDER BY b.height DESC LIMIT ? OFFSET ?`, encode(txnID), limit, offset) @@ -112,7 +112,7 @@ ORDER BY transaction_order ASC` func transactionSiacoinOutputs(tx *txn, txnIDs []int64) (map[int64][]explorer.SiacoinOutput, error) { query := `SELECT ts.transaction_id, sc.output_id, sc.leaf_index, sc.spent_index, sc.source, sc.maturity_height, sc.address, sc.value FROM siacoin_elements sc -INNER JOIN transaction_siacoin_outputs ts ON (ts.output_id = sc.id) +INNER JOIN transaction_siacoin_outputs ts ON ts.output_id = sc.id WHERE ts.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) ORDER BY ts.transaction_order ASC` rows, err := tx.Query(query, queryArgs(txnIDs)...) @@ -142,7 +142,7 @@ ORDER BY ts.transaction_order ASC` func transactionSiacoinInputs(tx *txn, txnIDs []int64) (map[int64][]explorer.SiacoinInput, error) { query := `SELECT sc.id, ts.transaction_id, sc.output_id, ts.unlock_conditions, sc.value FROM siacoin_elements sc -INNER JOIN transaction_siacoin_inputs ts ON (ts.parent_id = sc.id) +INNER JOIN transaction_siacoin_inputs ts ON ts.parent_id = sc.id WHERE ts.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) ORDER BY ts.transaction_order ASC` rows, err := tx.Query(query, queryArgs(txnIDs)...) @@ -168,7 +168,7 @@ ORDER BY ts.transaction_order ASC` func transactionSiafundInputs(tx *txn, txnIDs []int64) (map[int64][]explorer.SiafundInput, error) { query := `SELECT ts.transaction_id, sf.output_id, ts.unlock_conditions, ts.claim_address, sf.value FROM siafund_elements sf -INNER JOIN transaction_siafund_inputs ts ON (ts.parent_id = sf.id) +INNER JOIN transaction_siafund_inputs ts ON ts.parent_id = sf.id WHERE ts.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) ORDER BY ts.transaction_order ASC` rows, err := tx.Query(query, queryArgs(txnIDs)...) @@ -195,7 +195,7 @@ ORDER BY ts.transaction_order ASC` func transactionSiafundOutputs(tx *txn, txnIDs []int64) (map[int64][]explorer.SiafundOutput, error) { query := `SELECT ts.transaction_id, sf.output_id, sf.leaf_index, sf.spent_index, sf.claim_start, sf.address, sf.value FROM siafund_elements sf -INNER JOIN transaction_siafund_outputs ts ON (ts.output_id = sf.id) +INNER JOIN transaction_siafund_outputs ts ON ts.output_id = sf.id WHERE ts.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) ORDER BY ts.transaction_order ASC` rows, err := tx.Query(query, queryArgs(txnIDs)...) @@ -286,8 +286,8 @@ type contractOrder struct { func transactionFileContracts(tx *txn, txnIDs []int64) (map[int64][]explorer.FileContract, error) { query := `SELECT ts.transaction_id, fc.id, rev.confirmation_index, rev.confirmation_transaction_id, rev.proof_index, rev.proof_transaction_id, fc.contract_id, fc.leaf_index, fc.resolved, fc.valid, fc.transaction_id, 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) -INNER JOIN last_contract_revision rev ON (rev.contract_id = fc.contract_id) +INNER JOIN transaction_file_contracts ts ON ts.contract_id = fc.id +INNER JOIN last_contract_revision rev ON rev.contract_id = fc.contract_id WHERE ts.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) ORDER BY ts.transaction_order ASC` rows, err := tx.Query(query, queryArgs(txnIDs)...) @@ -347,8 +347,8 @@ ORDER BY ts.transaction_order ASC` func transactionFileContractRevisions(tx *txn, txnIDs []int64) (map[int64][]explorer.FileContractRevision, error) { query := `SELECT ts.transaction_id, fc.id, rev.confirmation_index, rev.confirmation_transaction_id, rev.proof_index, rev.proof_transaction_id, ts.parent_id, ts.unlock_conditions, fc.contract_id, fc.leaf_index, fc.resolved, fc.valid, fc.transaction_id, 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_contract_revisions ts ON (ts.contract_id = fc.id) -INNER JOIN last_contract_revision rev ON (rev.contract_id = fc.contract_id) +INNER JOIN transaction_file_contract_revisions ts ON ts.contract_id = fc.id +INNER JOIN last_contract_revision rev ON rev.contract_id = fc.contract_id WHERE ts.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) ORDER BY ts.transaction_order ASC` rows, err := tx.Query(query, queryArgs(txnIDs)...) @@ -442,7 +442,7 @@ type transactionID struct { func blockTransactionIDs(tx *txn, blockID types.BlockID) (idMap map[int64]transactionID, err error) { rows, err := tx.Query(`SELECT bt.transaction_id, block_order, t.transaction_id FROM block_transactions bt -INNER JOIN transactions t ON (t.id = bt.transaction_id) +INNER JOIN transactions t ON t.id = bt.transaction_id WHERE block_id = ? ORDER BY block_order ASC`, encode(blockID)) if err != nil { return nil, err @@ -466,7 +466,7 @@ WHERE block_id = ? ORDER BY block_order ASC`, encode(blockID)) func blockMinerPayouts(tx *txn, blockID types.BlockID) ([]explorer.SiacoinOutput, error) { query := `SELECT sc.output_id, sc.leaf_index, sc.spent_index, sc.source, sc.maturity_height, sc.address, sc.value FROM siacoin_elements sc -INNER JOIN miner_payouts mp ON (mp.output_id = sc.id) +INNER JOIN miner_payouts mp ON mp.output_id = sc.id WHERE mp.block_id = ? ORDER BY mp.block_order ASC` rows, err := tx.Query(query, encode(blockID)) diff --git a/persist/sqlite/v2consensus.go b/persist/sqlite/v2consensus.go index c6e1e492..02ab71f5 100644 --- a/persist/sqlite/v2consensus.go +++ b/persist/sqlite/v2consensus.go @@ -82,6 +82,13 @@ func updateV2FileContractElements(tx *txn, revert bool, b types.Block, fces []ex } defer revisionStmt.Close() + // so we can get the ids of revision parents to add to the DB + parentStmt, err := tx.Prepare(`SELECT id FROM v2_file_contract_elements WHERE contract_id = ? AND revision_number = ?`) + if err != nil { + return nil, fmt.Errorf("updateV2FileContractElements: failed to prepare parent statement: %w", err) + } + defer parentStmt.Close() + fcTxns := make(map[explorer.DBFileContract]types.TransactionID) for _, txn := range b.V2Transactions() { id := txn.ID() @@ -164,6 +171,8 @@ func updateV2FileContractElements(tx *txn, revert bool, b types.Block, fces []ex } for _, txn := range b.V2Transactions() { + // add in any contracts that are not the latest, i.e. contracts that + // were created and revised in the same block for j, fc := range txn.FileContracts { fcID := txn.V2FileContractID(txn.ID(), j) dbFC := explorer.DBFileContract{ID: fcID, RevisionNumber: fc.RevisionNumber} @@ -175,18 +184,31 @@ func updateV2FileContractElements(tx *txn, revert bool, b types.Block, fces []ex return nil, fmt.Errorf("updateFileContractElements: %w", err) } } + // add in any revisions that are not the latest, i.e. contracts that + // were revised multiple times in one block for _, fcr := range txn.FileContractRevisions { fc := fcr.Revision - fcid := types.FileContractID(fcr.Parent.ID) - dbFC := explorer.DBFileContract{ID: fcid, RevisionNumber: fc.RevisionNumber} + fcID := types.FileContractID(fcr.Parent.ID) + dbFC := explorer.DBFileContract{ID: fcID, RevisionNumber: fc.RevisionNumber} if _, exists := fcDBIds[dbFC]; exists { continue } - if err := addFC(fcid, 0, fc, nil, false); err != nil { + if err := addFC(fcID, 0, fc, nil, false); err != nil { return nil, fmt.Errorf("updateFileContractElements: %w", err) } } + // don't add anything, just set parent db IDs in fcDBIds map + for _, fcr := range txn.FileContractRevisions { + fcID := types.FileContractID(fcr.Parent.ID) + parentDBFC := explorer.DBFileContract{ID: fcID, RevisionNumber: fcr.Parent.V2FileContract.RevisionNumber} + + var dbID int64 + if err := parentStmt.QueryRow(encode(fcID), encode(parentDBFC.RevisionNumber)).Scan(&dbID); err != nil { + return nil, fmt.Errorf("updateFileContractElements: failed to get parent contract ID: %w", err) + } + fcDBIds[parentDBFC] = dbID + } } return fcDBIds, nil @@ -342,6 +364,37 @@ func addV2FileContracts(tx *txn, txnID int64, txn types.V2Transaction, dbIDs map return nil } +func addV2FileContractRevisions(tx *txn, txnID int64, txn types.V2Transaction, dbIDs map[explorer.DBFileContract]int64) error { + stmt, err := tx.Prepare(`INSERT INTO v2_transaction_file_contract_revisions(transaction_id, transaction_order, parent_contract_id, revision_contract_id) VALUES (?, ?, ?, ?)`) + if err != nil { + return fmt.Errorf("addV2FileContractRevisions: failed to prepare statement: %w", err) + } + defer stmt.Close() + + for i, fcr := range txn.FileContractRevisions { + parentDBID, ok := dbIDs[explorer.DBFileContract{ + ID: types.FileContractID(fcr.Parent.ID), + RevisionNumber: fcr.Parent.V2FileContract.RevisionNumber, + }] + if !ok { + return errors.New("addV2FileContractRevisions: parent dbID not in map") + } + + dbID, ok := dbIDs[explorer.DBFileContract{ + ID: types.FileContractID(fcr.Parent.ID), + RevisionNumber: fcr.Revision.RevisionNumber, + }] + if !ok { + return errors.New("addV2FileContractRevisions: dbID not in map") + } + + if _, err := stmt.Exec(txnID, i, parentDBID, dbID); err != nil { + return fmt.Errorf("addV2FileContractRevisions: failed to execute statement: %w", err) + } + } + return nil +} + func addV2Attestations(tx *txn, txnID int64, txn types.V2Transaction) error { stmt, err := tx.Prepare(`INSERT INTO v2_transaction_attestations(transaction_id, transaction_order, public_key, key, value, signature) VALUES (?, ?, ?, ?, ?, ?)`) if err != nil { @@ -381,6 +434,8 @@ func addV2TransactionFields(tx *txn, txns []types.V2Transaction, scDBIds map[typ return fmt.Errorf("failed to add siafund outputs: %w", err) } else if err := addV2FileContracts(tx, dbID.id, txn, v2FcDBIds); err != nil { return fmt.Errorf("failed to add file contracts: %w", err) + } else if err := addV2FileContractRevisions(tx, dbID.id, txn, v2FcDBIds); err != nil { + return fmt.Errorf("failed to add file contract revisions: %w", err) } } diff --git a/persist/sqlite/v2consensus_test.go b/persist/sqlite/v2consensus_test.go index 3528e6bb..a5a6176f 100644 --- a/persist/sqlite/v2consensus_test.go +++ b/persist/sqlite/v2consensus_test.go @@ -45,6 +45,23 @@ func getSFE(t *testing.T, db explorer.Store, sfid types.SiafundOutputID) types.S return sfe.SiafundElement } +func getFCE(t *testing.T, db explorer.Store, fcid types.FileContractID) types.V2FileContractElement { + fces, err := db.V2Contracts([]types.FileContractID{fcid}) + if err != nil { + t.Fatal(err) + } else if len(fces) == 0 { + t.Fatal("can't find fces") + } + fce := fces[0] + + fce.V2FileContractElement.MerkleProof, err = db.MerkleProof(fce.V2FileContractElement.StateElement.LeafIndex) + if err != nil { + t.Fatal(err) + } + + return fce.V2FileContractElement +} + func TestV2ArbitraryData(t *testing.T) { _, _, cm, db := newStore(t, true, func(network *consensus.Network, genesisBlock types.Block) { network.HardforkV2.AllowHeight = 1 @@ -612,7 +629,7 @@ func TestV2FileContractRevert(t *testing.T) { } { - fcs, err := db.V2ContractRevisions(txn1.V2FileContractID(txn2.ID(), 0)) + fcs, err := db.V2ContractRevisions(txn2.V2FileContractID(txn2.ID(), 0)) if err != nil { t.Fatal(err) } @@ -732,7 +749,7 @@ func TestV2FileContractKey(t *testing.T) { } { - fcs, err := db.V2Contracts([]types.FileContractID{txn1.V2FileContractID(txn2.ID(), 0)}) + fcs, err := db.V2Contracts([]types.FileContractID{txn2.V2FileContractID(txn2.ID(), 0)}) if err != nil { t.Fatal(err) } @@ -740,7 +757,7 @@ func TestV2FileContractKey(t *testing.T) { } { - fcs, err := db.V2ContractRevisions(txn1.V2FileContractID(txn2.ID(), 0)) + fcs, err := db.V2ContractRevisions(txn2.V2FileContractID(txn2.ID(), 0)) if err != nil { t.Fatal(err) } @@ -772,3 +789,105 @@ func TestV2FileContractKey(t *testing.T) { testutil.CheckV2FC(t, txn2.FileContracts[0], fcs[1]) } } + +func TestV2FileContractRevision(t *testing.T) { + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + addr1Policy := types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(pk1.PublicKey()))} + + renterPrivateKey := types.GeneratePrivateKey() + renterPublicKey := renterPrivateKey.PublicKey() + + hostPrivateKey := types.GeneratePrivateKey() + hostPublicKey := hostPrivateKey.PublicKey() + + _, genesisBlock, cm, db := newStore(t, true, func(network *consensus.Network, genesisBlock types.Block) { + network.HardforkV2.AllowHeight = 1 + network.HardforkV2.RequireHeight = 2 + genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + }) + giftSC := genesisBlock.Transactions[0].SiacoinOutputs[0].Value + + v1FC := testutil.PrepareContractFormation(renterPublicKey, hostPublicKey, types.Siacoins(1), types.Siacoins(1), 100, 105, types.VoidAddress) + v1FC.Filesize = 65 + v2FC := types.V2FileContract{ + Capacity: v1FC.Filesize, + Filesize: v1FC.Filesize, + FileMerkleRoot: v1FC.FileMerkleRoot, + ProofHeight: 20, + ExpirationHeight: 30, + RenterOutput: v1FC.ValidProofOutputs[0], + HostOutput: v1FC.ValidProofOutputs[1], + MissedHostValue: v1FC.MissedProofOutputs[1].Value, + TotalCollateral: v1FC.ValidProofOutputs[0].Value, + RenterPublicKey: renterPublicKey, + HostPublicKey: hostPublicKey, + } + fcOut := v2FC.RenterOutput.Value.Add(v2FC.HostOutput.Value).Add(cm.TipState().V2FileContractTax(v2FC)) + + txn1 := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: getSCE(t, db, genesisBlock.Transactions[0].SiacoinOutputID(0)), + SatisfiedPolicy: types.SatisfiedPolicy{Policy: addr1Policy}, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Value: giftSC.Sub(fcOut), + Address: addr1, + }}, + FileContracts: []types.V2FileContract{v2FC}, + } + testutil.SignV2TransactionWithContracts(cm.TipState(), pk1, renterPrivateKey, hostPrivateKey, &txn1) + + if err := cm.AddBlocks([]types.Block{testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn1}, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + v2FCRevision := v2FC + v2FCRevision.RevisionNumber++ + txn2 := types.V2Transaction{ + FileContractRevisions: []types.V2FileContractRevision{{ + Parent: getFCE(t, db, txn1.V2FileContractID(txn1.ID(), 0)), + Revision: v2FCRevision, + }}, + } + testutil.SignV2TransactionWithContracts(cm.TipState(), pk1, renterPrivateKey, hostPrivateKey, &txn2) + + if err := cm.AddBlocks([]types.Block{testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn2}, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn2.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2Transaction(t, txn2, dbTxns[0]) + } + + { + fcs, err := db.V2Contracts([]types.FileContractID{txn1.V2FileContractID(txn1.ID(), 0)}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn2.FileContractRevisions[0].Revision, fcs[0]) + } + + { + fcs, err := db.V2ContractRevisions(txn1.V2FileContractID(txn1.ID(), 0)) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn1.FileContracts[0], fcs[0]) + testutil.CheckV2FC(t, txn2.FileContractRevisions[0].Revision, fcs[1]) + } + + { + fcs, err := db.V2ContractsKey(renterPublicKey) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn2.FileContractRevisions[0].Revision, fcs[0]) + } +} diff --git a/persist/sqlite/v2contracts.go b/persist/sqlite/v2contracts.go index ec694fcc..d692d841 100644 --- a/persist/sqlite/v2contracts.go +++ b/persist/sqlite/v2contracts.go @@ -13,7 +13,7 @@ func scanV2FileContract(s scanner) (fce explorer.V2FileContract, err error) { var confirmationTransactionID, resolutionTransactionID types.TransactionID fc := &fce.V2FileContractElement.V2FileContract - if err = s.Scan(decodeNull(&confirmationIndex), decodeNull(&confirmationTransactionID), decodeNull(&resolution), decodeNull(&resolutionIndex), decodeNull(&resolutionTransactionID), decode(&fce.V2FileContractElement.ID), decode(&fce.V2FileContractElement.LeafIndex), decode(&fc.Capacity), decode(&fc.Filesize), decode(&fc.FileMerkleRoot), decode(&fc.ProofHeight), decode(&fc.ExpirationHeight), decode(&fc.RenterOutput.Address), decode(&fc.RenterOutput.Value), decode(&fc.HostOutput.Address), decode(&fc.HostOutput.Value), decode(&fc.MissedHostValue), decode(&fc.TotalCollateral), decode(&fc.RenterPublicKey), decode(&fc.HostPublicKey), decode(&fc.RevisionNumber), decode(&fc.RenterSignature), decode(&fc.HostSignature)); err != nil { + if err = s.Scan(decode(&fce.TransactionID), decodeNull(&confirmationIndex), decodeNull(&confirmationTransactionID), decodeNull(&resolution), decodeNull(&resolutionIndex), decodeNull(&resolutionTransactionID), decode(&fce.V2FileContractElement.ID), decode(&fce.V2FileContractElement.LeafIndex), decode(&fc.Capacity), decode(&fc.Filesize), decode(&fc.FileMerkleRoot), decode(&fc.ProofHeight), decode(&fc.ExpirationHeight), decode(&fc.RenterOutput.Address), decode(&fc.RenterOutput.Value), decode(&fc.HostOutput.Address), decode(&fc.HostOutput.Value), decode(&fc.MissedHostValue), decode(&fc.TotalCollateral), decode(&fc.RenterPublicKey), decode(&fc.HostPublicKey), decode(&fc.RevisionNumber), decode(&fc.RenterSignature), decode(&fc.HostSignature)); err != nil { return } @@ -39,9 +39,9 @@ func scanV2FileContract(s scanner) (fce explorer.V2FileContract, err error) { // V2Contracts implements explorer.Store. func (s *Store) V2Contracts(ids []types.FileContractID) (result []explorer.V2FileContract, err error) { err = s.transaction(func(tx *txn) error { - stmt, err := tx.Prepare(`SELECT rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature + stmt, err := tx.Prepare(`SELECT fc.transaction_id, rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature FROM v2_last_contract_revision rev -INNER JOIN v2_file_contract_elements fc ON (rev.contract_element_id = fc.id) +INNER JOIN v2_file_contract_elements fc ON rev.contract_element_id = fc.id WHERE rev.contract_id = ? `) if err != nil { @@ -66,9 +66,9 @@ WHERE rev.contract_id = ? // V2ContractRevisions implements explorer.Store. func (s *Store) V2ContractRevisions(id types.FileContractID) (revisions []explorer.V2FileContract, err error) { err = s.transaction(func(tx *txn) error { - query := `SELECT rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature + query := `SELECT fc.transaction_id, rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature FROM v2_file_contract_elements fc -INNER JOIN v2_last_contract_revision rev ON (rev.contract_id = fc.contract_id) +INNER JOIN v2_last_contract_revision rev ON rev.contract_id = fc.contract_id WHERE fc.contract_id = ? ORDER BY fc.revision_number ASC ` @@ -99,9 +99,9 @@ ORDER BY fc.revision_number ASC func (s *Store) V2ContractsKey(key types.PublicKey) (result []explorer.V2FileContract, err error) { err = s.transaction(func(tx *txn) error { encoded := encode(key) - rows, err := tx.Query(`SELECT rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature + rows, err := tx.Query(`SELECT fc.transaction_id, rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature FROM v2_last_contract_revision rev -INNER JOIN v2_file_contract_elements fc ON (rev.contract_element_id = fc.id) +INNER JOIN v2_file_contract_elements fc ON rev.contract_element_id = fc.id WHERE fc.renter_public_key = ? OR fc.host_public_key = ? ORDER BY rev.confirmation_index ASC `, encoded, encoded) diff --git a/persist/sqlite/v2transactions.go b/persist/sqlite/v2transactions.go index 8ad45f48..782705c3 100644 --- a/persist/sqlite/v2transactions.go +++ b/persist/sqlite/v2transactions.go @@ -76,6 +76,8 @@ func getV2Transactions(tx *txn, ids []types.TransactionID) ([]explorer.V2Transac return nil, fmt.Errorf("getV2Transactions: failed to get siafund outputs: %w", err) } else if err := fillV2TransactionFileContracts(tx, dbIDs, txns); err != nil { return nil, fmt.Errorf("getV2Transactions: failed to get file contracts: %w", err) + } else if err := fillV2TransactionFileContractRevisions(tx, dbIDs, txns); err != nil { + return nil, fmt.Errorf("getV2Transactions: failed to get file contracts: %w", err) } // add host announcements if we have any @@ -334,32 +336,11 @@ ORDER BY ts.transaction_order ASC`) defer rows.Close() for rows.Next() { - var resolution types.V2FileContractResolutionType - var confirmationIndex, resolutionIndex types.ChainIndex - var confirmationTransactionID, resolutionTransactionID types.TransactionID - - var fce explorer.V2FileContract - fc := &fce.V2FileContractElement.V2FileContract - if err := rows.Scan(decode(&fce.TransactionID), decodeNull(&confirmationIndex), decodeNull(&confirmationTransactionID), decodeNull(&resolution), decodeNull(&resolutionIndex), decodeNull(&resolutionTransactionID), decode(&fce.V2FileContractElement.ID), decode(&fce.V2FileContractElement.LeafIndex), decode(&fc.Capacity), decode(&fc.Filesize), decode(&fc.FileMerkleRoot), decode(&fc.ProofHeight), decode(&fc.ExpirationHeight), decode(&fc.RenterOutput.Address), decode(&fc.RenterOutput.Value), decode(&fc.HostOutput.Address), decode(&fc.HostOutput.Value), decode(&fc.MissedHostValue), decode(&fc.TotalCollateral), decode(&fc.RenterPublicKey), decode(&fc.HostPublicKey), decode(&fc.RevisionNumber), decode(&fc.RenterSignature), decode(&fc.HostSignature)); err != nil { + fce, err := scanV2FileContract(rows) + if err != nil { return fmt.Errorf("failed to scan file contract: %w", err) } - if resolution != nil { - fce.Resolution = &resolution - } - if confirmationIndex != (types.ChainIndex{}) { - fce.ConfirmationIndex = &confirmationIndex - } - if resolutionIndex != (types.ChainIndex{}) { - fce.ResolutionIndex = &resolutionIndex - } - if confirmationTransactionID != (types.TransactionID{}) { - fce.ConfirmationTransactionID = &confirmationTransactionID - } - if resolutionTransactionID != (types.TransactionID{}) { - fce.ResolutionTransactionID = &resolutionTransactionID - } - txns[i].FileContracts = append(txns[i].FileContracts, fce) } return nil @@ -371,6 +352,75 @@ ORDER BY ts.transaction_order ASC`) return nil } +// fillV2TransactionFileContractRevisions fills in the file contracts for each +// transaction. +func fillV2TransactionFileContractRevisions(tx *txn, dbIDs []int64, txns []explorer.V2Transaction) error { + parentStmt, err := tx.Prepare(`SELECT fc.transaction_id, rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature +FROM v2_file_contract_elements fc +INNER JOIN v2_transaction_file_contract_revisions ts ON (ts.parent_contract_id = fc.id) +INNER JOIN v2_last_contract_revision rev ON (rev.contract_id = fc.contract_id) +WHERE ts.transaction_id = ? +ORDER BY ts.transaction_order ASC`) + if err != nil { + return fmt.Errorf("failed to prepare file contracts parent statement: %w", err) + } + defer parentStmt.Close() + + revisionStmt, err := tx.Prepare(`SELECT fc.transaction_id, rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature +FROM v2_file_contract_elements fc +INNER JOIN v2_transaction_file_contract_revisions ts ON (ts.revision_contract_id = fc.id) +INNER JOIN v2_last_contract_revision rev ON (rev.contract_id = fc.contract_id) +WHERE ts.transaction_id = ? +ORDER BY ts.transaction_order ASC`) + if err != nil { + return fmt.Errorf("failed to prepare file contracts revision statement: %w", err) + } + defer revisionStmt.Close() + + collectFileContracts := func(stmt *stmt, dbID int64) ([]explorer.V2FileContract, error) { + rows, err := stmt.Query(dbID) + if err != nil { + return nil, fmt.Errorf("failed to query file contracts: %w", err) + } + defer rows.Close() + + var contracts []explorer.V2FileContract + for rows.Next() { + fce, err := scanV2FileContract(rows) + if err != nil { + return nil, fmt.Errorf("failed to scan file contract: %w", err) + } + contracts = append(contracts, fce) + } + return contracts, nil + } + + for i, dbID := range dbIDs { + parents, err := collectFileContracts(parentStmt, dbID) + if err != nil { + return err + } + + revisions, err := collectFileContracts(revisionStmt, dbID) + if err != nil { + return err + } + + for j := range parents { + fcr := explorer.V2FileContractRevision{ + Parent: parents[j], + } + if j < len(revisions) { + fcr.Revision = revisions[j] + } + + txns[i].FileContractRevisions = append(txns[i].FileContractRevisions, fcr) + } + } + + return nil +} + // V2Transactions implements explorer.Store. func (s *Store) V2Transactions(ids []types.TransactionID) (results []explorer.V2Transaction, err error) { err = s.transaction(func(tx *txn) error {