diff --git a/api/bus.go b/api/bus.go index eea464caf..f6ad1ffbd 100644 --- a/api/bus.go +++ b/api/bus.go @@ -107,6 +107,7 @@ type ConsensusNetwork struct { type ContractsIDAddRequest struct { Contract rhpv2.ContractRevision `json:"contract"` StartHeight uint64 `json:"startHeight"` + State string `json:"state,omitempty"` TotalCost types.Currency `json:"totalCost"` } @@ -122,6 +123,7 @@ type ContractsIDRenewedRequest struct { Contract rhpv2.ContractRevision `json:"contract"` RenewedFrom types.FileContractID `json:"renewedFrom"` StartHeight uint64 `json:"startHeight"` + State string `json:"state,omitempty"` TotalCost types.Currency `json:"totalCost"` } diff --git a/api/contract.go b/api/contract.go index 0e7d580ed..9944992a5 100644 --- a/api/contract.go +++ b/api/contract.go @@ -5,6 +5,15 @@ import ( "go.sia.tech/core/types" ) +const ( + ContractStateInvalid = "invalid" + ContractStateUnknown = "unknown" + ContractStatePending = "pending" + ContractStateActive = "active" + ContractStateComplete = "complete" + ContractStateFailed = "failed" +) + type ( // A Contract wraps the contract metadata with the latest contract revision. Contract struct { @@ -31,6 +40,7 @@ type ( RevisionNumber uint64 `json:"revisionNumber"` Size uint64 `json:"size"` StartHeight uint64 `json:"startHeight"` + State string `json:"state"` WindowStart uint64 `json:"windowStart"` WindowEnd uint64 `json:"windowEnd"` @@ -68,6 +78,7 @@ type ( RevisionNumber uint64 `json:"revisionNumber"` Size uint64 `json:"size"` StartHeight uint64 `json:"startHeight"` + State string `json:"state"` WindowStart uint64 `json:"windowStart"` WindowEnd uint64 `json:"windowEnd"` } diff --git a/autopilot/autopilot.go b/autopilot/autopilot.go index 589a5a5f9..3fdfb4184 100644 --- a/autopilot/autopilot.go +++ b/autopilot/autopilot.go @@ -55,8 +55,8 @@ type Bus interface { // contracts Contracts(ctx context.Context) (contracts []api.ContractMetadata, err error) - AddContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64) (api.ContractMetadata, error) - AddRenewedContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID) (api.ContractMetadata, error) + AddContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) + AddRenewedContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) AncestorContracts(ctx context.Context, id types.FileContractID, minStartHeight uint64) ([]api.ArchivedContract, error) ArchiveContracts(ctx context.Context, toArchive map[types.FileContractID]string) error ContractSetContracts(ctx context.Context, set string) ([]api.ContractMetadata, error) diff --git a/autopilot/contractor.go b/autopilot/contractor.go index 9871b0122..a75e893a5 100644 --- a/autopilot/contractor.go +++ b/autopilot/contractor.go @@ -1335,7 +1335,7 @@ func (c *contractor) renewContract(ctx context.Context, w Worker, ci contractInf *budget = budget.Sub(renterFunds) // persist the contract - renewedContract, err := c.ap.bus.AddRenewedContract(ctx, newRevision, renterFunds, cs.BlockHeight, fcid) + renewedContract, err := c.ap.bus.AddRenewedContract(ctx, newRevision, renterFunds, cs.BlockHeight, fcid, api.ContractStatePending) if err != nil { c.logger.Errorw(fmt.Sprintf("renewal failed to persist, err: %v", err), "hk", hk, "fcid", fcid) return api.ContractMetadata{}, false, err @@ -1423,7 +1423,7 @@ func (c *contractor) refreshContract(ctx context.Context, w Worker, ci contractI *budget = budget.Sub(renterFunds) // persist the contract - refreshedContract, err := c.ap.bus.AddRenewedContract(ctx, newRevision, renterFunds, cs.BlockHeight, contract.ID) + refreshedContract, err := c.ap.bus.AddRenewedContract(ctx, newRevision, renterFunds, cs.BlockHeight, contract.ID, api.ContractStatePending) if err != nil { c.logger.Errorw(fmt.Sprintf("refresh failed, err: %v", err), "hk", hk, "fcid", fcid) return api.ContractMetadata{}, false, err @@ -1495,7 +1495,7 @@ func (c *contractor) formContract(ctx context.Context, w Worker, host hostdb.Hos *budget = budget.Sub(renterFunds) // persist contract in store - formedContract, err := c.ap.bus.AddContract(ctx, contract, renterFunds, cs.BlockHeight) + formedContract, err := c.ap.bus.AddContract(ctx, contract, renterFunds, cs.BlockHeight, api.ContractStatePending) if err != nil { c.logger.Errorw(fmt.Sprintf("contract formation failed, err: %v", err), "hk", hk) return api.ContractMetadata{}, true, err diff --git a/bus/bus.go b/bus/bus.go index d44d5e6d9..8258990cc 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -108,8 +108,8 @@ type ( // A MetadataStore stores information about contracts and objects. MetadataStore interface { - AddContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64) (api.ContractMetadata, error) - AddRenewedContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID) (api.ContractMetadata, error) + AddContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) + AddRenewedContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) AncestorContracts(ctx context.Context, fcid types.FileContractID, minStartHeight uint64) ([]api.ArchivedContract, error) ArchiveContract(ctx context.Context, id types.FileContractID, reason string) error ArchiveContracts(ctx context.Context, toArchive map[types.FileContractID]string) error @@ -906,8 +906,11 @@ func (b *bus) contractIDHandlerPOST(jc jape.Context) { http.Error(jc.ResponseWriter, "TotalCost can not be zero", http.StatusBadRequest) return } + if req.State == "" { + req.State = api.ContractStatePending + } - a, err := b.ms.AddContract(jc.Request.Context(), req.Contract, req.TotalCost, req.StartHeight) + a, err := b.ms.AddContract(jc.Request.Context(), req.Contract, req.TotalCost, req.StartHeight, req.State) if jc.Check("couldn't store contract", err) == nil { jc.Encode(a) } @@ -927,8 +930,11 @@ func (b *bus) contractIDRenewedHandlerPOST(jc jape.Context) { http.Error(jc.ResponseWriter, "TotalCost can not be zero", http.StatusBadRequest) return } + if req.State == "" { + req.State = api.ContractStatePending + } - r, err := b.ms.AddRenewedContract(jc.Request.Context(), req.Contract, req.TotalCost, req.StartHeight, req.RenewedFrom) + r, err := b.ms.AddRenewedContract(jc.Request.Context(), req.Contract, req.TotalCost, req.StartHeight, req.RenewedFrom, req.State) if jc.Check("couldn't store contract", err) == nil { jc.Encode(r) } diff --git a/bus/client/contracts.go b/bus/client/contracts.go index 540cba440..1ed5f2540 100644 --- a/bus/client/contracts.go +++ b/bus/client/contracts.go @@ -13,21 +13,23 @@ import ( ) // AddContract adds the provided contract to the metadata store. -func (c *Client) AddContract(ctx context.Context, contract rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64) (added api.ContractMetadata, err error) { +func (c *Client) AddContract(ctx context.Context, contract rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, state string) (added api.ContractMetadata, err error) { err = c.c.WithContext(ctx).POST(fmt.Sprintf("/contract/%s", contract.ID()), api.ContractsIDAddRequest{ Contract: contract, StartHeight: startHeight, + State: state, TotalCost: totalCost, }, &added) return } // AddRenewedContract adds the provided contract to the metadata store. -func (c *Client) AddRenewedContract(ctx context.Context, contract rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID) (renewed api.ContractMetadata, err error) { +func (c *Client) AddRenewedContract(ctx context.Context, contract rhpv2.ContractRevision, totalCost 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.ContractsIDRenewedRequest{ Contract: contract, RenewedFrom: renewedFrom, StartHeight: startHeight, + State: state, TotalCost: totalCost, }, &renewed) return diff --git a/internal/testing/cluster_test.go b/internal/testing/cluster_test.go index 9822320b2..6b3d7cf80 100644 --- a/internal/testing/cluster_test.go +++ b/internal/testing/cluster_test.go @@ -120,6 +120,9 @@ func TestNewTestCluster(t *testing.T) { if contracts[0].ProofHeight != 0 { return errors.New("proof height should be 0 since the contract was renewed and therefore doesn't require a proof") } + if contracts[0].State != api.ContractStatePending { + return fmt.Errorf("contract should be pending but was %v", contracts[0].State) + } return nil }) @@ -134,6 +137,7 @@ func TestNewTestCluster(t *testing.T) { } // Now wait for the revision and proof to be caught by the hostdb. + var ac api.ArchivedContract tt.Retry(20, time.Second, func() error { cluster.MineBlocks(1) @@ -149,13 +153,16 @@ func TestNewTestCluster(t *testing.T) { if len(archivedContracts) != 1 { return fmt.Errorf("should have 1 archived contract but got %v", len(archivedContracts)) } - ac := archivedContracts[0] + ac = archivedContracts[0] if ac.RevisionHeight == 0 || ac.RevisionNumber != math.MaxUint64 { return fmt.Errorf("revision information is wrong: %v %v", ac.RevisionHeight, ac.RevisionNumber) } if ac.ProofHeight != 0 { t.Fatal("proof height should be 0 since the contract was renewed and therefore doesn't require a proof") } + if ac.State != api.ContractStateComplete { + return fmt.Errorf("contract should be complete but was %v", ac.State) + } archivedContracts, err = cluster.Bus.AncestorContracts(context.Background(), contracts[0].ID, math.MaxUint32) if err != nil { t.Fatal(err) @@ -165,7 +172,6 @@ func TestNewTestCluster(t *testing.T) { } return nil }) - tt.OK(err) // Get host info for every host. hosts, err := cluster.Bus.Hosts(context.Background(), api.GetHostsOptions{}) @@ -1246,11 +1252,11 @@ func TestUploadDownloadSameHost(t *testing.T) { // form 2 more contracts with the same host rev2, _, err := cluster.Worker.RHPForm(context.Background(), c.WindowStart, c.HostKey, c.HostIP, wallet.Address, c.RenterFunds(), c.Revision.ValidHostPayout()) tt.OK(err) - c2, err := cluster.Bus.AddContract(context.Background(), rev2, c.TotalCost, c.StartHeight) + c2, err := cluster.Bus.AddContract(context.Background(), rev2, c.TotalCost, c.StartHeight, api.ContractStatePending) tt.OK(err) rev3, _, err := cluster.Worker.RHPForm(context.Background(), c.WindowStart, c.HostKey, c.HostIP, wallet.Address, c.RenterFunds(), c.Revision.ValidHostPayout()) tt.OK(err) - c3, err := cluster.Bus.AddContract(context.Background(), rev3, c.TotalCost, c.StartHeight) + c3, err := cluster.Bus.AddContract(context.Background(), rev3, c.TotalCost, c.StartHeight, api.ContractStatePending) tt.OK(err) // create a contract set with all 3 contracts diff --git a/stores/hostdb.go b/stores/hostdb.go index 6f695d143..1879cf1c0 100644 --- a/stores/hostdb.go +++ b/stores/hostdb.go @@ -928,25 +928,6 @@ func (ss *SQLStore) processConsensusChangeHostDB(cc modules.ConsensusChange) { ss.unappliedHostKeys[hostKey] = struct{}{} }) } - - // Update RevisionHeight and RevisionNumber for our contracts. - for _, txn := range sb.Transactions { - for _, rev := range txn.FileContractRevisions { - if ss.isKnownContract(types.FileContractID(rev.ParentID)) { - ss.unappliedRevisions[types.FileContractID(rev.ParentID)] = revisionUpdate{ - height: height, - number: rev.NewRevisionNumber, - size: rev.NewFileSize, - } - } - } - // Get ProofHeight for our contracts. - for _, sp := range txn.StorageProofs { - if ss.isKnownContract(types.FileContractID(sp.ParentID)) { - ss.unappliedProofs[types.FileContractID(sp.ParentID)] = height - } - } - } height++ } @@ -1043,6 +1024,21 @@ func applyRevisionUpdate(db *gorm.DB, fcid types.FileContractID, rev revisionUpd }) } +func updateContractState(db *gorm.DB, fcid types.FileContractID, cs contractState) error { + return updateActiveAndArchivedContract(db, fcid, map[string]interface{}{ + "state": cs, + }) +} + +func markFailedContracts(db *gorm.DB, height uint64) error { + if err := db.Model(&dbContract{}). + Where("state = ? AND ? > window_end", contractStateActive, height). + Update("state", contractStateFailed).Error; err != nil { + return fmt.Errorf("failed to mark failed contracts: %w", err) + } + return nil +} + func updateProofHeight(db *gorm.DB, fcid types.FileContractID, blockHeight uint64) error { return updateActiveAndArchivedContract(db, fcid, map[string]interface{}{ "proof_height": blockHeight, diff --git a/stores/metadata.go b/stores/metadata.go index 2014c9197..d04589eec 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -13,6 +13,7 @@ import ( "go.sia.tech/core/types" "go.sia.tech/renterd/api" "go.sia.tech/renterd/object" + "go.sia.tech/siad/modules" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -29,7 +30,17 @@ var ( errShardRootChanged = errors.New("shard root changed") ) +const ( + contractStateInvalid contractState = iota + contractStatePending + contractStateActive + contractStateComplete + contractStateFailed +) + type ( + contractState uint8 + dbArchivedContract struct { Model @@ -53,6 +64,7 @@ type ( FCID fileContractID `gorm:"unique;index;NOT NULL;column:fcid;size:32"` RenewedFrom fileContractID `gorm:"index;size:32"` + State contractState `gorm:"index;NOT NULL;default:0"` TotalCost currency ProofHeight uint64 `gorm:"index;default:0"` RevisionHeight uint64 `gorm:"index;default:0"` @@ -202,6 +214,41 @@ type ( } ) +func (s *contractState) LoadString(state string) error { + switch strings.ToLower(state) { + case api.ContractStateInvalid: + *s = contractStateInvalid + case api.ContractStatePending: + *s = contractStatePending + case api.ContractStateActive: + *s = contractStateActive + case api.ContractStateComplete: + *s = contractStateComplete + case api.ContractStateFailed: + *s = contractStateFailed + default: + *s = contractStateInvalid + } + return nil +} + +func (s contractState) String() string { + switch s { + case contractStateInvalid: + return api.ContractStateInvalid + case contractStatePending: + return api.ContractStatePending + case contractStateActive: + return api.ContractStateActive + case contractStateComplete: + return api.ContractStateComplete + case contractStateFailed: + return api.ContractStateFailed + default: + return api.ContractStateUnknown + } +} + // TableName implements the gorm.Tabler interface. func (dbArchivedContract) TableName() string { return "archived_contracts" } @@ -249,6 +296,7 @@ func (c dbArchivedContract) convert() api.ArchivedContract { RevisionNumber: revisionNumber, Size: c.Size, StartHeight: c.StartHeight, + State: c.State.String(), WindowStart: c.WindowStart, WindowEnd: c.WindowEnd, @@ -286,6 +334,7 @@ func (c dbContract) convert() api.ContractMetadata { RevisionNumber: revisionNumber, Size: c.Size, StartHeight: c.StartHeight, + State: c.State.String(), WindowStart: c.WindowStart, WindowEnd: c.WindowEnd, } @@ -631,10 +680,14 @@ func (s *SQLStore) SlabBuffers(ctx context.Context) ([]api.SlabBuffer, error) { return buffers, nil } -func (s *SQLStore) AddContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64) (_ api.ContractMetadata, err error) { +func (s *SQLStore) AddContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, state string) (_ api.ContractMetadata, err error) { + var cs contractState + if err := cs.LoadString(state); err != nil { + return api.ContractMetadata{}, err + } var added dbContract if err = s.retryTransaction(func(tx *gorm.DB) error { - added, err = addContract(tx, c, totalCost, startHeight, types.FileContractID{}) + added, err = addContract(tx, c, totalCost, startHeight, types.FileContractID{}, cs) return err }); err != nil { return @@ -666,7 +719,12 @@ func (s *SQLStore) Contracts(ctx context.Context) ([]api.ContractMetadata, error // 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, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID) (api.ContractMetadata, error) { +func (s *SQLStore) AddRenewedContract(ctx context.Context, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) { + var cs contractState + if err := cs.LoadString(state); err != nil { + return api.ContractMetadata{}, err + } + var renewed dbContract if err := s.retryTransaction(func(tx *gorm.DB) error { // Fetch contract we renew from. @@ -688,7 +746,7 @@ func (s *SQLStore) AddRenewedContract(ctx context.Context, c rhpv2.ContractRevis } // Overwrite the old contract with the new one. - newContract := newContract(oldContract.HostID, c.ID(), renewedFrom, totalCost, startHeight, c.Revision.WindowStart, c.Revision.WindowEnd, oldContract.Size) + newContract := newContract(oldContract.HostID, c.ID(), renewedFrom, totalCost, startHeight, c.Revision.WindowStart, c.Revision.WindowEnd, oldContract.Size, cs) newContract.Model = oldContract.Model newContract.CreatedAt = time.Now() err = tx.Save(&newContract).Error @@ -2059,7 +2117,7 @@ func contractsForHost(tx *gorm.DB, host dbHost) (contracts []dbContract, err err return } -func newContract(hostID uint, fcid, renewedFrom types.FileContractID, totalCost types.Currency, startHeight, windowStart, windowEnd, size uint64) dbContract { +func newContract(hostID uint, fcid, renewedFrom types.FileContractID, totalCost types.Currency, startHeight, windowStart, windowEnd, size uint64, state contractState) dbContract { return dbContract{ HostID: hostID, @@ -2067,6 +2125,7 @@ func newContract(hostID uint, fcid, renewedFrom types.FileContractID, totalCost FCID: fileContractID(fcid), RenewedFrom: fileContractID(renewedFrom), + State: state, TotalCost: currency(totalCost), RevisionNumber: "0", Size: size, @@ -2084,7 +2143,7 @@ func newContract(hostID uint, fcid, renewedFrom types.FileContractID, totalCost } // addContract adds a contract to the store. -func addContract(tx *gorm.DB, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID) (dbContract, error) { +func addContract(tx *gorm.DB, c rhpv2.ContractRevision, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID, state contractState) (dbContract, error) { fcid := c.ID() // Find host. @@ -2096,7 +2155,7 @@ func addContract(tx *gorm.DB, c rhpv2.ContractRevision, totalCost types.Currency } // Create contract. - contract := newContract(host.ID, fcid, renewedFrom, totalCost, startHeight, c.Revision.WindowStart, c.Revision.WindowEnd, c.Revision.Filesize) + contract := newContract(host.ID, fcid, renewedFrom, totalCost, startHeight, c.Revision.WindowStart, c.Revision.WindowEnd, c.Revision.Filesize, state) // Insert contract. err = tx.Create(&contract).Error @@ -2299,3 +2358,92 @@ func (s *SQLStore) ListObjects(ctx context.Context, bucket, prefix, marker strin Objects: objects, }, nil } + +func (ss *SQLStore) processConsensusChangeContracts(cc modules.ConsensusChange) { + height := uint64(cc.InitialHeight()) + for _, sb := range cc.RevertedBlocks { + var b types.Block + convertToCore(sb, (*types.V1Block)(&b)) + + // revert contracts that got reorged to "pending". + for _, txn := range b.Transactions { + // handle contracts + for i := range txn.FileContracts { + fcid := txn.FileContractID(i) + if ss.isKnownContract(fcid) { + ss.unappliedContractState[fcid] = contractStatePending // revert from 'active' to 'pending' + ss.logger.Infow("contract state changed: active -> pending", + "fcid", fcid, + "reason", "contract reverted") + } + } + // handle contract revision + for _, rev := range txn.FileContractRevisions { + if ss.isKnownContract(rev.ParentID) { + if rev.RevisionNumber == math.MaxUint64 && rev.Filesize == 0 { + ss.unappliedContractState[rev.ParentID] = contractStateActive // revert from 'complete' to 'active' + ss.logger.Infow("contract state changed: complete -> active", + "fcid", rev.ParentID, + "reason", "final revision reverted") + } + } + } + // handle storage proof + for _, sp := range txn.StorageProofs { + if ss.isKnownContract(sp.ParentID) { + ss.unappliedContractState[sp.ParentID] = contractStateActive // revert from 'complete' to 'active' + ss.logger.Infow("contract state changed: complete -> active", + "fcid", sp.ParentID, + "reason", "storage proof reverted") + } + } + } + height-- + } + + for _, sb := range cc.AppliedBlocks { + var b types.Block + convertToCore(sb, (*types.V1Block)(&b)) + + // Update RevisionHeight and RevisionNumber for our contracts. + for _, txn := range b.Transactions { + // handle contracts + for i := range txn.FileContracts { + fcid := txn.FileContractID(i) + if ss.isKnownContract(fcid) { + ss.unappliedContractState[fcid] = contractStateActive // 'pending' -> 'active' + ss.logger.Infow("contract state changed: pending -> active", + "fcid", fcid, + "reason", "contract confirmed") + } + } + // handle contract revision + for _, rev := range txn.FileContractRevisions { + if ss.isKnownContract(rev.ParentID) { + ss.unappliedRevisions[types.FileContractID(rev.ParentID)] = revisionUpdate{ + height: height, + number: rev.RevisionNumber, + size: rev.Filesize, + } + if rev.RevisionNumber == math.MaxUint64 && rev.Filesize == 0 { + ss.unappliedContractState[rev.ParentID] = contractStateComplete // renewed: 'active' -> 'complete' + ss.logger.Infow("contract state changed: active -> complete", + "fcid", rev.ParentID, + "reason", "final revision confirmed") + } + } + } + // handle storage proof + for _, sp := range txn.StorageProofs { + if ss.isKnownContract(sp.ParentID) { + ss.unappliedProofs[sp.ParentID] = height + ss.unappliedContractState[sp.ParentID] = contractStateComplete // storage proof: 'active' -> 'complete' + ss.logger.Infow("contract state changed: active -> complete", + "fcid", sp.ParentID, + "reason", "storage proof confirmed") + } + } + } + height++ + } +} diff --git a/stores/metadata_test.go b/stores/metadata_test.go index 735cc8066..e2e24ab1a 100644 --- a/stores/metadata_test.go +++ b/stores/metadata_test.go @@ -231,7 +231,7 @@ func TestSQLContractStore(t *testing.T) { // Insert it. totalCost := types.NewCurrency64(456) startHeight := uint64(100) - returned, err := ss.AddContract(ctx, c, totalCost, startHeight) + returned, err := ss.AddContract(ctx, c, totalCost, startHeight, api.ContractStatePending) if err != nil { t.Fatal(err) } @@ -240,6 +240,7 @@ func TestSQLContractStore(t *testing.T) { HostIP: "address", HostKey: hk, StartHeight: 100, + State: api.ContractStatePending, WindowStart: 400, WindowEnd: 500, RenewedFrom: types.FileContractID{}, @@ -485,7 +486,7 @@ func TestRenewedContract(t *testing.T) { oldContractTotal := types.NewCurrency64(111) oldContractStartHeight := uint64(100) ctx := context.Background() - added, err := ss.AddContract(ctx, c, oldContractTotal, oldContractStartHeight) + added, err := ss.AddContract(ctx, c, oldContractTotal, oldContractStartHeight, api.ContractStatePending) if err != nil { t.Fatal(err) } @@ -499,7 +500,7 @@ func TestRenewedContract(t *testing.T) { c2 := c c2.Revision.ParentID = fcid2 c2.Revision.UnlockConditions = uc2 - _, err = ss.AddContract(ctx, c2, oldContractTotal, oldContractStartHeight) + _, err = ss.AddContract(ctx, c2, oldContractTotal, oldContractStartHeight, api.ContractStatePending) if err != nil { t.Fatal(err) } @@ -588,7 +589,7 @@ func TestRenewedContract(t *testing.T) { } newContractTotal := types.NewCurrency64(222) newContractStartHeight := uint64(200) - if _, err := ss.AddRenewedContract(ctx, rev, newContractTotal, newContractStartHeight, fcid1); err != nil { + if _, err := ss.AddRenewedContract(ctx, rev, newContractTotal, newContractStartHeight, fcid1, api.ContractStatePending); err != nil { t.Fatal(err) } @@ -640,6 +641,7 @@ func TestRenewedContract(t *testing.T) { StartHeight: newContractStartHeight, RenewedFrom: fcid1, Size: rhpv2.SectorSize, + State: api.ContractStatePending, Spending: api.ContractSpending{ Uploads: types.ZeroCurrency, Downloads: types.ZeroCurrency, @@ -678,6 +680,7 @@ func TestRenewedContract(t *testing.T) { WindowStart: 2, WindowEnd: 3, Size: rhpv2.SectorSize, + State: contractStatePending, UploadSpending: currency(types.Siacoins(1)), DownloadSpending: currency(types.Siacoins(2)), @@ -706,7 +709,7 @@ func TestRenewedContract(t *testing.T) { newContractStartHeight = uint64(300) // Assert the renewed contract is returned - renewedContract, err := ss.AddRenewedContract(ctx, rev, newContractTotal, newContractStartHeight, fcid1Renewed) + renewedContract, err := ss.AddRenewedContract(ctx, rev, newContractTotal, newContractStartHeight, fcid1Renewed, api.ContractStatePending) if err != nil { t.Fatal(err) } @@ -755,6 +758,7 @@ func TestAncestorsContracts(t *testing.T) { RenewedTo: fcids[len(fcids)-1-i], StartHeight: 2, Size: 4096, + State: api.ContractStatePending, WindowStart: 400, WindowEnd: 500, }) { @@ -835,12 +839,12 @@ func (s *SQLStore) addTestContracts(keys []types.PublicKey) (fcids []types.FileC func (s *SQLStore) addTestContract(fcid types.FileContractID, hk types.PublicKey) (api.ContractMetadata, error) { rev := testContractRevision(fcid, hk) - return s.AddContract(context.Background(), rev, types.ZeroCurrency, 0) + return s.AddContract(context.Background(), rev, types.ZeroCurrency, 0, api.ContractStatePending) } func (s *SQLStore) addTestRenewedContract(fcid, renewedFrom types.FileContractID, hk types.PublicKey, startHeight uint64) (api.ContractMetadata, error) { rev := testContractRevision(fcid, hk) - return s.AddRenewedContract(context.Background(), rev, types.ZeroCurrency, startHeight, renewedFrom) + return s.AddRenewedContract(context.Background(), rev, types.ZeroCurrency, startHeight, renewedFrom, api.ContractStatePending) } func (s *SQLStore) contractsCount() (cnt int64, err error) { @@ -1068,6 +1072,7 @@ func TestSQLMetadataStore(t *testing.T) { WindowStart: 400, WindowEnd: 500, Size: 4096, + State: contractStatePending, UploadSpending: zeroCurrency, DownloadSpending: zeroCurrency, @@ -1106,6 +1111,7 @@ func TestSQLMetadataStore(t *testing.T) { WindowStart: 400, WindowEnd: 500, Size: 4096, + State: contractStatePending, UploadSpending: zeroCurrency, DownloadSpending: zeroCurrency, @@ -3482,7 +3488,7 @@ func TestMarkSlabUploadedAfterRenew(t *testing.T) { }, }, } - _, err = ss.AddRenewedContract(context.Background(), rev, types.NewCurrency64(1), 100, fcid) + _, err = ss.AddRenewedContract(context.Background(), rev, types.NewCurrency64(1), 100, fcid, api.ContractStatePending) if err != nil { t.Fatal(err) } diff --git a/stores/migrations.go b/stores/migrations.go index 44845079b..01a5424b8 100644 --- a/stores/migrations.go +++ b/stores/migrations.go @@ -285,6 +285,12 @@ func performMigrations(db *gorm.DB, logger *zap.SugaredLogger) error { return performMigration00024_slabIndices(tx, logger) }, }, + { + ID: "00025_contractState", + Migrate: func(tx *gorm.DB) error { + return performMigration00025_contractState(tx, logger) + }, + }, } // Create migrator. m := gormigrate.New(db, gormigrate.DefaultOptions, migrations) @@ -1113,3 +1119,38 @@ func performMigration00024_slabIndices(txn *gorm.DB, logger *zap.SugaredLogger) logger.Info("migration 00024_slabIndices complete") return nil } + +func performMigration00025_contractState(txn *gorm.DB, logger *zap.SugaredLogger) error { + logger.Info("performing migration 00025_contractState") + // create column + if !txn.Migrator().HasColumn(&dbContract{}, "State") { + if err := txn.Migrator().AddColumn(&dbContract{}, "State"); err != nil { + return err + } + } + if !txn.Migrator().HasColumn(&dbArchivedContract{}, "State") { + if err := txn.Migrator().AddColumn(&dbArchivedContract{}, "State"); err != nil { + return err + } + } + // update column + if err := txn.Model(&dbContract{}).Where("TRUE").Update("State", contractStateActive).Error; err != nil { + return err + } + if err := txn.Model(&dbArchivedContract{}).Where("TRUE").Update("State", contractStateComplete).Error; err != nil { + return err + } + // create index + if !txn.Migrator().HasIndex(&dbContract{}, "State") { + if err := txn.Migrator().CreateIndex(&dbContract{}, "State"); err != nil { + return err + } + } + if !txn.Migrator().HasIndex(&dbArchivedContract{}, "State") { + if err := txn.Migrator().CreateIndex(&dbArchivedContract{}, "State"); err != nil { + return err + } + } + logger.Info("migration 00025_contractState complete") + return nil +} diff --git a/stores/sql.go b/stores/sql.go index 6d039822e..c496bcf9f 100644 --- a/stores/sql.go +++ b/stores/sql.go @@ -50,6 +50,7 @@ type ( persistMu sync.Mutex persistTimer *time.Timer unappliedAnnouncements []announcement + unappliedContractState map[types.FileContractID]contractState unappliedHostKeys map[types.PublicKey]struct{} unappliedRevisions map[types.FileContractID]revisionUpdate unappliedProofs map[types.FileContractID]uint64 @@ -197,18 +198,19 @@ func NewSQLStore(conn gorm.Dialector, alerts alerts.Alerter, partialSlabDir stri } ss := &SQLStore{ - alerts: alerts, - db: db, - logger: l, - knownContracts: isOurContract, - lastSave: time.Now(), - persistInterval: persistInterval, - hasAllowlist: allowlistCnt > 0, - hasBlocklist: blocklistCnt > 0, - settings: make(map[string]string), - unappliedHostKeys: make(map[types.PublicKey]struct{}), - unappliedRevisions: make(map[types.FileContractID]revisionUpdate), - unappliedProofs: make(map[types.FileContractID]uint64), + alerts: alerts, + db: db, + logger: l, + knownContracts: isOurContract, + lastSave: time.Now(), + persistInterval: persistInterval, + hasAllowlist: allowlistCnt > 0, + hasBlocklist: blocklistCnt > 0, + settings: make(map[string]string), + unappliedContractState: make(map[types.FileContractID]contractState), + unappliedHostKeys: make(map[types.PublicKey]struct{}), + unappliedRevisions: make(map[types.FileContractID]revisionUpdate), + unappliedProofs: make(map[types.FileContractID]uint64), announcementMaxAge: announcementMaxAge, @@ -304,6 +306,7 @@ func (ss *SQLStore) ProcessConsensusChange(cc modules.ConsensusChange) { defer ss.persistMu.Unlock() ss.processConsensusChangeHostDB(cc) + ss.processConsensusChangeContracts(cc) ss.processConsensusChangeWallet(cc) // Update consensus fields. @@ -349,7 +352,8 @@ func (ss *SQLStore) applyUpdates(force bool) (err error) { softLimitReached := len(ss.unappliedAnnouncements) >= announcementBatchSoftLimit // enough announcements have accumulated unappliedRevisionsOrProofs := len(ss.unappliedRevisions) > 0 || len(ss.unappliedProofs) > 0 // enough revisions/proofs have accumulated unappliedOutputsOrTxns := len(ss.unappliedOutputChanges) > 0 || len(ss.unappliedTxnChanges) > 0 // enough outputs/txns have accumualted - if !force && !persistIntervalPassed && !softLimitReached && !unappliedRevisionsOrProofs && !unappliedOutputsOrTxns { + unappliedContractState := len(ss.unappliedContractState) > 0 // the chain state of a contract changed + if !force && !persistIntervalPassed && !softLimitReached && !unappliedRevisionsOrProofs && !unappliedOutputsOrTxns && !unappliedContractState { return nil } @@ -414,9 +418,18 @@ func (ss *SQLStore) applyUpdates(force bool) (err error) { return fmt.Errorf("%w; failed to apply unapplied txn change", err) } } + for fcid, cs := range ss.unappliedContractState { + if err := updateContractState(tx, fcid, cs); err != nil { + return fmt.Errorf("%w; failed to update chain state", err) + } + } + if err := markFailedContracts(tx, ss.chainIndex.Height); err != nil { + return err + } return updateCCID(tx, ss.ccid, ss.chainIndex) }) + ss.unappliedContractState = make(map[types.FileContractID]contractState) ss.unappliedProofs = make(map[types.FileContractID]uint64) ss.unappliedRevisions = make(map[types.FileContractID]revisionUpdate) ss.unappliedHostKeys = make(map[types.PublicKey]struct{})