From 002a78eabe83b75071b0eac1a7e395022fac0e35 Mon Sep 17 00:00:00 2001 From: PJ Date: Tue, 10 Sep 2024 16:41:21 +0200 Subject: [PATCH] stores: use metadata for insert/updating contracts --- api/contract.go | 29 +- api/host.go | 4 + bus/bus.go | 50 +- bus/client/contracts.go | 22 +- bus/routes.go | 98 +-- internal/bus/chainsubscriber.go | 4 +- internal/test/e2e/cluster_test.go | 30 +- stores/chain_test.go | 7 +- stores/hostdb_test.go | 23 +- stores/metadata.go | 74 ++- stores/metadata_test.go | 556 +++--------------- stores/sql/chain.go | 2 +- stores/sql/consts.go | 6 + stores/sql/database.go | 38 +- stores/sql/main.go | 202 +++---- stores/sql/mysql/chain.go | 8 +- stores/sql/mysql/main.go | 37 +- .../migration_00018_archived_contracts.sql | 106 +--- stores/sql/mysql/migrations/main/schema.sql | 9 +- stores/sql/rows.go | 7 +- stores/sql/sqlite/chain.go | 8 +- stores/sql/sqlite/main.go | 17 +- .../migration_00018_archived_contracts.sql | 13 +- stores/sql/sqlite/migrations/main/schema.sql | 3 +- stores/sql/types.go | 28 + stores/sql_test.go | 18 +- 26 files changed, 503 insertions(+), 896 deletions(-) diff --git a/api/contract.go b/api/contract.go index 080d56ea2..5606b1b35 100644 --- a/api/contract.go +++ b/api/contract.go @@ -51,26 +51,31 @@ type ( // ContractMetadata contains all metadata for a contract. ContractMetadata struct { ID types.FileContractID `json:"id"` - HostIP string `json:"hostIP"` HostKey types.PublicKey `json:"hostKey"` - ContractPrice types.Currency `json:"contractPrice"` - InitialRenterFunds types.Currency `json:"initialRenterFunds"` - - ArchivalReason string `json:"archivalReason,omitempty"` - ContractSets []string `json:"contractSets,omitempty"` ProofHeight uint64 `json:"proofHeight"` RenewedFrom types.FileContractID `json:"renewedFrom"` - RenewedTo types.FileContractID `json:"renewedTo,omitempty"` RevisionHeight uint64 `json:"revisionHeight"` RevisionNumber uint64 `json:"revisionNumber"` - SiamuxAddr string `json:"siamuxAddr,omitempty"` Size uint64 `json:"size"` - Spending ContractSpending `json:"spending"` StartHeight uint64 `json:"startHeight"` State string `json:"state"` WindowStart uint64 `json:"windowStart"` WindowEnd uint64 `json:"windowEnd"` + + // costs & spending + ContractPrice types.Currency `json:"contractPrice"` + InitialRenterFunds types.Currency `json:"initialRenterFunds"` + Spending ContractSpending `json:"spending"` + + // following fields are decorated + HostIP string `json:"hostIP"` + ContractSets []string `json:"contractSets,omitempty"` + SiamuxAddr string `json:"siamuxAddr,omitempty"` + + // following fields are only set on archived contracts + ArchivalReason string `json:"archivalReason,omitempty"` + RenewedTo types.FileContractID `json:"renewedTo,omitempty"` } // ContractPrunableData wraps a contract's size information with its id. @@ -114,7 +119,7 @@ type ( // ContractAddRequest is the request type for the /contract/:id endpoint. ContractAddRequest struct { - Contract rhpv2.ContractRevision `json:"contract"` + Revision rhpv2.ContractRevision `json:"revision"` ContractPrice types.Currency `json:"contractPrice"` InitialRenterFunds types.Currency `json:"initialRenterFunds"` StartHeight uint64 `json:"startHeight"` @@ -199,8 +204,8 @@ type ( } ContractsOpts struct { - ContractSet string `json:"contractset"` - IncludeArchived bool `json:"includeArchived"` + ContractSet string `json:"contractset"` + FilterMode string `json:"filterMode"` } ) diff --git a/api/host.go b/api/host.go index 0a6b506ff..fda7cc581 100644 --- a/api/host.go +++ b/api/host.go @@ -13,6 +13,10 @@ import ( ) const ( + ContractFilterModeAll = "all" + ContractFilterModeActive = "active" + ContractFilterModeArchived = "archived" + HostFilterModeAll = "all" HostFilterModeAllowed = "allowed" HostFilterModeBlocked = "blocked" diff --git a/bus/bus.go b/bus/bus.go index 1665947e2..420862d9e 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -210,8 +210,7 @@ type ( // A MetadataStore stores information about contracts and objects. MetadataStore interface { - AddContract(ctx context.Context, c rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) - AddRenewedContract(ctx context.Context, c rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) + AddContract(ctx context.Context, c api.ContractMetadata) error AncestorContracts(ctx context.Context, fcid types.FileContractID, minStartHeight uint64) ([]api.ContractMetadata, error) ArchiveContract(ctx context.Context, id types.FileContractID, reason string) error ArchiveContracts(ctx context.Context, toArchive map[types.FileContractID]string) error @@ -221,6 +220,7 @@ type ( ContractSets(ctx context.Context) ([]string, error) RecordContractSpending(ctx context.Context, records []api.ContractSpendingRecord) error RemoveContractSet(ctx context.Context, name string) error + RenewContract(ctx context.Context, c api.ContractMetadata) error RenewedContract(ctx context.Context, renewedFrom types.FileContractID) (api.ContractMetadata, error) UpdateContractSet(ctx context.Context, set string, toAdd, toRemove []types.FileContractID) error @@ -429,7 +429,6 @@ func (b *Bus) Handler() http.Handler { "POST /contract/:id/keepalive": b.contractKeepaliveHandlerPOST, "POST /contract/:id/prune": b.contractPruneHandlerPOST, "POST /contract/:id/renew": b.contractIDRenewHandlerPOST, - "POST /contract/:id/renewed": b.contractIDRenewedHandlerPOST, "POST /contract/:id/release": b.contractReleaseHandlerPOST, "GET /contract/:id/roots": b.contractIDRootsHandlerGET, "GET /contract/:id/size": b.contractSizeHandlerGET, @@ -533,7 +532,20 @@ func (b *Bus) Shutdown(ctx context.Context) error { } func (b *Bus) addContract(ctx context.Context, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) { - c, err := b.ms.AddContract(ctx, rev, contractPrice, initialRenterFunds, startHeight, state) + if err := b.ms.AddContract(ctx, api.ContractMetadata{ + ID: rev.ID(), + HostKey: rev.HostKey(), + StartHeight: startHeight, + State: state, + WindowStart: rev.Revision.WindowStart, + WindowEnd: rev.Revision.WindowEnd, + ContractPrice: contractPrice, + InitialRenterFunds: initialRenterFunds, + }); err != nil { + return api.ContractMetadata{}, err + } + + added, err := b.ms.Contract(ctx, rev.ID()) if err != nil { return api.ContractMetadata{}, err } @@ -542,29 +554,45 @@ func (b *Bus) addContract(ctx context.Context, rev rhpv2.ContractRevision, contr Module: api.ModuleContract, Event: api.EventAdd, Payload: api.EventContractAdd{ - Added: c, + Added: added, Timestamp: time.Now().UTC(), }, }) - return c, nil + + return added, err } -func (b *Bus) addRenewedContract(ctx context.Context, renewedFrom types.FileContractID, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) { - r, err := b.ms.AddRenewedContract(ctx, rev, contractPrice, initialRenterFunds, startHeight, renewedFrom, state) +func (b *Bus) addRenewal(ctx context.Context, renewedFrom types.FileContractID, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) { + if err := b.ms.RenewContract(ctx, api.ContractMetadata{ + ID: rev.ID(), + HostKey: rev.HostKey(), + RenewedFrom: renewedFrom, + StartHeight: startHeight, + State: state, + WindowStart: rev.Revision.WindowStart, + WindowEnd: rev.Revision.WindowEnd, + ContractPrice: contractPrice, + InitialRenterFunds: initialRenterFunds, + }); err != nil { + return api.ContractMetadata{}, fmt.Errorf("couldn't add renewal: %w", err) + } + + renewal, err := b.ms.Contract(ctx, rev.ID()) if err != nil { return api.ContractMetadata{}, err } - b.sectors.HandleRenewal(r.ID, r.RenewedFrom) + b.sectors.HandleRenewal(renewal.ID, renewal.RenewedFrom) b.broadcastAction(webhooks.Event{ Module: api.ModuleContract, Event: api.EventRenew, Payload: api.EventContractRenew{ - Renewal: r, + Renewal: renewal, Timestamp: time.Now().UTC(), }, }) - return r, nil + + return renewal, err } func (b *Bus) broadcastContract(ctx context.Context, fcid types.FileContractID) (txnID types.TransactionID, _ error) { diff --git a/bus/client/contracts.go b/bus/client/contracts.go index 9ecdc7baa..5789eb035 100644 --- a/bus/client/contracts.go +++ b/bus/client/contracts.go @@ -12,9 +12,9 @@ import ( ) // AddContract adds the provided contract to the metadata store. -func (c *Client) AddContract(ctx context.Context, contract rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (added api.ContractMetadata, err error) { - err = c.c.WithContext(ctx).POST(fmt.Sprintf("/contract/%s", contract.ID()), api.ContractAddRequest{ - Contract: contract, +func (c *Client) AddContract(ctx context.Context, revision rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (added api.ContractMetadata, err error) { + err = c.c.WithContext(ctx).POST(fmt.Sprintf("/contract/%s", revision.ID()), api.ContractAddRequest{ + Revision: revision, ContractPrice: contractPrice, InitialRenterFunds: initialRenterFunds, StartHeight: startHeight, @@ -23,19 +23,6 @@ func (c *Client) AddContract(ctx context.Context, contract rhpv2.ContractRevisio return } -// AddRenewedContract adds the provided contract to the metadata store. -func (c *Client) AddRenewedContract(ctx context.Context, contract rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (renewed api.ContractMetadata, err error) { - err = c.c.WithContext(ctx).POST(fmt.Sprintf("/contract/%s/renewed", contract.ID()), api.ContractRenewedRequest{ - Contract: contract, - ContractPrice: contractPrice, - InitialRenterFunds: initialRenterFunds, - RenewedFrom: renewedFrom, - StartHeight: startHeight, - State: state, - }, &renewed) - return -} - // AncestorContracts returns any ancestors of a given contract. func (c *Client) AncestorContracts(ctx context.Context, contractID types.FileContractID, minStartHeight uint64) (contracts []api.ContractMetadata, err error) { values := url.Values{} @@ -103,6 +90,9 @@ func (c *Client) Contracts(ctx context.Context, opts api.ContractsOpts) (contrac if opts.ContractSet != "" { values.Set("contractset", opts.ContractSet) } + if opts.FilterMode != "" { + values.Set("filtermode", opts.FilterMode) + } err = c.c.WithContext(ctx).GET("/contracts?"+values.Encode(), &contracts) return } diff --git a/bus/routes.go b/bus/routes.go index b74323858..0f4efe725 100644 --- a/bus/routes.go +++ b/bus/routes.go @@ -753,8 +753,23 @@ func (b *Bus) contractsHandlerGET(jc jape.Context) { if jc.DecodeForm("contractset", &cs) != nil { return } + filterMode := api.ContractFilterModeActive + if jc.DecodeForm("filtermode", &filterMode) != nil { + return + } + + switch filterMode { + case api.ContractFilterModeAll: + case api.ContractFilterModeActive: + case api.ContractFilterModeArchived: + default: + jc.Error(fmt.Errorf("invalid filter mode: %v", filterMode), http.StatusBadRequest) + return + } + contracts, err := b.ms.Contracts(jc.Request.Context(), api.ContractsOpts{ ContractSet: cs, + FilterMode: filterMode, }) if jc.Check("couldn't load contracts", err) == nil { jc.Encode(contracts) @@ -1074,23 +1089,36 @@ func (b *Bus) contractIDHandlerGET(jc jape.Context) { } func (b *Bus) contractIDHandlerPOST(jc jape.Context) { + // decode parameters var id types.FileContractID - var req api.ContractAddRequest - if jc.DecodeParam("id", &id) != nil || jc.Decode(&req) != nil { + if jc.DecodeParam("id", &id) != nil { return - } else if req.Contract.ID() != id { - http.Error(jc.ResponseWriter, "contract ID mismatch", http.StatusBadRequest) + } + var req api.ContractAddRequest + if jc.Decode(&req) != nil { return - } else if req.InitialRenterFunds.IsZero() { + } + + // validate the request + if req.InitialRenterFunds.IsZero() { http.Error(jc.ResponseWriter, "InitialRenterFunds can not be zero", http.StatusBadRequest) return + } else if req.Revision.ID() != id { + http.Error(jc.ResponseWriter, "Contract ID missmatch", http.StatusBadRequest) + return + } else if req.Revision.ID() == (types.FileContractID{}) { + http.Error(jc.ResponseWriter, "Contract ID is required", http.StatusBadRequest) + return + } else if req.Revision.HostKey() == (types.PublicKey{}) { + http.Error(jc.ResponseWriter, "HostKey is required", http.StatusBadRequest) + return } - a, err := b.addContract(jc.Request.Context(), req.Contract, req.ContractPrice, req.InitialRenterFunds, req.StartHeight, req.State) - if jc.Check("couldn't store contract", err) != nil { - return + // add the contract + metadata, err := b.addContract(jc.Request.Context(), req.Revision, req.ContractPrice, req.InitialRenterFunds, req.StartHeight, req.State) + if jc.Check("couldn't add contract", err) == nil { + jc.Encode(metadata) } - jc.Encode(a) } func (b *Bus) contractIDRenewHandlerPOST(jc jape.Context) { @@ -1150,11 +1178,11 @@ func (b *Bus) contractIDRenewHandlerPOST(jc jape.Context) { // send V2 transaction if we're passed the V2 hardfork allow height var newRevision rhpv2.ContractRevision - var contractPrice, fundAmount types.Currency + var contractPrice, initialRenterFunds types.Currency if b.isPassedV2AllowHeight() { panic("not implemented") } else { - newRevision, contractPrice, fundAmount, err = b.renewContract(ctx, cs, gp, c, h.Settings, rrr.RenterFunds, rrr.MinNewCollateral, rrr.MaxFundAmount, rrr.EndHeight, rrr.ExpectedNewStorage) + newRevision, contractPrice, initialRenterFunds, err = b.renewContract(ctx, cs, gp, c, h.Settings, rrr.RenterFunds, rrr.MinNewCollateral, rrr.MaxFundAmount, rrr.EndHeight, rrr.ExpectedNewStorage) if errors.Is(err, api.ErrMaxFundAmountExceeded) { jc.Error(err, http.StatusBadRequest) return @@ -1163,39 +1191,11 @@ func (b *Bus) contractIDRenewHandlerPOST(jc jape.Context) { } } - // add renewal contract to store - metadata, err := b.addRenewedContract(ctx, fcid, newRevision, contractPrice, fundAmount, cs.Index.Height, api.ContractStatePending) - if jc.Check("couldn't store contract", err) != nil { - return - } - - // send the response - jc.Encode(metadata) -} - -func (b *Bus) contractIDRenewedHandlerPOST(jc jape.Context) { - var id types.FileContractID - var req api.ContractRenewedRequest - if jc.DecodeParam("id", &id) != nil || jc.Decode(&req) != nil { - return + // add the renewal + metadata, err := b.addRenewal(ctx, fcid, newRevision, contractPrice, initialRenterFunds, cs.Index.Height, api.ContractStatePending) + if jc.Check("couldn't add renewal", err) == nil { + jc.Encode(metadata) } - if req.Contract.ID() != id { - http.Error(jc.ResponseWriter, "contract ID mismatch", http.StatusBadRequest) - return - } - if req.InitialRenterFunds.IsZero() { - http.Error(jc.ResponseWriter, "InitialRenterFunds can not be zero", http.StatusBadRequest) - return - } - if req.State == "" { - req.State = api.ContractStatePending - } - r, err := b.addRenewedContract(jc.Request.Context(), req.RenewedFrom, req.Contract, req.ContractPrice, req.InitialRenterFunds, req.StartHeight, req.State) - if jc.Check("couldn't store contract", err) != nil { - return - } - - jc.Encode(r) } func (b *Bus) contractIDRootsHandlerGET(jc jape.Context) { @@ -2441,11 +2441,11 @@ func (b *Bus) contractsFormHandler(jc jape.Context) { } // send V2 transaction if we're passed the V2 hardfork allow height - var contract rhpv2.ContractRevision + var rev rhpv2.ContractRevision if b.isPassedV2AllowHeight() { panic("not implemented") } else { - contract, err = b.formContract( + rev, err = b.formContract( ctx, settings, rfr.RenterAddress, @@ -2460,16 +2460,16 @@ func (b *Bus) contractsFormHandler(jc jape.Context) { } } - // store the contract + // add the contract metadata, err := b.addContract( ctx, - contract, - contract.Revision.MissedHostPayout().Sub(rfr.HostCollateral), + rev, + rev.Revision.MissedHostPayout().Sub(rfr.HostCollateral), rfr.RenterFunds, b.cm.Tip().Height, api.ContractStatePending, ) - if jc.Check("couldn't store contract", err) != nil { + if jc.Check("couldn't add contract", err) != nil { return } diff --git a/internal/bus/chainsubscriber.go b/internal/bus/chainsubscriber.go index e1200c24b..c279a1188 100644 --- a/internal/bus/chainsubscriber.go +++ b/internal/bus/chainsubscriber.go @@ -407,7 +407,7 @@ func (s *chainSubscriber) updateContract(tx sql.ChainUpdateTx, index types.Chain // reverted renewal: 'complete' -> 'active' if curr != nil { - if err := tx.UpdateContract(fcid, index.Height, prev.revisionNumber, prev.fileSize); err != nil { + if err := tx.UpdateContractRevision(fcid, index.Height, prev.revisionNumber, prev.fileSize); err != nil { return fmt.Errorf("failed to revert contract: %w", err) } if state == api.ContractStateComplete { @@ -440,7 +440,7 @@ func (s *chainSubscriber) updateContract(tx sql.ChainUpdateTx, index types.Chain } // handle apply - if err := tx.UpdateContract(fcid, index.Height, curr.revisionNumber, curr.fileSize); err != nil { + if err := tx.UpdateContractRevision(fcid, index.Height, curr.revisionNumber, curr.fileSize); err != nil { return fmt.Errorf("failed to update contract %v: %w", fcid, err) } diff --git a/internal/test/e2e/cluster_test.go b/internal/test/e2e/cluster_test.go index c1b7faa49..5a424aa3c 100644 --- a/internal/test/e2e/cluster_test.go +++ b/internal/test/e2e/cluster_test.go @@ -1517,26 +1517,16 @@ func TestUnconfirmedContractArchival(t *testing.T) { c := contracts[0] // add a contract to the bus - _, err = cluster.bs.AddContract(context.Background(), rhpv2.ContractRevision{ - Revision: types.FileContractRevision{ - ParentID: types.FileContractID{1}, - UnlockConditions: types.UnlockConditions{ - PublicKeys: []types.UnlockKey{ - c.HostKey.UnlockKey(), - c.HostKey.UnlockKey(), - }, - }, - FileContract: types.FileContract{ - Filesize: 0, - FileMerkleRoot: types.Hash256{}, - WindowStart: math.MaxUint32, - WindowEnd: math.MaxUint32 + 10, - Payout: types.ZeroCurrency, - UnlockHash: types.Hash256{}, - RevisionNumber: 0, - }, - }, - }, types.NewCurrency64(1), types.Siacoins(1), cs.BlockHeight, api.ContractStatePending) + err = cluster.bs.AddContract(context.Background(), api.ContractMetadata{ + ID: types.FileContractID{1}, + HostKey: types.PublicKey{1}, + StartHeight: cs.BlockHeight, + State: api.ContractStatePending, + WindowStart: math.MaxUint32, + WindowEnd: math.MaxUint32 + 10, + ContractPrice: types.NewCurrency64(1), + InitialRenterFunds: types.NewCurrency64(2), + }) tt.OK(err) // should have 2 contracts now diff --git a/stores/chain_test.go b/stores/chain_test.go index e0bcc8480..ebf2c87c6 100644 --- a/stores/chain_test.go +++ b/stores/chain_test.go @@ -64,7 +64,7 @@ func TestProcessChainUpdate(t *testing.T) { // assert update contract is successful if err := ss.ProcessChainUpdate(context.Background(), func(tx sql.ChainUpdateTx) error { - if err := tx.UpdateContract(fcid, 1, 2, 3); err != nil { + if err := tx.UpdateContractRevision(fcid, 1, 2, 3); err != nil { return err } else if err := tx.UpdateContractState(fcid, api.ContractStateActive); err != nil { return err @@ -95,7 +95,7 @@ func TestProcessChainUpdate(t *testing.T) { // assert we only update revision height if the rev number doesn't increase if err := ss.ProcessChainUpdate(context.Background(), func(tx sql.ChainUpdateTx) error { - return tx.UpdateContract(fcid, 2, 2, 4) + return tx.UpdateContractRevision(fcid, 2, 2, 4) }); err != nil { t.Fatal("unexpected error", err) } @@ -122,8 +122,7 @@ func TestProcessChainUpdate(t *testing.T) { } // renew the contract - _, err = ss.addTestRenewedContract(types.FileContractID{2}, fcid, hks[0], 1) - if err != nil { + if err = ss.renewTestContract(hks[0], fcid, types.FileContractID{2}, 1); err != nil { t.Fatal(err) } diff --git a/stores/hostdb_test.go b/stores/hostdb_test.go index ba3b2c1ac..d59640d26 100644 --- a/stores/hostdb_test.go +++ b/stores/hostdb_test.go @@ -106,17 +106,6 @@ func TestSQLHostDB(t *testing.T) { } } -func (s *SQLStore) addTestScan(hk types.PublicKey, t time.Time, err error, settings rhpv2.HostSettings) error { - return s.RecordHostScans(context.Background(), []api.HostScan{ - { - HostKey: hk, - Settings: settings, - Success: err == nil, - Timestamp: t, - }, - }) -} - // TestSQLHosts tests the Hosts method of the SQLHostDB type. func TestSQLHosts(t *testing.T) { ss := newTestSQLStore(t, defaultTestSQLStoreConfig) @@ -1187,6 +1176,18 @@ func (s *testSQLStore) addTestHosts(n int) (keys []types.PublicKey, err error) { return } +// addTestScan adds a host scan to the database. +func (s *SQLStore) addTestScan(hk types.PublicKey, t time.Time, err error, settings rhpv2.HostSettings) error { + return s.RecordHostScans(context.Background(), []api.HostScan{ + { + HostKey: hk, + Settings: settings, + Success: err == nil, + Timestamp: t, + }, + }) +} + // announceHost adds a host announcement to the database. func (s *testSQLStore) announceHost(hk types.PublicKey, na string) error { return s.db.Transaction(context.Background(), func(tx sql.DatabaseTx) error { diff --git a/stores/metadata.go b/stores/metadata.go index 2507f30ec..2832d9d1e 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -8,7 +8,6 @@ import ( "strings" "time" - rhpv2 "go.sia.tech/core/rhp/v2" "go.sia.tech/core/types" "go.sia.tech/renterd/alerts" "go.sia.tech/renterd/api" @@ -109,43 +108,6 @@ func (s *SQLStore) SlabBuffers(ctx context.Context) ([]api.SlabBuffer, error) { return buffers, nil } -func (s *SQLStore) AddContract(ctx context.Context, c rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (_ api.ContractMetadata, err error) { - var contract api.ContractMetadata - err = s.db.Transaction(ctx, func(tx sql.DatabaseTx) error { - contract, err = tx.InsertContract(ctx, c, contractPrice, initialRenterFunds, startHeight, state) - return err - }) - if err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to add contract: %w", err) - } - - return contract, nil -} - -func (s *SQLStore) Contracts(ctx context.Context, opts api.ContractsOpts) ([]api.ContractMetadata, error) { - var contracts []api.ContractMetadata - err := s.db.Transaction(ctx, func(tx sql.DatabaseTx) (err error) { - contracts, err = tx.Contracts(ctx, opts) - return - }) - return contracts, err -} - -// AddRenewedContract adds a new contract which was created as the result of a renewal to the store. -// The old contract specified as 'renewedFrom' will be deleted from the active -// contracts and moved to the archive. Both new and old contract will be linked -// to each other through the RenewedFrom and RenewedTo fields respectively. -func (s *SQLStore) AddRenewedContract(ctx context.Context, c rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (renewed api.ContractMetadata, err error) { - err = s.db.Transaction(ctx, func(tx sql.DatabaseTx) error { - renewed, err = tx.RenewContract(ctx, c, contractPrice, initialRenterFunds, startHeight, renewedFrom, state) - return err - }) - if err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to add renewed contract: %w", err) - } - return -} - func (s *SQLStore) AncestorContracts(ctx context.Context, id types.FileContractID, startHeight uint64) (ancestors []api.ContractMetadata, err error) { err = s.db.Transaction(ctx, func(tx sql.DatabaseTx) error { ancestors, err = tx.AncestorContracts(ctx, id, startHeight) @@ -205,6 +167,15 @@ func (s *SQLStore) Contract(ctx context.Context, id types.FileContractID) (cm ap return } +func (s *SQLStore) Contracts(ctx context.Context, opts api.ContractsOpts) ([]api.ContractMetadata, error) { + var contracts []api.ContractMetadata + err := s.db.Transaction(ctx, func(tx sql.DatabaseTx) (err error) { + contracts, err = tx.Contracts(ctx, opts) + return + }) + return contracts, err +} + func (s *SQLStore) ContractRoots(ctx context.Context, id types.FileContractID) (roots []types.Hash256, err error) { err = s.db.Transaction(ctx, func(tx sql.DatabaseTx) error { roots, err = tx.ContractRoots(ctx, id) @@ -237,6 +208,12 @@ func (s *SQLStore) ContractSize(ctx context.Context, id types.FileContractID) (c return cs, err } +func (s *SQLStore) AddContract(ctx context.Context, c api.ContractMetadata) error { + return s.db.Transaction(ctx, func(tx sql.DatabaseTx) error { + return tx.InsertContract(ctx, c) + }) +} + func (s *SQLStore) UpdateContractSet(ctx context.Context, name string, toAdd, toRemove []types.FileContractID) error { toAddMap := make(map[types.FileContractID]struct{}) for _, fcid := range toAdd { @@ -579,6 +556,27 @@ func (s *SQLStore) RemoveObjects(ctx context.Context, bucket, prefix string) err return nil } +func (s *SQLStore) RenewContract(ctx context.Context, c api.ContractMetadata) error { + return s.db.Transaction(ctx, func(tx sql.DatabaseTx) error { + // fetch renewed contract + renewed, err := tx.Contract(ctx, c.RenewedFrom) + if err != nil { + return err + } + + // insert renewal by updating the renewed contract + err = tx.UpdateContract(ctx, c.RenewedFrom, c) + if err != nil { + return err + } + + // reinsert renewed contract + renewed.ArchivalReason = api.ContractArchivalReasonRenewed + renewed.RenewedTo = c.ID + return tx.InsertContract(ctx, renewed) + }) +} + func (s *SQLStore) Slab(ctx context.Context, key object.EncryptionKey) (slab object.Slab, err error) { err = s.db.Transaction(ctx, func(tx sql.DatabaseTx) error { slab, err = tx.Slab(ctx, key) diff --git a/stores/metadata_test.go b/stores/metadata_test.go index 042569d7f..3c7e93cfc 100644 --- a/stores/metadata_test.go +++ b/stores/metadata_test.go @@ -102,18 +102,6 @@ func (s *SQLStore) waitForPruneLoop(ts time.Time) error { }) } -func randomMultisigUC() types.UnlockConditions { - uc := types.UnlockConditions{ - PublicKeys: make([]types.UnlockKey, 2), - SignaturesRequired: 1, - } - for i := range uc.PublicKeys { - uc.PublicKeys[i].Algorithm = types.SpecifierEd25519 - uc.PublicKeys[i].Key = frand.Bytes(32) - } - return uc -} - func updateAllObjectsHealth(db *isql.DB) error { _, err := db.Exec(context.Background(), ` UPDATE objects @@ -368,147 +356,94 @@ func TestSQLContractStore(t *testing.T) { ss := newTestSQLStore(t, defaultTestSQLStoreConfig) defer ss.Close() - // Create a host for the contract. + // add test host hk := types.GeneratePrivateKey().PublicKey() err := ss.addTestHost(hk) if err != nil { t.Fatal(err) } - // Add an announcement. + // announce a custom address if err := ss.announceHost(hk, "address"); err != nil { t.Fatal(err) } - // Create random unlock conditions for the host. - uc := randomMultisigUC() - uc.PublicKeys[1].Key = hk[:] - uc.Timelock = 192837 - - // Create a contract and set all fields. - fcid := types.FileContractID{1, 1, 1, 1, 1} - c := rhpv2.ContractRevision{ - Revision: types.FileContractRevision{ - ParentID: fcid, - UnlockConditions: uc, - FileContract: types.FileContract{ - RevisionNumber: 200, - Filesize: 4096, - FileMerkleRoot: types.Hash256{222}, - WindowStart: 400, - WindowEnd: 500, - ValidProofOutputs: []types.SiacoinOutput{ - { - Value: types.NewCurrency64(121), - Address: types.Address{2, 1, 2}, - }, - }, - MissedProofOutputs: []types.SiacoinOutput{ - { - Value: types.NewCurrency64(323), - Address: types.Address{2, 3, 2}, - }, - }, - UnlockHash: types.Hash256{6, 6, 6}, - }, - }, - Signatures: [2]types.TransactionSignature{ - { - ParentID: types.Hash256(fcid), - PublicKeyIndex: 0, - Timelock: 100000, - CoveredFields: types.CoveredFields{WholeTransaction: true}, - Signature: []byte("signature1"), - }, - { - ParentID: types.Hash256(fcid), - PublicKeyIndex: 1, - Timelock: 200000, - CoveredFields: types.CoveredFields{WholeTransaction: true}, - Signature: []byte("signature2"), - }, - }, - } - - // Look it up. Should fail. - ctx := context.Background() - _, err = ss.Contract(ctx, c.ID()) - if !errors.Is(err, api.ErrContractNotFound) { - t.Fatal(err) - } - contracts, err := ss.Contracts(ctx, api.ContractsOpts{}) - if err != nil { + // assert api.ErrContractNotFound is returned + fcid := types.FileContractID{1} + if _, err := ss.Contract(context.Background(), fcid); !errors.Is(err, api.ErrContractNotFound) { t.Fatal(err) } - if len(contracts) != 0 { - t.Fatalf("should have 0 contracts but got %v", len(contracts)) - } - // Insert it. - contractPrice := types.NewCurrency64(1) - initialRenterFunds := types.NewCurrency64(456) - startHeight := uint64(100) - returned, err := ss.AddContract(ctx, c, contractPrice, initialRenterFunds, startHeight, api.ContractStatePending) - if err != nil { - t.Fatal(err) - } - expected := api.ContractMetadata{ - ID: fcid, - HostIP: "address", - HostKey: hk, - StartHeight: 100, - State: api.ContractStatePending, - WindowStart: 400, - WindowEnd: 500, - RenewedFrom: types.FileContractID{}, - Spending: api.ContractSpending{}, + // add the contract + c := api.ContractMetadata{ + ID: fcid, + HostKey: hk, + + ProofHeight: 1, + RenewedFrom: types.FileContractID{1}, + RevisionHeight: 2, + RevisionNumber: 3, + Size: 4, + StartHeight: 5, + State: api.ContractStateActive, + WindowStart: 6, + WindowEnd: 7, + ContractPrice: types.NewCurrency64(1), - InitialRenterFunds: initialRenterFunds, - Size: c.Revision.Filesize, + InitialRenterFunds: types.NewCurrency64(2), + + Spending: api.ContractSpending{ + Deletions: types.NewCurrency64(3), + FundAccount: types.NewCurrency64(4), + SectorRoots: types.NewCurrency64(5), + Uploads: types.NewCurrency64(6), + }, } - if !reflect.DeepEqual(returned, expected) { - t.Fatal("contract mismatch", cmp.Diff(returned, expected)) + if err := ss.AddContract(context.Background(), c); err != nil { + t.Fatal(err) } - // Look it up again. - fetched, err := ss.Contract(ctx, c.ID()) + // decorate the host IP + c.HostIP = "address" + + // fetch the contract + inserted, err := ss.Contract(context.Background(), fcid) if err != nil { t.Fatal(err) + } else if !reflect.DeepEqual(inserted, c) { + t.Fatal("contract mismatch", cmp.Diff(inserted, c)) } - if !reflect.DeepEqual(fetched, expected) { - t.Fatal("contract mismatch") - } - contracts, err = ss.Contracts(ctx, api.ContractsOpts{}) - if err != nil { + + // fetch all contracts + if contracts, err := ss.Contracts(context.Background(), api.ContractsOpts{}); err != nil { t.Fatal(err) - } - if len(contracts) != 1 { + } else if len(contracts) != 1 { t.Fatalf("should have 1 contracts but got %v", len(contracts)) - } - if !reflect.DeepEqual(contracts[0], expected) { + } else if !reflect.DeepEqual(contracts[0], c) { t.Fatal("contract mismatch") } - // Add a contract set with our contract and assert we can fetch it using the set name - if err := ss.UpdateContractSet(ctx, "foo", []types.FileContractID{contracts[0].ID}, nil); err != nil { + // add a contract set, assert we can fetch it and it holds our contract + if err := ss.UpdateContractSet(context.Background(), "foo", []types.FileContractID{fcid}, nil); err != nil { t.Fatal(err) - } - if contracts, err := ss.Contracts(ctx, api.ContractsOpts{ContractSet: "foo"}); err != nil { + } else if contracts, err := ss.Contracts(context.Background(), api.ContractsOpts{ContractSet: "foo"}); err != nil { t.Fatal(err) } else if len(contracts) != 1 { t.Fatalf("should have 1 contracts but got %v", len(contracts)) } - if _, err := ss.Contracts(ctx, api.ContractsOpts{ContractSet: "bar"}); !errors.Is(err, api.ErrContractSetNotFound) { + + // assert api.ErrContractSetNotFound is returned + if _, err := ss.Contracts(context.Background(), api.ContractsOpts{ContractSet: "bar"}); !errors.Is(err, api.ErrContractSetNotFound) { t.Fatal(err) } - // Add another contract set. - if err := ss.UpdateContractSet(ctx, "foo2", []types.FileContractID{contracts[0].ID}, nil); err != nil { + // add another contract set + if err := ss.UpdateContractSet(context.Background(), "foo2", []types.FileContractID{fcid}, nil); err != nil { t.Fatal(err) } - // Fetch contract sets. - sets, err := ss.ContractSets(ctx) + // fetch all sets + sets, err := ss.ContractSets(context.Background()) if err != nil { t.Fatal(err) } @@ -519,17 +454,17 @@ func TestSQLContractStore(t *testing.T) { t.Fatal("wrong sets returned", sets) } - // Archive the contract. - if err := ss.ArchiveContract(ctx, c.ID(), api.ContractArchivalReasonRemoved); err != nil { + // archive the contract + if err := ss.ArchiveContract(context.Background(), fcid, api.ContractArchivalReasonRemoved); err != nil { t.Fatal(err) } - // Look it up. Should fail. - _, err = ss.Contract(ctx, c.ID()) + // assert archived contracts are not returned + _, err = ss.Contract(context.Background(), fcid) if !errors.Is(err, api.ErrContractNotFound) { t.Fatal(err) } - contracts, err = ss.Contracts(ctx, api.ContractsOpts{}) + contracts, err := ss.Contracts(context.Background(), api.ContractsOpts{}) if err != nil { t.Fatal(err) } @@ -537,7 +472,7 @@ func TestSQLContractStore(t *testing.T) { t.Fatalf("should have 0 contracts but got %v", len(contracts)) } - // Make sure the sectors were removed + // assert sectors got removed if count := ss.Count("contract_sectors"); count != 0 { t.Fatalf("expected %v objects in contract_sectors but got %v", 0, count) } @@ -590,226 +525,14 @@ func TestContractRoots(t *testing.T) { } } -// TestRenewContract is a test for AddRenewedContract. -func TestRenewedContract(t *testing.T) { - ss := newTestSQLStore(t, defaultTestSQLStoreConfig) - defer ss.Close() - - // create a host - hk := types.PublicKey{1} - err := ss.addTestHost(hk) - if err != nil { - t.Fatal(err) - } - - // announce the host so we can assert the net address - if err := ss.announceHost(hk, "address"); err != nil { - t.Fatal(err) - } - - // create a contract - fcid := types.FileContractID{1} - c := newTestContract(fcid, hk) - added, err := ss.AddContract(context.Background(), c, types.NewCurrency64(1), types.NewCurrency64(2), 1, api.ContractStatePending) - if err != nil { - t.Fatal(err) - } - - // add it to a set - err = ss.UpdateContractSet(context.Background(), testContractSet, []types.FileContractID{fcid}, nil) - if err != nil { - t.Fatal(err) - } - - // assert the contract is returned - if added.RenewedFrom != (types.FileContractID{}) { - t.Fatal("unexpected") - } - - // add an object - obj := newTestObject(1) - obj.Slabs[0].MinShards = 1 - obj.Slabs[0].Shards[0].Contracts = map[types.PublicKey][]types.FileContractID{hk: {fcid}} - obj.Slabs[0].Shards[0].LatestHost = hk - obj.Slabs[0].Shards = obj.Slabs[0].Shards[:1] - if _, err := ss.addTestObject(t.Name(), obj); err != nil { - t.Fatal(err) - } - - // record contract spending - s := api.ContractSpending{ - Uploads: types.Siacoins(1), - FundAccount: types.Siacoins(2), - Deletions: types.Siacoins(3), - SectorRoots: types.Siacoins(4), - } - if err := ss.RecordContractSpending(context.Background(), []api.ContractSpendingRecord{ - {ContractID: fcid, RevisionNumber: 1, Size: rhpv2.SectorSize, ContractSpending: s}, - }); err != nil { - t.Fatal(err) - } - - // no slabs should be unhealthy. - if err := ss.RefreshHealth(context.Background()); err != nil { - t.Fatal(err) - } - slabs, err := ss.UnhealthySlabs(context.Background(), 0.99, testContractSet, -1) - if err != nil { - t.Fatal(err) - } else if len(slabs) > 0 { - t.Fatal("shouldn't return any slabs", len(slabs), slabs[0].Health) - } - - // assert there's no renewal - _, err = ss.RenewedContract(context.Background(), fcid) - if !errors.Is(err, api.ErrContractNotFound) { - t.Fatal("unexpected", err) - } - - // renew it - fcidR := types.FileContractID{2} - rev := rhpv2.ContractRevision{ - Revision: types.FileContractRevision{ - ParentID: fcidR, - UnlockConditions: c.Revision.UnlockConditions, - FileContract: types.FileContract{ - Filesize: 2 * rhpv2.SectorSize, - MissedProofOutputs: []types.SiacoinOutput{}, - ValidProofOutputs: []types.SiacoinOutput{}, - }, - }, - } - if _, err := ss.AddRenewedContract(context.Background(), rev, types.NewCurrency64(3), types.NewCurrency64(4), 2, fcid, api.ContractStatePending); err != nil { - t.Fatal(err) - } - - // assert there's a renewal - renewed, err := ss.RenewedContract(context.Background(), fcid) - if err != nil { - t.Fatal("unexpected", err) - } else if renewed.ID != fcidR { - t.Fatal("unexpected") - } - - // make sure the contract set was updated. - contracts, err := ss.Contracts(context.Background(), api.ContractsOpts{ContractSet: testContractSet}) - if err != nil { - t.Fatal(err) - } else if len(contracts) != 1 { - t.Fatal("unexpected number of contracts", len(contracts)) - } else if contracts[0].ID != fcidR { - t.Fatal("unexpected contract ID", contracts[0].ID) - } - - // slab should still be in good shape. - if err := ss.RefreshHealth(context.Background()); err != nil { - t.Fatal(err) - } - slabs, err = ss.UnhealthySlabs(context.Background(), 0.99, "test", 10) - if err != nil { - t.Fatal(err) - } - if len(slabs) > 0 { - t.Fatal("shouldn't return any slabs", len(slabs)) - } - - // renewed contract should not be returned - _, err = ss.Contract(context.Background(), fcid) - if !errors.Is(err, api.ErrContractNotFound) { - t.Fatal(err) - } - - // renewal should be returned - renewal, err := ss.Contract(context.Background(), fcidR) - if err != nil { - t.Fatal(err) - } - expected := api.ContractMetadata{ - ID: fcidR, - HostIP: "address", - HostKey: hk, - StartHeight: 2, - RenewedFrom: fcid, - Size: 2 * rhpv2.SectorSize, - State: api.ContractStatePending, - Spending: api.ContractSpending{ - Uploads: types.ZeroCurrency, - FundAccount: types.ZeroCurrency, - }, - ContractPrice: types.NewCurrency64(3), - ContractSets: []string{"test"}, - InitialRenterFunds: types.NewCurrency64(4), - } - if !reflect.DeepEqual(renewal, expected) { - t.Fatal("mismatch") - } - - // ancestor should be returned - ancestors, err := ss.AncestorContracts(context.Background(), fcidR, 0) - if err != nil { - t.Fatal(err) - } else if len(ancestors) != 1 { - t.Fatalf("expected 1 ancestor but got %v", len(ancestors)) - } - - expectedContract := api.ContractMetadata{ - ID: fcid, - HostIP: "address", - HostKey: c.HostKey(), - RenewedTo: fcidR, - Spending: s, - - ArchivalReason: api.ContractArchivalReasonRenewed, - ContractPrice: types.NewCurrency64(1), - ProofHeight: 0, - RenewedFrom: types.FileContractID{}, - RevisionHeight: 0, - RevisionNumber: 1, - Size: rhpv2.SectorSize, - StartHeight: 1, - State: api.ContractStatePending, - InitialRenterFunds: types.NewCurrency64(2), - WindowStart: 400, - WindowEnd: 500, - } - if !reflect.DeepEqual(ancestors[0], expectedContract) { - t.Fatal("mismatch", cmp.Diff(ancestors[0], expectedContract)) - } - - // renew it again - fcidRR := types.FileContractID{3} - rev = rhpv2.ContractRevision{ - Revision: types.FileContractRevision{ - ParentID: fcidRR, - UnlockConditions: c.Revision.UnlockConditions, - FileContract: types.FileContract{ - MissedProofOutputs: []types.SiacoinOutput{}, - ValidProofOutputs: []types.SiacoinOutput{}, - }, - }, - } - - // assert the renewed contract is returned - renewedContract, err := ss.AddRenewedContract(context.Background(), rev, types.NewCurrency64(5), types.NewCurrency64(6), 3, fcidR, api.ContractStatePending) - if err != nil { - t.Fatal(err) - } else if renewedContract.RenewedFrom != fcidR { - t.Fatal("unexpected") - } - - // ancestor should be returned - ancestors, err = ss.AncestorContracts(context.Background(), fcidRR, 0) - if err != nil { - t.Fatal(err) - } else if len(ancestors) != 2 { - t.Fatalf("expected 2 ancestor but got %v", len(ancestors)) - } -} - // TestAncestorsContracts verifies that AncestorContracts returns the right // ancestors in the correct order. func TestAncestorsContracts(t *testing.T) { - ss := newTestSQLStore(t, defaultTestSQLStoreConfig) + cfg := defaultTestSQLStoreConfig + cfg.persistent = true + cfg.dir = "/Users/peterjan/testing3" + os.RemoveAll(cfg.dir) + ss := newTestSQLStore(t, cfg) defer ss.Close() hk := types.PublicKey{1, 2, 3} @@ -824,7 +547,7 @@ func TestAncestorsContracts(t *testing.T) { t.Fatal(err) } for i := 1; i < len(fcids); i++ { - if _, err := ss.addTestRenewedContract(fcids[i], fcids[i-1], hk, uint64(i)); err != nil { + if err := ss.renewTestContract(hk, fcids[i-1], fcids[i], uint64(i)); err != nil { t.Fatal(err) } } @@ -847,19 +570,13 @@ func TestAncestorsContracts(t *testing.T) { if j := len(fcids) - 1 - i; j >= 0 { renewedTo = fcids[j] } - expected := api.ContractMetadata{ - ArchivalReason: api.ContractArchivalReasonRenewed, - ID: fcids[len(fcids)-2-i], - HostKey: hk, - RenewedFrom: renewedFrom, - RenewedTo: renewedTo, - RevisionNumber: 200, - StartHeight: uint64(len(fcids) - 2 - i), - Size: 4096, - State: api.ContractStatePending, - WindowStart: 400, - WindowEnd: 500, - } + + expected := newTestContract(fcids[len(fcids)-2-i], hk) + expected.RenewedFrom = renewedFrom + expected.RenewedTo = renewedTo + expected.ArchivalReason = api.ContractArchivalReasonRenewed + expected.StartHeight = uint64(len(fcids) - 2 - i) + expected.Spending = api.ContractSpending{} if !reflect.DeepEqual(contracts[i], expected) { t.Log(cmp.Diff(contracts[i], expected)) t.Fatal("wrong contract", i, contracts[i]) @@ -937,51 +654,13 @@ func TestArchiveContracts(t *testing.T) { } } -func newTestContract(fcid types.FileContractID, hk types.PublicKey) rhpv2.ContractRevision { - uc := randomMultisigUC() - uc.PublicKeys[1].Key = hk[:] - uc.Timelock = 192837 - return rhpv2.ContractRevision{ - Revision: types.FileContractRevision{ - ParentID: fcid, - UnlockConditions: uc, - FileContract: types.FileContract{ - RevisionNumber: 200, - Filesize: 4096, - FileMerkleRoot: types.Hash256{222}, - WindowStart: 400, - WindowEnd: 500, - ValidProofOutputs: []types.SiacoinOutput{ - { - Value: types.NewCurrency64(121), - Address: types.Address{2, 1, 2}, - }, - }, - MissedProofOutputs: []types.SiacoinOutput{ - { - Value: types.NewCurrency64(323), - Address: types.Address{2, 3, 2}, - }, - }, - UnlockHash: types.Hash256{6, 6, 6}, - }, - }, - Signatures: [2]types.TransactionSignature{ - { - ParentID: types.Hash256(fcid), - PublicKeyIndex: 0, - Timelock: 100000, - CoveredFields: types.CoveredFields{WholeTransaction: true}, - Signature: []byte("signature1"), - }, - { - ParentID: types.Hash256(fcid), - PublicKeyIndex: 1, - Timelock: 200000, - CoveredFields: types.CoveredFields{WholeTransaction: true}, - Signature: []byte("signature2"), - }, - }, +func newTestContract(fcid types.FileContractID, hk types.PublicKey) api.ContractMetadata { + return api.ContractMetadata{ + ID: fcid, + HostKey: hk, + State: api.ContractStatePending, + ContractPrice: types.NewCurrency64(1), + InitialRenterFunds: types.NewCurrency64(2), } } @@ -990,25 +669,21 @@ func TestSQLMetadataStore(t *testing.T) { ss := newTestSQLStore(t, defaultTestSQLStoreConfig) defer ss.Close() - // Create 2 hosts + // add 2 hosts hks, err := ss.addTestHosts(2) if err != nil { t.Fatal(err) } hk1, hk2 := hks[0], hks[1] - // Create 2 contracts - fcids, contracts, err := ss.addTestContracts(hks) + // add 2 contracts + fcids, _, err := ss.addTestContracts(hks) if err != nil { t.Fatal(err) } fcid1, fcid2 := fcids[0], fcids[1] - // Extract start height and total cost - startHeight1, initialRenterFunds1 := contracts[0].StartHeight, contracts[0].InitialRenterFunds - startHeight2, initialRenterFunds2 := contracts[1].StartHeight, contracts[1].InitialRenterFunds - - // Create an object with 2 slabs pointing to 2 different sectors. + // create an object with 2 slabs pointing to 2 different sectors. obj1 := object.Object{ Key: object.GenerateEncryptionKey(), Slabs: []object.SlabSlice{ @@ -1035,14 +710,14 @@ func TestSQLMetadataStore(t *testing.T) { }, } - // Store it. + // add it ctx := context.Background() objID := "key1" if _, err := ss.addTestObject(objID, obj1); err != nil { t.Fatal(err) } - // Fetch it using get and verify every field. + // fetch it obj, err := ss.Object(context.Background(), api.DefaultBucketName, objID) if err != nil { t.Fatal(err) @@ -1113,12 +788,12 @@ func TestSQLMetadataStore(t *testing.T) { t.Fatal("object mismatch", cmp.Diff(obj, expectedObj, cmp.AllowUnexported(object.EncryptionKey{}), cmp.Comparer(api.CompareTimeRFC3339))) } - // Try to store it again. Should work. + // try to add it again, should work if _, err := ss.addTestObject(objID, obj1); err != nil { t.Fatal(err) } - // Fetch it again and verify. + // fetch it again and verify obj, err = ss.Object(context.Background(), api.DefaultBucketName, objID) if err != nil { t.Fatal(err) @@ -1130,12 +805,12 @@ func TestSQLMetadataStore(t *testing.T) { } obj.ModTime = api.TimeRFC3339{} - // The expected object is the same. + // the expected object is the same if !reflect.DeepEqual(obj, expectedObj) { t.Fatal("object mismatch", cmp.Diff(obj, expectedObj, cmp.AllowUnexported(object.EncryptionKey{}), cmp.Comparer(api.CompareTimeRFC3339))) } - // Fetch it and verify again. + // fetch it and verify again fullObj, err := ss.Object(ctx, api.DefaultBucketName, objID) if err != nil { t.Fatal(err) @@ -1161,22 +836,10 @@ func TestSQLMetadataStore(t *testing.T) { expectedContract1 := api.ContractMetadata{ ID: fcid1, - HostIP: "", HostKey: hk1, - SiamuxAddr: "", - ProofHeight: 0, - RevisionHeight: 0, - RevisionNumber: 0, - Size: 4096, - StartHeight: startHeight1, State: api.ContractStatePending, - WindowStart: 400, - WindowEnd: 500, - ContractPrice: types.ZeroCurrency, - RenewedFrom: types.FileContractID{}, - Spending: api.ContractSpending{}, - InitialRenterFunds: initialRenterFunds1, - ContractSets: nil, + ContractPrice: types.NewCurrency64(1), + InitialRenterFunds: types.NewCurrency64(2), } expectedObjSlab2 := object.Slab{ @@ -1196,25 +859,13 @@ func TestSQLMetadataStore(t *testing.T) { expectedContract2 := api.ContractMetadata{ ID: fcid2, - HostIP: "", HostKey: hk2, - SiamuxAddr: "", - ProofHeight: 0, - RevisionHeight: 0, - RevisionNumber: 0, - Size: 4096, - StartHeight: startHeight2, State: api.ContractStatePending, - WindowStart: 400, - WindowEnd: 500, - ContractPrice: types.ZeroCurrency, - RenewedFrom: types.FileContractID{}, - Spending: api.ContractSpending{}, - InitialRenterFunds: initialRenterFunds2, - ContractSets: nil, + ContractPrice: types.NewCurrency64(1), + InitialRenterFunds: types.NewCurrency64(2), } - // Compare slabs. + // compare slabs slab1, err := ss.Slab(context.Background(), obj1Slab0Key) if err != nil { t.Fatal(err) @@ -1244,7 +895,7 @@ func TestSQLMetadataStore(t *testing.T) { t.Fatal("mismatch", cmp.Diff(contract2, expectedContract2)) } - // Remove the first slab of the object. + // remove the first slab of the object obj1.Slabs = obj1.Slabs[1:] fullObj, err = ss.addTestObject(objID, obj1) if err != nil { @@ -1253,7 +904,7 @@ func TestSQLMetadataStore(t *testing.T) { t.Fatal("object mismatch") } - // Sanity check the db at the end of the test. We expect: + // sanity check the db at the end of the test. We expect: // - 1 element in the object table since we only stored and overwrote a single object // - 1 element in the slabs table since we updated the object to only have 1 slab // - 1 element in the slices table for the same reason @@ -1284,8 +935,8 @@ func TestSQLMetadataStore(t *testing.T) { t.Fatal(err) } - // Delete the object. Due to the cascade this should delete everything - // but the sectors. + // delete the object, due to the cascade this should delete everything but + // the sectors if err := ss.RemoveObjectBlocking(ctx, api.DefaultBucketName, objID); err != nil { t.Fatal(err) } @@ -3517,18 +3168,7 @@ func TestMarkSlabUploadedAfterRenew(t *testing.T) { // renew the contract. fcidRenewed := types.FileContractID{2, 2, 2, 2, 2} - uc := randomMultisigUC() - rev := rhpv2.ContractRevision{ - Revision: types.FileContractRevision{ - ParentID: fcidRenewed, - UnlockConditions: uc, - FileContract: types.FileContract{ - MissedProofOutputs: []types.SiacoinOutput{}, - ValidProofOutputs: []types.SiacoinOutput{}, - }, - }, - } - _, err = ss.AddRenewedContract(context.Background(), rev, types.NewCurrency64(1), types.NewCurrency64(1), 100, fcid, api.ContractStatePending) + err = ss.renewTestContract(hk, fcid, fcidRenewed, 1) if err != nil { t.Fatal(err) } diff --git a/stores/sql/chain.go b/stores/sql/chain.go index 7247e23ee..bb0ed4ed7 100644 --- a/stores/sql/chain.go +++ b/stores/sql/chain.go @@ -44,7 +44,7 @@ func UpdateChainIndex(ctx context.Context, tx sql.Tx, index types.ChainIndex, l return nil } -func UpdateContract(ctx context.Context, tx sql.Tx, fcid types.FileContractID, revisionHeight, revisionNumber, size uint64, l *zap.SugaredLogger) error { +func UpdateContractRevision(ctx context.Context, tx sql.Tx, fcid types.FileContractID, revisionHeight, revisionNumber, size uint64, l *zap.SugaredLogger) error { // fetch current contract, in SQLite we could use a single query to // perform the conditional update, however we have to compare the // revision number which are stored as strings so we need to fetch the diff --git a/stores/sql/consts.go b/stores/sql/consts.go index 64558343d..2132dfee5 100644 --- a/stores/sql/consts.go +++ b/stores/sql/consts.go @@ -1,11 +1,16 @@ package sql import ( + "errors" "strings" "go.sia.tech/renterd/api" ) +var ( + ErrInvalidContractState = errors.New("invalid contract state") +) + type ContractState uint8 const ( @@ -47,6 +52,7 @@ func (s *ContractState) LoadString(state string) error { *s = contractStateFailed default: *s = contractStateInvalid + return ErrInvalidContractState } return nil } diff --git a/stores/sql/database.go b/stores/sql/database.go index 6c26879c7..c4b9fa4a1 100644 --- a/stores/sql/database.go +++ b/stores/sql/database.go @@ -5,7 +5,6 @@ import ( "io" "time" - rhpv2 "go.sia.tech/core/rhp/v2" "go.sia.tech/core/types" "go.sia.tech/coreutils/chain" "go.sia.tech/coreutils/syncer" @@ -21,9 +20,9 @@ type ( ChainUpdateTx interface { ContractState(fcid types.FileContractID) (api.ContractState, error) UpdateChainIndex(index types.ChainIndex) error - UpdateContract(fcid types.FileContractID, revisionHeight, revisionNumber, size uint64) error - UpdateContractState(fcid types.FileContractID, state api.ContractState) error UpdateContractProofHeight(fcid types.FileContractID, proofHeight uint64) error + UpdateContractRevision(fcid types.FileContractID, revisionHeight, revisionNumber, size uint64) error + UpdateContractState(fcid types.FileContractID, state api.ContractState) error UpdateFailedContracts(blockHeight uint64) error UpdateHost(hk types.PublicKey, ha chain.HostAnnouncement, bh uint64, blockID types.BlockID, ts time.Time) error @@ -158,33 +157,33 @@ type ( // webhooks.ErrWebhookNotFound is returned. DeleteWebhook(ctx context.Context, wh webhooks.Webhook) error + // HostAllowlist returns the list of public keys of hosts on the + // allowlist. + HostAllowlist(ctx context.Context) ([]types.PublicKey, error) + + // HostBlocklist returns the list of host addresses on the blocklist. + HostBlocklist(ctx context.Context) ([]string, error) + // InsertBufferedSlab inserts a buffered slab into the database. This // includes the creation of a buffered slab as well as the corresponding // regular slab it is linked to. It returns the ID of the buffered slab // that was created. InsertBufferedSlab(ctx context.Context, fileName string, contractSetID int64, ec object.EncryptionKey, minShards, totalShards uint8) (int64, error) - // InsertContract inserts a new contract into the database. - InsertContract(ctx context.Context, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) + // InsertContract creates a new contract with the given metadata. + InsertContract(ctx context.Context, c api.ContractMetadata) error // InsertMultipartUpload creates a new multipart upload and returns a // unique upload ID. InsertMultipartUpload(ctx context.Context, bucket, path string, ec object.EncryptionKey, mimeType string, metadata api.ObjectUserMetadata) (string, error) + // InsertObject inserts a new object into the database. + InsertObject(ctx context.Context, bucket, key, contractSet string, dirID int64, o object.Object, mimeType, eTag string, md api.ObjectUserMetadata) error + // InvalidateSlabHealthByFCID invalidates the health of all slabs that // are associated with any of the provided contracts. InvalidateSlabHealthByFCID(ctx context.Context, fcids []types.FileContractID, limit int64) (int64, error) - // HostAllowlist returns the list of public keys of hosts on the - // allowlist. - HostAllowlist(ctx context.Context) ([]types.PublicKey, error) - - // HostBlocklist returns the list of host addresses on the blocklist. - HostBlocklist(ctx context.Context) ([]string, error) - - // InsertObject inserts a new object into the database. - InsertObject(ctx context.Context, bucket, key, contractSet string, dirID int64, o object.Object, mimeType, eTag string, md api.ObjectUserMetadata) error - // HostsForScanning returns a list of hosts to scan which haven't been // scanned since at least maxLastScan. HostsForScanning(ctx context.Context, maxLastScan time.Time, offset, limit int) ([]api.HostAddress, error) @@ -294,12 +293,6 @@ type ( // returned. RenameObjects(ctx context.Context, bucket, prefixOld, prefixNew string, dirID int64, force bool) error - // RenewContract renews the contract in the database. That means the - // contract with the ID of 'renewedFrom' will be moved to the archived - // contracts and the new contract will overwrite the existing one, - // inheriting its sectors. - RenewContract(ctx context.Context, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) - // RenewedContract returns the metadata of the contract that was renewed // from the specified contract or ErrContractNotFound otherwise. RenewedContract(ctx context.Context, renewedFrom types.FileContractID) (api.ContractMetadata, error) @@ -357,6 +350,9 @@ type ( // one, fully overwriting the existing policy. UpdateBucketPolicy(ctx context.Context, bucket string, policy api.BucketPolicy) error + // UpdateContract sets the given metadata on the contract with given fcid. + UpdateContract(ctx context.Context, fcid types.FileContractID, c api.ContractMetadata) error + // UpdateHostAllowlistEntries updates the allowlist in the database UpdateHostAllowlistEntries(ctx context.Context, add, remove []types.PublicKey, clear bool) error diff --git a/stores/sql/main.go b/stores/sql/main.go index 3fd31c241..37ca0255e 100644 --- a/stores/sql/main.go +++ b/stores/sql/main.go @@ -28,7 +28,9 @@ import ( "lukechampine.com/frand" ) -var ErrNegativeOffset = errors.New("offset can not be negative") +var ( + ErrNegativeOffset = errors.New("offset can not be negative") +) // helper types type ( @@ -135,14 +137,14 @@ func AncestorContracts(ctx context.Context, tx sql.Tx, fcid types.FileContractID WHERE contracts.renewed_to = c.fcid ) SELECT - c.created_at, c.fcid, c.host_key, + c.created_at, c.fcid, c.host_id, c.host_key, c.archival_reason, c.proof_height, c.renewed_from, c.renewed_to, c.revision_height, c.revision_number, c.size, c.start_height, c.state, c.window_start, c.window_end, c.contract_price, c.initial_renter_funds, c.delete_spending, c.fund_account_spending, c.sector_roots_spending, c.upload_spending, "", COALESCE(h.net_address, ""), COALESCE(h.settings->>'$.siamuxport', "") FROM contracts AS c LEFT JOIN hosts h ON h.public_key = c.host_key - WHERE start_height >= ? AND archival_reason != '' + WHERE start_height >= ? AND archival_reason IS NOT NULL ORDER BY start_height DESC `, FileContractID(fcid), startHeight) if err != nil { @@ -162,6 +164,11 @@ func AncestorContracts(ctx context.Context, tx sql.Tx, fcid types.FileContractID } func ArchiveContract(ctx context.Context, tx sql.Tx, fcid types.FileContractID, reason string) error { + // validate reason + if reason == "" { + return fmt.Errorf("archival reason cannot be empty") + } + // archive contract res, err := tx.Exec(ctx, "UPDATE contracts SET archival_reason = ? WHERE fcid = ?", reason, FileContractID(fcid)) if err != nil { @@ -218,7 +225,7 @@ func Bucket(ctx context.Context, tx sql.Tx, bucket string) (api.Bucket, error) { } func Contract(ctx context.Context, tx sql.Tx, fcid types.FileContractID) (api.ContractMetadata, error) { - contracts, err := QueryContracts(ctx, tx, []string{"c.fcid = ?", "c.archival_reason = ''"}, []any{FileContractID(fcid)}) + contracts, err := QueryContracts(ctx, tx, []string{"c.fcid = ?", "c.archival_reason IS NULL"}, []any{FileContractID(fcid)}) if err != nil { return api.ContractMetadata{}, fmt.Errorf("failed to fetch contract: %w", err) } else if len(contracts) == 0 { @@ -264,9 +271,23 @@ func Contracts(ctx context.Context, tx sql.Tx, opts api.ContractsOpts) ([]api.Co whereExprs = append(whereExprs, "cs.id = ?") whereArgs = append(whereArgs, contractSetID) } - if !opts.IncludeArchived { - whereExprs = append(whereExprs, "c.archival_reason = ''") + + if opts.FilterMode != "" { + // validate filter mode + switch opts.FilterMode { + case api.ContractFilterModeActive: + whereExprs = append(whereExprs, "c.archival_reason IS NULL") + case api.ContractFilterModeArchived: + whereExprs = append(whereExprs, "c.archival_reason IS NOT NULL") + case api.ContractFilterModeAll: + default: + return nil, fmt.Errorf("invalid filter mode: %v", opts.FilterMode) + } + } else { + // default to active contracts + whereExprs = append(whereExprs, "c.archival_reason IS NULL") } + return QueryContracts(ctx, tx, whereExprs, whereArgs) } @@ -301,7 +322,7 @@ func ContractSets(ctx context.Context, tx sql.Tx) ([]string, error) { func ContractSize(ctx context.Context, tx sql.Tx, id types.FileContractID) (api.ContractSize, error) { var contractID, size uint64 - if err := tx.QueryRow(ctx, "SELECT id, size FROM contracts WHERE fcid = ? AND archival_reason = ''", FileContractID(id)). + if err := tx.QueryRow(ctx, "SELECT id, size FROM contracts WHERE fcid = ? AND archival_reason IS NULL", FileContractID(id)). Scan(&contractID, &size); errors.Is(err, dsql.ErrNoRows) { return api.ContractSize{}, api.ErrContractNotFound } else if err != nil { @@ -334,7 +355,7 @@ func ContractSizes(ctx context.Context, tx sql.Tx) (map[types.FileContractID]api rows, err := tx.Query(ctx, ` SELECT c.fcid, c.size, c.size FROM contracts c - WHERE archival_reason = '' AND NOT EXISTS ( + WHERE archival_reason IS NULL AND NOT EXISTS ( SELECT 1 FROM contract_sectors cs WHERE cs.db_contract_id = c.id @@ -345,7 +366,7 @@ func ContractSizes(ctx context.Context, tx sql.Tx) (map[types.FileContractID]api SELECT c.fcid, c.size, MAX(c.size) as contract_size, COUNT(*) * ? as sector_size FROM contracts c INNER JOIN contract_sectors cs ON cs.db_contract_id = c.id - WHERE archival_reason = '' + WHERE archival_reason IS NULL GROUP BY c.fcid ) i `, rhpv2.SectorSize) @@ -486,7 +507,7 @@ func DeleteHostSector(ctx context.Context, tx sql.Tx, hk types.PublicKey, root t FROM contracts c INNER JOIN contract_sectors cs ON cs.db_contract_id = c.id INNER JOIN sectors s ON s.id = cs.db_sector_id - WHERE s.root = ? AND c.host_key != ? AND c.archival_reason = '' + WHERE s.root = ? AND c.host_key != ? AND c.archival_reason IS NULL LIMIT 1 ) AS _ ), ?) @@ -711,43 +732,41 @@ func InsertBufferedSlab(ctx context.Context, tx sql.Tx, fileName string, contrac return bufferedSlabID, nil } -func InsertContract(ctx context.Context, tx sql.Tx, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) { - var contractState ContractState - if err := contractState.LoadString(state); err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to load contract state: %w", err) +func InsertContract(ctx context.Context, tx sql.Tx, c api.ContractMetadata) error { + // validate metadata + var state ContractState + if err := state.LoadString(c.State); err != nil { + return err + } else if c.ID == (types.FileContractID{}) { + return errors.New("contract id is required") + } else if c.HostKey == (types.PublicKey{}) { + return errors.New("host key is required") } + var hostID int64 - if err := tx.QueryRow(ctx, "SELECT id FROM hosts WHERE public_key = ?", - PublicKey(rev.HostKey())).Scan(&hostID); err != nil { - return api.ContractMetadata{}, api.ErrHostNotFound + err := tx.QueryRow(ctx, `SELECT id FROM hosts WHERE public_key = ?`, PublicKey(c.HostKey)).Scan(&hostID) + if errors.Is(err, dsql.ErrNoRows) { + return api.ErrHostNotFound + } else if err != nil { + return err } - res, err := tx.Exec(ctx, ` + // insert contract + _, err = tx.Exec(ctx, ` INSERT INTO contracts ( - created_at, fcid, host_key, - proof_height, revision_height, revision_number, size, start_height, state, window_start, window_end, - contract_price, initial_renter_funds, - delete_spending, fund_account_spending, sector_roots_spending, upload_spending -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - time.Now(), FileContractID(rev.ID()), PublicKey(rev.HostKey()), - 0, 0, "0", rev.Revision.Filesize, startHeight, contractState, rev.Revision.WindowStart, rev.Revision.WindowEnd, - Currency(contractPrice), Currency(initialRenterFunds), - ZeroCurrency, ZeroCurrency, ZeroCurrency, ZeroCurrency) - if err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to insert contract: %w", err) - } - cid, err := res.LastInsertId() - if err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to fetch contract id: %w", err) +created_at, fcid, host_id, host_key, +archival_reason, proof_height, renewed_from, renewed_to, revision_height, revision_number, size, start_height, state, window_start, window_end, +contract_price, initial_renter_funds, +delete_spending, fund_account_spending, sector_roots_spending, upload_spending +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + time.Now(), FileContractID(c.ID), hostID, PublicKey(c.HostKey), + NullableString(c.ArchivalReason), c.ProofHeight, FileContractID(c.RenewedFrom), FileContractID(c.RenewedTo), c.RevisionHeight, c.RevisionNumber, c.Size, c.StartHeight, state, c.WindowStart, c.WindowEnd, + Currency(c.ContractPrice), Currency(c.InitialRenterFunds), + Currency(c.Spending.Deletions), Currency(c.Spending.FundAccount), Currency(c.Spending.SectorRoots), Currency(c.Spending.Uploads)) + if err != nil { + return fmt.Errorf("failed to insert contract: %w", err) } - - contracts, err := QueryContracts(ctx, tx, []string{"c.id = ?"}, []any{cid}) - if err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to fetch contract: %w", err) - } else if len(contracts) == 0 { - return api.ContractMetadata{}, api.ErrContractNotFound - } - return contracts[0], nil + return nil } func InsertMetadata(ctx context.Context, tx sql.Tx, objID, muID *int64, md api.ObjectUserMetadata) error { @@ -1634,7 +1653,7 @@ func ObjectsStats(ctx context.Context, tx sql.Tx, opts api.ObjectsStatsOpts) (ap } var totalUploaded uint64 - err = tx.QueryRow(ctx, "SELECT COALESCE(SUM(size), 0) FROM contracts WHERE archival_reason = ''"). + err = tx.QueryRow(ctx, "SELECT COALESCE(SUM(size), 0) FROM contracts WHERE archival_reason IS NULL"). Scan(&totalUploaded) if err != nil { return api.ObjectsStatsResponse{}, fmt.Errorf("failed to fetch contract stats: %w", err) @@ -1861,70 +1880,6 @@ WHERE h.recent_downtime >= ? AND h.recent_scan_failures >= ?`, DurationMS(maxDow return res.RowsAffected() } -func RenewContract(ctx context.Context, tx sql.Tx, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) { - var contractState ContractState - if err := contractState.LoadString(state); err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to load contract state: %w", err) - } - - // fetch existing contract - var r ContractRow - err := r.Scan(tx.QueryRow(ctx, ` -SELECT - c.created_at, c.fcid, c.host_key, - c.archival_reason, c.proof_height, c.renewed_from, c.renewed_to, c.revision_height, c.revision_number, c.size, c.start_height, c.state, c.window_start, c.window_end, - c.contract_price, c.initial_renter_funds, - c.delete_spending, c.fund_account_spending, c.sector_roots_spending, c.upload_spending, - "", "", "" -FROM contracts AS c -WHERE fcid = ?`, FileContractID(renewedFrom))) - if errors.Is(err, dsql.ErrNoRows) { - return api.ContractMetadata{}, contractNotFoundErr(renewedFrom) - } else if err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to fetch contract: %w", err) - } - - // overwrite existing contract - _, err = tx.Exec(ctx, ` -UPDATE contracts SET - created_at = ?, fcid = ?, - proof_height = ?, renewed_from = ?, revision_height = ?, revision_number = ?, size = ?, start_height = ?, state = ?, window_start = ?, window_end = ?, - contract_price = ?, initial_renter_funds = ?, - delete_spending = ?, fund_account_spending = ?, sector_roots_spending = ?, upload_spending = ? -WHERE fcid = ?`, - time.Now(), FileContractID(rev.ID()), - 0, FileContractID(renewedFrom), 0, fmt.Sprint(rev.Revision.RevisionNumber), rev.Revision.Filesize, startHeight, contractState, rev.Revision.WindowStart, rev.Revision.WindowEnd, - Currency(contractPrice), Currency(initialRenterFunds), - ZeroCurrency, ZeroCurrency, ZeroCurrency, ZeroCurrency, - FileContractID(renewedFrom), - ) - if err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to update contract: %w", err) - } - - // insert archived contract - res, err := tx.Exec(ctx, ` -INSERT INTO contracts ( - created_at, fcid, host_key, - archival_reason, proof_height, renewed_from, renewed_to, revision_height, revision_number, size, start_height, state, window_start, window_end, - contract_price, initial_renter_funds, - delete_spending, fund_account_spending, sector_roots_spending, upload_spending -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - r.CreatedAt, r.FCID, r.HostKey, - api.ContractArchivalReasonRenewed, r.ProofHeight, r.RenewedFrom, FileContractID(rev.ID()), r.RevisionHeight, r.RevisionNumber, r.Size, r.StartHeight, r.State, r.WindowStart, r.WindowEnd, - r.ContractPrice, r.InitialRenterFunds, - r.DeleteSpending, r.FundAccountSpending, r.SectorRootsSpending, r.UploadSpending) - if err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to insert archived contract: %w", err) - } else if n, err := res.RowsAffected(); err != nil { - return api.ContractMetadata{}, fmt.Errorf("failed to insert archived contract: %w", err) - } else if n != 1 { - return api.ContractMetadata{}, fmt.Errorf("failed to insert archived contract: no rows affected") - } - - return Contract(ctx, tx, rev.ID()) -} - func QueryContracts(ctx context.Context, tx sql.Tx, whereExprs []string, whereArgs []any) ([]api.ContractMetadata, error) { var whereExpr string if len(whereExprs) > 0 { @@ -1933,7 +1888,7 @@ func QueryContracts(ctx context.Context, tx sql.Tx, whereExprs []string, whereAr rows, err := tx.Query(ctx, fmt.Sprintf(` SELECT - c.created_at, c.fcid, c.host_key, + c.created_at, c.fcid, c.host_id, c.host_key, c.archival_reason, c.proof_height, c.renewed_from, c.renewed_to, c.revision_height, c.revision_number, c.size, c.start_height, c.state, c.window_start, c.window_end, c.contract_price, c.initial_renter_funds, c.delete_spending, c.fund_account_spending, c.sector_roots_spending, c.upload_spending, @@ -2104,7 +2059,7 @@ func SearchHosts(ctx context.Context, tx sql.Tx, autopilot, filterMode, usabilit offsetLimitStr := fmt.Sprintf("LIMIT %d OFFSET %d", limit, offset) // fetch stored data for each host - rows, err := tx.Query(ctx, "SELECT host_key, SUM(size) FROM contracts WHERE archival_reason = '' GROUP BY host_key") + rows, err := tx.Query(ctx, "SELECT host_key, SUM(size) FROM contracts WHERE archival_reason IS NULL GROUP BY host_key") if err != nil { return nil, fmt.Errorf("failed to fetch stored data: %w", err) } @@ -2421,6 +2376,37 @@ func UpdateBucketPolicy(ctx context.Context, tx sql.Tx, bucket string, bp api.Bu return nil } +func UpdateContract(ctx context.Context, tx sql.Tx, fcid types.FileContractID, c api.ContractMetadata) error { + // validate metadata + var state ContractState + if err := state.LoadString(c.State); err != nil { + return err + } else if c.ID == (types.FileContractID{}) { + return errors.New("contract id is required") + } else if c.HostKey == (types.PublicKey{}) { + return errors.New("host key is required") + } + + // update contract + _, err := tx.Exec(ctx, ` +UPDATE contracts SET + created_at = ?, fcid = ?, + proof_height = ?, renewed_from = ?, revision_height = ?, revision_number = ?, size = ?, start_height = ?, state = ?, window_start = ?, window_end = ?, + contract_price = ?, initial_renter_funds = ?, + delete_spending = ?, fund_account_spending = ?, sector_roots_spending = ?, upload_spending = ? +WHERE fcid = ?`, + time.Now(), FileContractID(c.ID), + 0, FileContractID(c.RenewedFrom), 0, fmt.Sprint(c.RevisionNumber), c.Size, c.StartHeight, state, c.WindowStart, c.WindowEnd, + Currency(c.ContractPrice), Currency(c.InitialRenterFunds), + ZeroCurrency, ZeroCurrency, ZeroCurrency, ZeroCurrency, + FileContractID(c.RenewedFrom), + ) + if err != nil { + return fmt.Errorf("failed to update contract: %w", err) + } + return nil +} + func UpdatePeerInfo(ctx context.Context, tx sql.Tx, addr string, fn func(*syncer.PeerInfo)) error { info, err := PeerInfo(ctx, tx, addr) if err != nil { diff --git a/stores/sql/mysql/chain.go b/stores/sql/mysql/chain.go index 4e5720c9e..fbb3fb549 100644 --- a/stores/sql/mysql/chain.go +++ b/stores/sql/mysql/chain.go @@ -180,14 +180,14 @@ func (c chainUpdateTx) UpdateChainIndex(index types.ChainIndex) error { return ssql.UpdateChainIndex(c.ctx, c.tx, index, c.l) } -func (c chainUpdateTx) UpdateContract(fcid types.FileContractID, revisionHeight, revisionNumber, size uint64) error { - return ssql.UpdateContract(c.ctx, c.tx, fcid, revisionHeight, revisionNumber, size, c.l) -} - func (c chainUpdateTx) UpdateContractProofHeight(fcid types.FileContractID, proofHeight uint64) error { return ssql.UpdateContractProofHeight(c.ctx, c.tx, fcid, proofHeight, c.l) } +func (c chainUpdateTx) UpdateContractRevision(fcid types.FileContractID, revisionHeight, revisionNumber, size uint64) error { + return ssql.UpdateContractRevision(c.ctx, c.tx, fcid, revisionHeight, revisionNumber, size, c.l) +} + func (c chainUpdateTx) UpdateContractState(fcid types.FileContractID, state api.ContractState) error { return ssql.UpdateContractState(c.ctx, c.tx, fcid, state, c.l) } diff --git a/stores/sql/mysql/main.go b/stores/sql/mysql/main.go index f126d2170..f4fd0d5ff 100644 --- a/stores/sql/mysql/main.go +++ b/stores/sql/mysql/main.go @@ -11,7 +11,6 @@ import ( "time" "unicode/utf8" - rhpv2 "go.sia.tech/core/rhp/v2" "go.sia.tech/core/types" "go.sia.tech/coreutils/syncer" "go.sia.tech/coreutils/wallet" @@ -332,18 +331,6 @@ func (tx *MainDatabaseTx) DeleteHostSector(ctx context.Context, hk types.PublicK return ssql.DeleteHostSector(ctx, tx, hk, root) } -func (tx *MainDatabaseTx) InsertBufferedSlab(ctx context.Context, fileName string, contractSetID int64, ec object.EncryptionKey, minShards, totalShards uint8) (int64, error) { - return ssql.InsertBufferedSlab(ctx, tx, fileName, contractSetID, ec, minShards, totalShards) -} - -func (tx *MainDatabaseTx) InsertContract(ctx context.Context, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) { - return ssql.InsertContract(ctx, tx, rev, contractPrice, initialRenterFunds, startHeight, state) -} - -func (tx *MainDatabaseTx) InsertMultipartUpload(ctx context.Context, bucket, key string, ec object.EncryptionKey, mimeType string, metadata api.ObjectUserMetadata) (string, error) { - return ssql.InsertMultipartUpload(ctx, tx, bucket, key, ec, mimeType, metadata) -} - func (tx *MainDatabaseTx) DeleteSettings(ctx context.Context, key string) error { return ssql.DeleteSettings(ctx, tx, key) } @@ -411,6 +398,18 @@ func (tx *MainDatabaseTx) HostsForScanning(ctx context.Context, maxLastScan time return ssql.HostsForScanning(ctx, tx, maxLastScan, offset, limit) } +func (tx *MainDatabaseTx) InsertBufferedSlab(ctx context.Context, fileName string, contractSetID int64, ec object.EncryptionKey, minShards, totalShards uint8) (int64, error) { + return ssql.InsertBufferedSlab(ctx, tx, fileName, contractSetID, ec, minShards, totalShards) +} + +func (tx *MainDatabaseTx) InsertContract(ctx context.Context, c api.ContractMetadata) error { + return ssql.InsertContract(ctx, tx, c) +} + +func (tx *MainDatabaseTx) InsertMultipartUpload(ctx context.Context, bucket, key string, ec object.EncryptionKey, mimeType string, metadata api.ObjectUserMetadata) (string, error) { + return ssql.InsertMultipartUpload(ctx, tx, bucket, key, ec, mimeType, metadata) +} + func (tx *MainDatabaseTx) InsertObject(ctx context.Context, bucket, key, contractSet string, dirID int64, o object.Object, mimeType, eTag string, md api.ObjectUserMetadata) error { // get bucket id var bucketID int64 @@ -760,12 +759,8 @@ func (tx *MainDatabaseTx) RenameObjects(ctx context.Context, bucket, prefixOld, return nil } -func (tx *MainDatabaseTx) RenewContract(ctx context.Context, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) { - return ssql.RenewContract(ctx, tx, rev, contractPrice, initialRenterFunds, startHeight, renewedFrom, state) -} - -func (tx *MainDatabaseTx) RenewedContract(ctx context.Context, renwedFrom types.FileContractID) (api.ContractMetadata, error) { - return ssql.RenewedContract(ctx, tx, renwedFrom) +func (tx *MainDatabaseTx) RenewedContract(ctx context.Context, renewedFrom types.FileContractID) (api.ContractMetadata, error) { + return ssql.RenewedContract(ctx, tx, renewedFrom) } func (tx *MainDatabaseTx) ResetChainState(ctx context.Context) error { @@ -871,6 +866,10 @@ func (tx *MainDatabaseTx) UpdateBucketPolicy(ctx context.Context, bucket string, return ssql.UpdateBucketPolicy(ctx, tx, bucket, bp) } +func (tx *MainDatabaseTx) UpdateContract(ctx context.Context, fcid types.FileContractID, c api.ContractMetadata) error { + return ssql.UpdateContract(ctx, tx, fcid, c) +} + func (tx *MainDatabaseTx) UpdateContractSet(ctx context.Context, name string, toAdd, toRemove []types.FileContractID) error { res, err := tx.Exec(ctx, "INSERT INTO contract_sets (name) VALUES (?) ON DUPLICATE KEY UPDATE id = last_insert_id(id)", name) if err != nil { diff --git a/stores/sql/mysql/migrations/main/migration_00018_archived_contracts.sql b/stores/sql/mysql/migrations/main/migration_00018_archived_contracts.sql index 689cdb432..99e3bd7ec 100644 --- a/stores/sql/mysql/migrations/main/migration_00018_archived_contracts.sql +++ b/stores/sql/mysql/migrations/main/migration_00018_archived_contracts.sql @@ -1,91 +1,24 @@ -DROP TABLE IF EXISTS contracts_temp; +ALTER TABLE contracts + RENAME COLUMN total_cost TO initial_renter_funds, + RENAME COLUMN list_spending TO sector_roots_spending; -CREATE TABLE contracts_temp ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `created_at` datetime(3) DEFAULT NULL, +ALTER TABLE contracts + ADD COLUMN host_key varbinary(32) NOT NULL DEFAULT UNHEX('0000000000000000000000000000000000000000000000000000000000000000'), + ADD COLUMN archival_reason VARCHAR(191) DEFAULT NULL, + ADD COLUMN renewed_to VARBINARY(32) DEFAULT NULL; - `archival_reason` varchar(191) NOT NULL DEFAULT '', - `fcid` varbinary(32) NOT NULL, - `host_key` varbinary(32) NOT NULL, - `proof_height` bigint unsigned DEFAULT '0', - `renewed_from` varbinary(32) DEFAULT NULL, - `renewed_to` varbinary(32) DEFAULT NULL, - `revision_height` bigint unsigned DEFAULT '0', - `revision_number` varchar(191) NOT NULL DEFAULT '0', - `size` bigint unsigned DEFAULT NULL, - `start_height` bigint unsigned NOT NULL, - `state` tinyint unsigned NOT NULL DEFAULT '0', - `window_start` bigint unsigned NOT NULL DEFAULT '0', - `window_end` bigint unsigned NOT NULL DEFAULT '0', +CREATE INDEX `idx_contracts_archival_reason` ON `contracts`(`archival_reason`); +CREATE INDEX `idx_contracts_host_key` ON `contracts`(`host_key`); +CREATE INDEX `idx_contracts_renewed_to` ON `contracts`(`renewed_to`); - `contract_price` longtext, - `initial_renter_funds` longtext, - - `delete_spending` longtext, - `fund_account_spending` longtext, - `sector_roots_spending` longtext, - `upload_spending` longtext, - PRIMARY KEY (`id`), - UNIQUE KEY `fcid` (`fcid`), - KEY `idx_contracts_archival_reason` (`archival_reason`), - KEY `idx_contracts_fcid` (`fcid`), - KEY `idx_contracts_host_key` (`host_key`), - KEY `idx_contracts_proof_height` (`proof_height`), - KEY `idx_contracts_renewed_from` (`renewed_from`), - KEY `idx_contracts_renewed_to` (`renewed_to`), - KEY `idx_contracts_revision_height` (`revision_height`), - KEY `idx_contracts_start_height` (`start_height`), - KEY `idx_contracts_state` (`state`), - KEY `idx_contracts_window_start` (`window_start`), - KEY `idx_contracts_window_end` (`window_end`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -INSERT INTO contracts_temp ( - id, - created_at, - fcid, - host_key, - proof_height, - renewed_from, - revision_height, - revision_number, - size, - start_height, - state, - window_start, - window_end, - contract_price, - initial_renter_funds, - delete_spending, - fund_account_spending, - sector_roots_spending, - upload_spending -) SELECT - c.id, - c.created_at, - c.fcid, - h.public_key, - c.proof_height, - c.renewed_from, - c.revision_height, - c.revision_number, - c.size, - c.start_height, - c.state, - c.window_start, - c.window_end, - c.contract_price, - c.total_cost, - c.delete_spending, - c.fund_account_spending, - c.list_spending, - c.upload_spending -FROM contracts c -INNER JOIN hosts h ON c.host_id = h.id; +UPDATE contracts c +INNER JOIN hosts h ON c.host_id = h.id +SET c.host_key = h.public_key; INSERT INTO contracts_temp ( created_at, fcid, + host_id, host_key, archival_reason, proof_height, @@ -107,6 +40,7 @@ INSERT INTO contracts_temp ( ) SELECT ac.created_at, ac.fcid, + h.id, COALESCE(h.public_key, UNHEX('0000000000000000000000000000000000000000000000000000000000000000')), ac.reason, ac.proof_height, @@ -128,12 +62,4 @@ INSERT INTO contracts_temp ( FROM `archived_contracts` ac LEFT JOIN hosts h ON ac.host = h.public_key; -ALTER TABLE contract_sectors DROP FOREIGN KEY fk_contract_sectors_db_contract; -ALTER TABLE contract_set_contracts DROP FOREIGN KEY fk_contract_set_contracts_db_contract; - -DROP TABLE `contracts`; -DROP TABLE `archived_contracts`; - -ALTER TABLE contracts_temp RENAME TO contracts; -ALTER TABLE contract_sectors ADD CONSTRAINT fk_contract_sectors_db_contract FOREIGN KEY (db_contract_id) REFERENCES contracts(id) ON DELETE CASCADE; -ALTER TABLE contract_set_contracts ADD CONSTRAINT fk_contract_set_contracts_db_contract FOREIGN KEY (db_contract_id) REFERENCES contracts(id) ON DELETE CASCADE; +DROP TABLE `archived_contracts`; \ No newline at end of file diff --git a/stores/sql/mysql/migrations/main/schema.sql b/stores/sql/mysql/migrations/main/schema.sql index 73bfbcf32..0933383a3 100644 --- a/stores/sql/mysql/migrations/main/schema.sql +++ b/stores/sql/mysql/migrations/main/schema.sql @@ -75,9 +75,10 @@ CREATE TABLE `contracts` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `created_at` datetime(3) DEFAULT NULL, `fcid` varbinary(32) NOT NULL, - `host_key` varbinary(32) NOT NULL, + `host_id` bigint unsigned DEFAULT NULL, + `host_key` varbinary(32), - `archival_reason` varchar(191) NOT NULL DEFAULT '', + `archival_reason` varchar(191) DEFAULT NULL, `proof_height` bigint unsigned DEFAULT '0', `renewed_from` varbinary(32) DEFAULT NULL, `renewed_to` varbinary(32) DEFAULT NULL, @@ -100,6 +101,7 @@ CREATE TABLE `contracts` ( UNIQUE KEY `fcid` (`fcid`), KEY `idx_contracts_archival_reason` (`archival_reason`), KEY `idx_contracts_fcid` (`fcid`), + KEY `idx_contracts_host_id` (`host_id`), KEY `idx_contracts_host_key` (`host_key`), KEY `idx_contracts_proof_height` (`proof_height`), KEY `idx_contracts_renewed_from` (`renewed_from`), @@ -108,7 +110,8 @@ CREATE TABLE `contracts` ( KEY `idx_contracts_start_height` (`start_height`), KEY `idx_contracts_state` (`state`), KEY `idx_contracts_window_start` (`window_start`), - KEY `idx_contracts_window_end` (`window_end`) + KEY `idx_contracts_window_end` (`window_end`), + CONSTRAINT `fk_contracts_host` FOREIGN KEY (`host_id`) REFERENCES `hosts` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- dbContractSet diff --git a/stores/sql/rows.go b/stores/sql/rows.go index d7f37d02f..6bc968af2 100644 --- a/stores/sql/rows.go +++ b/stores/sql/rows.go @@ -15,10 +15,11 @@ type Scanner interface { type ContractRow struct { CreatedAt time.Time FCID FileContractID + HostID int64 HostKey PublicKey // state fields - ArchivalReason string + ArchivalReason NullableString ProofHeight uint64 RenewedFrom FileContractID RenewedTo FileContractID @@ -48,7 +49,7 @@ type ContractRow struct { func (r *ContractRow) Scan(s Scanner) error { return s.Scan( - &r.CreatedAt, &r.FCID, &r.HostKey, + &r.CreatedAt, &r.FCID, &r.HostID, &r.HostKey, &r.ArchivalReason, &r.ProofHeight, &r.RenewedFrom, &r.RenewedTo, &r.RevisionHeight, &r.RevisionNumber, &r.Size, &r.StartHeight, &r.State, &r.WindowStart, &r.WindowEnd, &r.ContractPrice, &r.InitialRenterFunds, &r.DeleteSpending, &r.FundAccountSpending, &r.SectorRootsSpending, &r.UploadSpending, @@ -85,7 +86,7 @@ func (r *ContractRow) ContractMetadata() api.ContractMetadata { ContractPrice: types.Currency(r.ContractPrice), InitialRenterFunds: types.Currency(r.InitialRenterFunds), - ArchivalReason: r.ArchivalReason, + ArchivalReason: string(r.ArchivalReason), ContractSets: sets, ProofHeight: r.ProofHeight, RenewedFrom: types.FileContractID(r.RenewedFrom), diff --git a/stores/sql/sqlite/chain.go b/stores/sql/sqlite/chain.go index 0a80e1a98..d4d04c162 100644 --- a/stores/sql/sqlite/chain.go +++ b/stores/sql/sqlite/chain.go @@ -183,14 +183,14 @@ func (c chainUpdateTx) UpdateChainIndex(index types.ChainIndex) error { return ssql.UpdateChainIndex(c.ctx, c.tx, index, c.l) } -func (c chainUpdateTx) UpdateContract(fcid types.FileContractID, revisionHeight, revisionNumber, size uint64) error { - return ssql.UpdateContract(c.ctx, c.tx, fcid, revisionHeight, revisionNumber, size, c.l) -} - func (c chainUpdateTx) UpdateContractProofHeight(fcid types.FileContractID, proofHeight uint64) error { return ssql.UpdateContractProofHeight(c.ctx, c.tx, fcid, proofHeight, c.l) } +func (c chainUpdateTx) UpdateContractRevision(fcid types.FileContractID, revisionHeight, revisionNumber, size uint64) error { + return ssql.UpdateContractRevision(c.ctx, c.tx, fcid, revisionHeight, revisionNumber, size, c.l) +} + func (c chainUpdateTx) UpdateContractState(fcid types.FileContractID, state api.ContractState) error { return ssql.UpdateContractState(c.ctx, c.tx, fcid, state, c.l) } diff --git a/stores/sql/sqlite/main.go b/stores/sql/sqlite/main.go index 2018bc985..ba80e0ef4 100644 --- a/stores/sql/sqlite/main.go +++ b/stores/sql/sqlite/main.go @@ -11,7 +11,6 @@ import ( "time" "unicode/utf8" - rhpv2 "go.sia.tech/core/rhp/v2" "go.sia.tech/core/types" "go.sia.tech/coreutils/syncer" "go.sia.tech/coreutils/wallet" @@ -348,10 +347,6 @@ func (tx *MainDatabaseTx) InsertBufferedSlab(ctx context.Context, fileName strin return ssql.InsertBufferedSlab(ctx, tx, fileName, contractSetID, ec, minShards, totalShards) } -func (tx *MainDatabaseTx) InsertContract(ctx context.Context, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) { - return ssql.InsertContract(ctx, tx, rev, contractPrice, initialRenterFunds, startHeight, state) -} - func (tx *MainDatabaseTx) InsertMultipartUpload(ctx context.Context, bucket, key string, ec object.EncryptionKey, mimeType string, metadata api.ObjectUserMetadata) (string, error) { return ssql.InsertMultipartUpload(ctx, tx, bucket, key, ec, mimeType, metadata) } @@ -400,6 +395,10 @@ func (tx *MainDatabaseTx) HostsForScanning(ctx context.Context, maxLastScan time return ssql.HostsForScanning(ctx, tx, maxLastScan, offset, limit) } +func (tx *MainDatabaseTx) InsertContract(ctx context.Context, c api.ContractMetadata) error { + return ssql.InsertContract(ctx, tx, c) +} + func (tx *MainDatabaseTx) InsertObject(ctx context.Context, bucket, key, contractSet string, dirID int64, o object.Object, mimeType, eTag string, md api.ObjectUserMetadata) error { // get bucket id var bucketID int64 @@ -771,10 +770,6 @@ func (tx *MainDatabaseTx) RenameObjects(ctx context.Context, bucket, prefixOld, return nil } -func (tx *MainDatabaseTx) RenewContract(ctx context.Context, rev rhpv2.ContractRevision, contractPrice, initialRenterFunds types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) { - return ssql.RenewContract(ctx, tx, rev, contractPrice, initialRenterFunds, startHeight, renewedFrom, state) -} - func (tx *MainDatabaseTx) RenewedContract(ctx context.Context, renwedFrom types.FileContractID) (api.ContractMetadata, error) { return ssql.RenewedContract(ctx, tx, renwedFrom) } @@ -939,6 +934,10 @@ func (tx *MainDatabaseTx) UpdateBucketPolicy(ctx context.Context, bucket string, return ssql.UpdateBucketPolicy(ctx, tx, bucket, policy) } +func (tx *MainDatabaseTx) UpdateContract(ctx context.Context, fcid types.FileContractID, c api.ContractMetadata) error { + return ssql.UpdateContract(ctx, tx, fcid, c) +} + func (tx *MainDatabaseTx) UpdateHostAllowlistEntries(ctx context.Context, add, remove []types.PublicKey, clear bool) error { if clear { if _, err := tx.Exec(ctx, "DELETE FROM host_allowlist_entries"); err != nil { diff --git a/stores/sql/sqlite/migrations/main/migration_00018_archived_contracts.sql b/stores/sql/sqlite/migrations/main/migration_00018_archived_contracts.sql index ec2dd1bcd..a9c0635a3 100644 --- a/stores/sql/sqlite/migrations/main/migration_00018_archived_contracts.sql +++ b/stores/sql/sqlite/migrations/main/migration_00018_archived_contracts.sql @@ -4,9 +4,10 @@ CREATE TABLE contracts_temp ( `id` integer PRIMARY KEY AUTOINCREMENT, `created_at` datetime, `fcid` blob NOT NULL UNIQUE, + `host_id` integer, `host_key` blob NOT NULL, - `archival_reason` text NOT NULL DEFAULT "", + `archival_reason` text DEFAULT NULL, `proof_height` integer DEFAULT 0, `renewed_from` blob, `renewed_to` blob, @@ -43,6 +44,7 @@ INSERT INTO contracts_temp ( id, created_at, fcid, + host_id, host_key, proof_height, renewed_from, @@ -63,6 +65,7 @@ INSERT INTO contracts_temp ( c.id, c.created_at, c.fcid, + h.id, h.public_key, c.proof_height, c.renewed_from, @@ -86,6 +89,7 @@ INSERT INTO contracts_temp ( created_at, archival_reason, fcid, + host_id, host_key, proof_height, renewed_from, @@ -107,6 +111,7 @@ INSERT INTO contracts_temp ( ac.created_at, ac.reason, ac.fcid, + h.id, COALESCE(h.public_key, X'0000000000000000000000000000000000000000000000000000000000000000'), ac.proof_height, ac.renewed_from, @@ -128,8 +133,6 @@ FROM `archived_contracts` ac LEFT JOIN hosts h ON ac.host = h.public_key; DROP TABLE `archived_contracts`; - -PRAGMA foreign_keys = OFF; DROP TABLE `contracts`; -ALTER TABLE contracts_temp RENAME TO contracts; -PRAGMA foreign_keys = ON; \ No newline at end of file + +ALTER TABLE contracts_temp RENAME TO contracts; \ No newline at end of file diff --git a/stores/sql/sqlite/migrations/main/schema.sql b/stores/sql/sqlite/migrations/main/schema.sql index b66c72aa4..b1c64b696 100644 --- a/stores/sql/sqlite/migrations/main/schema.sql +++ b/stores/sql/sqlite/migrations/main/schema.sql @@ -8,9 +8,10 @@ CREATE INDEX `idx_hosts_public_key` ON `hosts`(`public_key`); CREATE INDEX `idx_hosts_net_address` ON `hosts`(`net_address`); -- dbContract -CREATE TABLE contracts (`id` integer PRIMARY KEY AUTOINCREMENT, `created_at` datetime, `fcid` blob NOT NULL UNIQUE, `host_key` blob NOT NULL, `archival_reason` text NOT NULL DEFAULT "", `proof_height` integer DEFAULT 0, `renewed_from` blob, `renewed_to` blob, `revision_height` integer DEFAULT 0, `revision_number` text NOT NULL DEFAULT "0", `size` integer, `start_height` integer NOT NULL, `state` integer NOT NULL DEFAULT 0, `window_start` integer NOT NULL DEFAULT 0, `window_end` integer NOT NULL DEFAULT 0, `contract_price` text, `initial_renter_funds` text, `delete_spending` text, `fund_account_spending` text, `sector_roots_spending` text, `upload_spending` text); +CREATE TABLE contracts (`id` integer PRIMARY KEY AUTOINCREMENT, `created_at` datetime, `fcid` blob NOT NULL UNIQUE, `host_id` integer, `host_key` blob NOT NULL, `archival_reason` text DEFAULT NULL, `proof_height` integer DEFAULT 0, `renewed_from` blob, `renewed_to` blob, `revision_height` integer DEFAULT 0, `revision_number` text NOT NULL DEFAULT "0", `size` integer, `start_height` integer NOT NULL, `state` integer NOT NULL DEFAULT 0, `window_start` integer NOT NULL DEFAULT 0, `window_end` integer NOT NULL DEFAULT 0, `contract_price` text, `initial_renter_funds` text, `delete_spending` text, `fund_account_spending` text, `sector_roots_spending` text, `upload_spending` text, CONSTRAINT `fk_contracts_host` FOREIGN KEY (`host_id`) REFERENCES `hosts`(`id`)); CREATE INDEX `idx_contracts_archival_reason` ON `contracts`(`archival_reason`); CREATE INDEX `idx_contracts_fcid` ON `contracts`(`fcid`); +CREATE INDEX `idx_contracts_host_id` ON `contracts`(`host_id`); CREATE INDEX `idx_contracts_host_key` ON `contracts`(`host_key`); CREATE INDEX `idx_contracts_proof_height` ON `contracts`(`proof_height`); CREATE INDEX `idx_contracts_renewed_from` ON `contracts`(`renewed_from`); diff --git a/stores/sql/types.go b/stores/sql/types.go index 3d92fd79c..66127c3ce 100644 --- a/stores/sql/types.go +++ b/stores/sql/types.go @@ -37,6 +37,7 @@ type ( FileContractID types.FileContractID Hash256 types.Hash256 MerkleProof struct{ Hashes []types.Hash256 } + NullableString string HostSettings rhpv2.HostSettings PriceTable rhpv3.HostPriceTable PublicKey types.PublicKey @@ -61,6 +62,7 @@ var ( _ scannerValuer = (*FileContractID)(nil) _ scannerValuer = (*Hash256)(nil) _ scannerValuer = (*MerkleProof)(nil) + _ scannerValuer = (*NullableString)(nil) _ scannerValuer = (*HostSettings)(nil) _ scannerValuer = (*PriceTable)(nil) _ scannerValuer = (*PublicKey)(nil) @@ -459,3 +461,29 @@ func (u *Unsigned64) Scan(value interface{}) error { func (u Unsigned64) Value() (driver.Value, error) { return int64(u), nil } + +// Scan scan value into NullableString, implements sql.Scanner interface. +func (s *NullableString) Scan(value interface{}) error { + if value == nil { + *s = "" + return nil + } + + switch value := value.(type) { + case string: + *s = NullableString(value) + case []byte: + *s = NullableString(value) + default: + return fmt.Errorf("failed to unmarshal NullableString value: %v %T", value, value) + } + return nil +} + +// Value returns a NullableString value, implements driver.Valuer interface. +func (s NullableString) Value() (driver.Value, error) { + if s == "" { + return nil, nil + } + return []byte(s), nil +} diff --git a/stores/sql_test.go b/stores/sql_test.go index e50d16382..5dcbe1236 100644 --- a/stores/sql_test.go +++ b/stores/sql_test.go @@ -314,13 +314,10 @@ func (s *testSQLStore) addTestContracts(keys []types.PublicKey) (fcids []types.F } func (s *SQLStore) addTestContract(fcid types.FileContractID, hk types.PublicKey) (api.ContractMetadata, error) { - rev := newTestContract(fcid, hk) - return s.AddContract(context.Background(), rev, types.ZeroCurrency, types.ZeroCurrency, 0, api.ContractStatePending) -} - -func (s *SQLStore) addTestRenewedContract(fcid, renewedFrom types.FileContractID, hk types.PublicKey, startHeight uint64) (api.ContractMetadata, error) { - rev := newTestContract(fcid, hk) - return s.AddRenewedContract(context.Background(), rev, types.ZeroCurrency, types.ZeroCurrency, startHeight, renewedFrom, api.ContractStatePending) + if err := s.AddContract(context.Background(), newTestContract(fcid, hk)); err != nil { + return api.ContractMetadata{}, err + } + return s.Contract(context.Background(), fcid) } func (s *testSQLStore) overrideSlabHealth(objectID string, health float64) (err error) { @@ -336,3 +333,10 @@ func (s *testSQLStore) overrideSlabHealth(objectID string, health float64) (err )`, health, objectID)) return } + +func (s *testSQLStore) renewTestContract(hk types.PublicKey, renewedFrom, renewedTo types.FileContractID, startHeight uint64) error { + renewal := newTestContract(renewedTo, hk) + renewal.StartHeight = startHeight + renewal.RenewedFrom = renewedFrom + return s.RenewContract(context.Background(), renewal) +}