diff --git a/bus/bus.go b/bus/bus.go index dbb3ebaa0..56c846c18 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -687,9 +687,6 @@ func (b *Bus) formContract(ctx context.Context, hostSettings rhpv2.HostSettings, return api.ContractMetadata{}, fmt.Errorf("couldn't add transaction set to the pool: %w", err) } - // broadcast the transaction set - go b.s.BroadcastTransactionSet(txnSet) - return api.ContractMetadata{ ID: contract.ID(), HostKey: contract.HostKey(), @@ -727,9 +724,6 @@ func (b *Bus) formContractV2(ctx context.Context, hk types.PublicKey, hostIP str return api.ContractMetadata{}, fmt.Errorf("failed to add v2 transaction set to the pool: %w", err) } - // broadcast the transaction set - go b.s.BroadcastV2TransactionSet(res.FormationSet.Basis, res.FormationSet.Transactions) - contract := res.Contract return api.ContractMetadata{ ID: contract.ID, @@ -805,14 +799,11 @@ func (b *Bus) renewContractV1(ctx context.Context, cs consensus.State, gp api.Go // renew contract gc := gouging.NewChecker(gp.GougingSettings, gp.ConsensusState) prepareRenew := b.prepareRenew(cs, rev, hs.Address, b.w.Address(), renterFunds, minNewCollateral, endHeight, expectedNewStorage) - newRevision, txnSet, contractPrice, fundAmount, err := b.rhp3Client.Renew(ctx, gc, rev, renterKey, c.HostKey, hs.SiamuxAddr(), prepareRenew, b.w.SignTransaction) + newRevision, _, contractPrice, fundAmount, err := b.rhp3Client.Renew(ctx, gc, rev, renterKey, c.HostKey, hs.SiamuxAddr(), prepareRenew, b.w.SignTransaction) if err != nil { return api.ContractMetadata{}, err } - // broadcast the transaction set - b.s.BroadcastTransactionSet(txnSet) - return api.ContractMetadata{ ID: newRevision.ID(), HostKey: newRevision.HostKey(), @@ -863,7 +854,6 @@ func (b *Bus) renewContractV2(ctx context.Context, cs consensus.State, h api.Hos } var contract cRhp4.ContractRevision - var txnSet cRhp4.TransactionSet if c.EndHeight() == endHeight { // when refreshing, the 'collateral' is added on top of the existing // collateral so we account for that by subtracting the rolled over @@ -880,7 +870,6 @@ func (b *Bus) renewContractV2(ctx context.Context, cs consensus.State, h api.Hos Collateral: collateral, }) contract = res.Contract - txnSet = res.RenewalSet } else { var res cRhp4.RPCRenewContractResult res, err = b.rhp4Client.RenewContract(ctx, h.PublicKey, h.V2SiamuxAddr(), b.cm, signer, cs, settings.Prices, rev, rhpv4.RPCRenewContractParams{ @@ -890,15 +879,11 @@ func (b *Bus) renewContractV2(ctx context.Context, cs consensus.State, h api.Hos ProofHeight: endHeight, }) contract = res.Contract - txnSet = res.RenewalSet } if err != nil { return api.ContractMetadata{}, err } - // broadcast the transaction set - b.s.BroadcastV2TransactionSet(txnSet.Basis, txnSet.Transactions) - return api.ContractMetadata{ ID: contract.ID, HostKey: h.PublicKey, diff --git a/internal/bus/chainsubscriber.go b/internal/bus/chainsubscriber.go index 5df3edfcb..23da73cfa 100644 --- a/internal/bus/chainsubscriber.go +++ b/internal/bus/chainsubscriber.go @@ -13,7 +13,6 @@ import ( rhp4 "go.sia.tech/coreutils/rhp/v4" "go.sia.tech/coreutils/wallet" "go.sia.tech/renterd/api" - "go.sia.tech/renterd/internal/utils" "go.sia.tech/renterd/stores/sql" "go.sia.tech/renterd/webhooks" "go.uber.org/zap" @@ -91,24 +90,7 @@ type ( syncSig chan struct{} wg sync.WaitGroup - mu sync.Mutex - knownContracts map[types.FileContractID]bool - unsubscribeFn func() - } -) - -type ( - revision struct { - revisionNumber uint64 - fileSize uint64 - } - - contractUpdate struct { - fcid types.FileContractID - prev *revision - curr *revision - resolved bool - valid bool + unsubscribeFn func() } ) @@ -131,8 +113,6 @@ func NewChainSubscriber(whm WebhookManager, cm ChainManager, cs ChainStore, s Sy shutdownCtx: ctx, shutdownCtxCancel: cancel, syncSig: make(chan struct{}, 1), - - knownContracts: make(map[types.FileContractID]bool), } // start the subscriber @@ -216,41 +196,50 @@ func (s *chainSubscriber) applyChainUpdate(tx sql.ChainUpdateTx, cau chain.Apply } // v1 contracts - cus := make(map[types.FileContractID]contractUpdate) - cau.ForEachFileContractElement(func(fce types.FileContractElement, _ bool, rev *types.FileContractElement, resolved, valid bool) { - cus[types.FileContractID(fce.ID)] = v1ContractUpdate(fce, rev, resolved, valid) + var err error + cau.ForEachFileContractElement(func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool) { + if err != nil { + return + } else if known, lookupErr := tx.IsKnownContract(fce.ID); lookupErr != nil { + err = lookupErr + return + } else if !known { + return // only consider known contracts + } + err = s.applyV1ContractUpdate(tx, cau.State.Index, fce, created, rev, resolved, valid) }) + if err != nil { + return fmt.Errorf("failed to apply v1 contract update: %w", err) + } // v2 contracts var revisedContracts []types.V2FileContractElement - cau.ForEachV2FileContractElement(func(fce types.V2FileContractElement, _ bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { + cau.ForEachV2FileContractElement(func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { + if err != nil { + return + } else if known, lookupErr := tx.IsKnownContract(fce.ID); lookupErr != nil { + err = lookupErr + return + } else if !known { + return // only consider known contracts + } if rev == nil { revisedContracts = append(revisedContracts, fce) } else { - revisedContracts = append(revisedContracts, *rev) // revised + revisedContracts = append(revisedContracts, *rev) } - cus[types.FileContractID(fce.ID)] = v2ContractUpdate(fce, rev, res) + err = s.applyV2ContractUpdate(tx, cau.State.Index, fce, created, rev, res) }) - - // updates - this updates the 'known' contracts too so we do this first - for _, cu := range cus { - if err := s.updateContract(tx, cau.State.Index, cu.fcid, cu.prev, cu.curr, cu.resolved, cu.valid); err != nil { - return fmt.Errorf("failed to apply contract updates: %w", err) - } + if err != nil { + return fmt.Errorf("failed to apply v2 contract update: %w", err) } - // new contracts - only consider the ones we are interested in - filtered := revisedContracts[:0] - for _, fce := range revisedContracts { - if s.isKnownContract(fce.ID) { - filtered = append(filtered, fce) - } - } - if err := tx.UpdateFileContractElements(filtered); err != nil { + // update revised contracts + if err := tx.UpdateFileContractElements(revisedContracts); err != nil { return fmt.Errorf("failed to insert v2 file contract elements: %w", err) } - // contract proofs + // update contract proofs if err := tx.UpdateFileContractElementProofs(cau); err != nil { return fmt.Errorf("failed to update file contract element proofs: %w", err) } @@ -275,42 +264,50 @@ func (s *chainSubscriber) revertChainUpdate(tx sql.ChainUpdateTx, cru chain.Reve // NOTE: host updates are not reverted // v1 contracts - var cus []contractUpdate - cru.ForEachFileContractElement(func(fce types.FileContractElement, _ bool, rev *types.FileContractElement, resolved, valid bool) { - cus = append(cus, v1ContractUpdate(fce, rev, resolved, valid)) + var err error + cru.ForEachFileContractElement(func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, _ bool) { + if err != nil { + return + } else if known, lookupErr := tx.IsKnownContract(fce.ID); lookupErr != nil { + err = lookupErr + return + } else if !known { + return // only consider known contracts + } + err = s.revertV1ContractUpdate(tx, fce, created, rev, resolved) }) + if err != nil { + return fmt.Errorf("failed to revert v1 contract update: %w", err) + } // v2 contracts - cus = cus[:0] var revertedContracts []types.V2FileContractElement cru.ForEachV2FileContractElement(func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { - if created { + if err != nil { + return + } else if known, lookupErr := tx.IsKnownContract(fce.ID); lookupErr != nil { + err = lookupErr + return + } else if !known { + return // only consider known contracts + } + if rev == nil { revertedContracts = append(revertedContracts, fce) - } else if rev != nil { + } else { revertedContracts = append(revertedContracts, *rev) } - cus = append(cus, v2ContractUpdate(fce, rev, res)) + err = s.revertV2ContractUpdate(tx, fce, created, rev, res) }) - - // updates - this updates the 'known' contracts too so we do this first - for _, cu := range cus { - if err := s.updateContract(tx, cru.State.Index, cu.fcid, cu.prev, cu.curr, cu.resolved, cu.valid); err != nil { - return fmt.Errorf("failed to revert v2 contract update: %w", err) - } + if err != nil { + return fmt.Errorf("failed to revert v2 contract update: %w", err) } - // reverted contracts - only consider the ones that we are interested in - filtered := revertedContracts[:0] - for _, fce := range revertedContracts { - if s.isKnownContract(fce.ID) { - filtered = append(filtered, fce) - } - } - if err := tx.UpdateFileContractElements(filtered); err != nil { + // update reverted contracts + if err := tx.UpdateFileContractElements(revertedContracts); err != nil { return fmt.Errorf("failed to remove v2 file contract elements: %w", err) } - // contract proofs + // update contract proofs if err := tx.UpdateFileContractElementProofs(cru); err != nil { return fmt.Errorf("failed to update file contract element proofs: %w", err) } @@ -444,199 +441,251 @@ func (s *chainSubscriber) broadcastExpiredFileContractResolutions(tx sql.ChainUp } } -func (s *chainSubscriber) updateContract(tx sql.ChainUpdateTx, index types.ChainIndex, fcid types.FileContractID, prev, curr *revision, resolved, valid bool) error { - // sanity check at least one is not nil - if prev == nil && curr == nil { - return errors.New("both prev and curr revisions are nil") // developer error - } - - // ignore unknown contracts - if !s.isKnownContract(fcid) { - return nil +func (s *chainSubscriber) applyV1ContractUpdate(tx sql.ChainUpdateTx, index types.ChainIndex, fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool) error { + fcid := fce.ID + if rev != nil { + fcid = rev.ID } // fetch contract state state, err := tx.ContractState(fcid) - if err != nil && utils.IsErr(err, api.ErrContractNotFound) { - s.updateKnownContracts(fcid, false) // ignore unknown contracts - return nil - } else if err != nil { + if err != nil { return fmt.Errorf("failed to get contract state: %w", err) - } else { - s.updateKnownContracts(fcid, true) // update known contracts } - // define a helper function to update the contract state - updateState := func(update api.ContractState) (err error) { - if state != update { - err = tx.UpdateContractState(fcid, update) - if err == nil { - state = update - } - } - return - } - - // handle reverts - if prev != nil { - // update state from 'active' -> 'pending' - if curr == nil { - if err := updateState(api.ContractStatePending); err != nil { - return fmt.Errorf("failed to update contract state: %w", err) - } - } - - // reverted renewal: 'complete' -> 'active' - if curr != 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 { - if err := updateState(api.ContractStateActive); err != nil { - return fmt.Errorf("failed to update contract state: %w", err) - } - s.logger.Infow("contract state changed: complete -> active", - "fcid", fcid, - "reason", "final revision reverted") - } - } - - // reverted storage proof: 'complete/failed' -> 'active' - if resolved { - if err := updateState(api.ContractStateActive); err != nil { - return fmt.Errorf("failed to update contract state: %w", err) - } - if valid { - s.logger.Infow("contract state changed: complete -> active", - "fcid", fcid, - "reason", "storage proof reverted") - } else { - s.logger.Infow("contract state changed: failed -> active", - "fcid", fcid, - "reason", "storage proof reverted") - } - } - - return nil + // update revision number and file size + revisionNumber := fce.FileContract.RevisionNumber + fileSize := fce.FileContract.Filesize + if rev != nil { + revisionNumber = rev.FileContract.RevisionNumber + fileSize = rev.FileContract.Filesize } - - // handle apply - if err := tx.UpdateContractRevision(fcid, index.Height, curr.revisionNumber, curr.fileSize); err != nil { + if err := tx.UpdateContractRevision(fcid, index.Height, revisionNumber, fileSize); err != nil { return fmt.Errorf("failed to update contract %v: %w", fcid, err) } - // update state from 'pending' -> 'active' - if state == api.ContractStatePending || state == api.ContractStateUnknown { - if err := updateState(api.ContractStateActive); err != nil { - return fmt.Errorf("failed to update contract state: %w", err) - } - s.logger.Infow("contract state changed: pending -> active", - "fcid", fcid, - "reason", "contract confirmed") + // consider a contract resolved if it has a max revision number and zero + // file size + if rev != nil && rev.FileContract.RevisionNumber == math.MaxUint64 && rev.FileContract.Filesize == 0 { + resolved = true + valid = true } - // storage proof: 'active' -> 'complete/failed' + // contract was resolved via proof or renewal -> 'complete/failed' if resolved { if err := tx.UpdateContractProofHeight(fcid, index.Height); err != nil { return fmt.Errorf("failed to update contract proof height: %w", err) } if valid { - if err := updateState(api.ContractStateComplete); err != nil { + if err := tx.UpdateContractState(fcid, api.ContractStateComplete); err != nil { return fmt.Errorf("failed to update contract state: %w", err) } - s.logger.Infow("contract state changed: active -> complete", + s.logger.Infow(fmt.Sprintf("contract state changed: %s -> failed", state), "fcid", fcid, "reason", "storage proof valid") } else { - if err := updateState(api.ContractStateFailed); err != nil { + if err := tx.UpdateContractState(fcid, api.ContractStateFailed); err != nil { return fmt.Errorf("failed to update contract state: %w", err) } - s.logger.Infow("contract state changed: active -> failed", + s.logger.Infow(fmt.Sprintf("contract state changed: %s -> failed", state), "fcid", fcid, "reason", "storage proof missed") } + return nil } - return nil -} - -func (s *chainSubscriber) isClosed() bool { - select { - case <-s.shutdownCtx.Done(): - return true - default: - } - return false -} -func (s *chainSubscriber) isKnownContract(fcid types.FileContractID) bool { - s.mu.Lock() - defer s.mu.Unlock() - known, ok := s.knownContracts[fcid] - if !ok { - return true // assume known + // contract was created -> 'active' + if created { + if err := tx.UpdateContractState(fcid, api.ContractStateActive); err != nil { + return fmt.Errorf("failed to update contract state: %w", err) + } + s.logger.Infow(fmt.Sprintf("contract state changed: %s -> active", state), + "fcid", fcid, + "reason", "contract confirmed") + return nil } - return known -} -func (s *chainSubscriber) updateKnownContracts(fcid types.FileContractID, known bool) { - s.mu.Lock() - defer s.mu.Unlock() - s.knownContracts[fcid] = known + return nil } -func v1ContractUpdate(fce types.FileContractElement, rev *types.FileContractElement, resolved, valid bool) contractUpdate { - curr := &revision{ - revisionNumber: fce.FileContract.RevisionNumber, - fileSize: fce.FileContract.Filesize, - } +func (s *chainSubscriber) revertV1ContractUpdate(tx sql.ChainUpdateTx, fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved bool) error { + fcid := fce.ID if rev != nil { - curr.revisionNumber = rev.FileContract.RevisionNumber - curr.fileSize = rev.FileContract.Filesize + fcid = rev.ID } - if curr.revisionNumber == math.MaxUint64 && curr.fileSize == 0 { + + // fetch contract state to see if contract is known + state, err := tx.ContractState(fcid) + if err != nil { + return fmt.Errorf("failed to get contract state: %w", err) + } + + // consider a contract resolved if it has a max revision number and zero + // file size + if rev != nil && rev.FileContract.RevisionNumber == math.MaxUint64 && rev.FileContract.Filesize == 0 { resolved = true - valid = true } - return contractUpdate{ - fcid: types.FileContractID(fce.ID), - prev: nil, - curr: curr, - resolved: resolved, - valid: valid, + + // contract was reverted -> 'pending' + if created { + if err := tx.UpdateContractState(fcid, api.ContractStatePending); err != nil { + return fmt.Errorf("failed to update contract state: %w", err) + } + s.logger.Infow(fmt.Sprintf("contract state changed: %s -> active", state), + "fcid", fcid, + "reason", "contract was reverted") + return nil } + + // reverted storage proof -> 'active' + if resolved { + if err := tx.UpdateContractState(fcid, api.ContractStateActive); err != nil { + return fmt.Errorf("failed to update contract state: %w", err) + } + s.logger.Infow(fmt.Sprintf("contract state changed: %s -> active", state), + "fcid", fcid, + "reason", "storage proof reverted") + return nil + } + + return nil } -func v2ContractUpdate(fce types.V2FileContractElement, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) contractUpdate { - curr := &revision{ - revisionNumber: fce.V2FileContract.RevisionNumber, - fileSize: fce.V2FileContract.Filesize, +func (s *chainSubscriber) applyV2ContractUpdate(tx sql.ChainUpdateTx, index types.ChainIndex, fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) error { + fcid := fce.ID + if rev != nil { + fcid = rev.ID } + + // fetch contract state + state, err := tx.ContractState(fcid) + if err != nil { + return fmt.Errorf("failed to get contract state: %w", err) + } + + // update revision number and file size + revisionNumber := fce.V2FileContract.RevisionNumber + fileSize := fce.V2FileContract.Filesize if rev != nil { - curr.revisionNumber = rev.V2FileContract.RevisionNumber - curr.fileSize = rev.V2FileContract.Filesize + revisionNumber = rev.V2FileContract.RevisionNumber + fileSize = rev.V2FileContract.Filesize + } + if err := tx.UpdateContractRevision(fcid, index.Height, revisionNumber, fileSize); err != nil { + return fmt.Errorf("failed to update contract %v: %w", fcid, err) } - var resolved, valid bool + // resolution -> 'complete/failed' if res != nil { - resolved = true + var newState api.ContractState + var reason string switch res.(type) { case *types.V2FileContractRenewal: - // hack to make sure v2 contracts also appear with a max revision - // number after being renewed - curr.revisionNumber = math.MaxUint64 - valid = true + newState = api.ContractStateComplete + reason = "renewal" + + // link the renewed contract to the new one, this should not be + // necessary if the renewal was successfully but there is a slim + // chance that it's not when the renewal was interrupted + if err := tx.RecordContractRenewal(fcid, fcid.V2RenewalID()); err != nil { + return fmt.Errorf("failed to record contract renewal: %w", err) + } + case *types.V2StorageProof: - valid = true + newState = api.ContractStateComplete + reason = "storage proof" case *types.V2FileContractExpiration: - valid = false + newState = api.ContractStateFailed + reason = "expiration" + default: + panic("unknown resolution type") // developer error + } + + // record height of encountering the resolution + if err := tx.UpdateContractProofHeight(fcid, index.Height); err != nil { + return fmt.Errorf("failed to update contract proof height: %w", err) + } + + // record new state + if err := tx.UpdateContractState(fcid, newState); err != nil { + return fmt.Errorf("failed to update contract state: %w", err) } + + s.logger.Infow(fmt.Sprintf("contract state changed: %s -> %s", state, newState), + "fcid", fcid, + "reason", reason) + return nil } - return contractUpdate{ - fcid: types.FileContractID(fce.ID), - prev: nil, - curr: curr, - resolved: resolved, - valid: valid, + // contract was created -> 'active' + if created { + if err := tx.UpdateContractState(fcid, api.ContractStateActive); err != nil { + return fmt.Errorf("failed to update contract state: %w", err) + } + s.logger.Infow(fmt.Sprintf("contract state changed: %s -> active", state), + "fcid", fcid, + "reason", "contract confirmed") + return nil + } + + return nil +} + +func (s *chainSubscriber) revertV2ContractUpdate(tx sql.ChainUpdateTx, fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) error { + fcid := fce.ID + if rev != nil { + fcid = rev.ID + } + + // ignore unknown contracts + if known, err := tx.IsKnownContract(fcid); err != nil { + return err + } else if !known { + return nil + } + + // fetch contract state to see if contract is known + state, err := tx.ContractState(fcid) + if err != nil { + return fmt.Errorf("failed to get contract state: %w", err) + } + + // contract was reverted -> 'pending' + if created { + if err := tx.UpdateContractState(fcid, api.ContractStatePending); err != nil { + return fmt.Errorf("failed to update contract state: %w", err) + } + s.logger.Infow(fmt.Sprintf("contract state changed: %s -> active", state), + "fcid", fcid, + "reason", "contract was reverted") + return nil } + + // reverted resolution -> 'active' + if res != nil { + // reset proof height + if err := tx.UpdateContractProofHeight(fcid, 0); err != nil { + return fmt.Errorf("failed to update contract proof height: %w", err) + } + + // record new state + if err := tx.UpdateContractState(fcid, api.ContractStateActive); err != nil { + return fmt.Errorf("failed to update contract state: %w", err) + } + + s.logger.Infow(fmt.Sprintf("contract state changed: %s -> active", state), + "fcid", fcid, + "reason", "resolution was reverted") + return nil + } + + return nil +} + +func (s *chainSubscriber) isClosed() bool { + select { + case <-s.shutdownCtx.Done(): + return true + default: + } + return false } diff --git a/internal/test/e2e/cluster_test.go b/internal/test/e2e/cluster_test.go index 88cfa5cdd..3ac61ffde 100644 --- a/internal/test/e2e/cluster_test.go +++ b/internal/test/e2e/cluster_test.go @@ -2830,7 +2830,7 @@ func TestContractFundsReturnWhenHostOffline(t *testing.T) { tt.OK(hosts[0].Close()) // mine until the contract is expired - cluster.mineBlocks(types.VoidAddress, contract.WindowEnd-cs.BlockHeight+10) + cluster.mineBlocks(types.VoidAddress, contract.WindowEnd-cs.BlockHeight) expectedBalance := wallet.Confirmed.Add(contract.InitialRenterFunds).Sub(fee.Mul64(ibus.ContractResolutionTxnWeight)) cluster.tt.Retry(10, time.Second, func() error { diff --git a/stores/sql/chain.go b/stores/sql/chain.go index db38a7642..35a91b9d0 100644 --- a/stores/sql/chain.go +++ b/stores/sql/chain.go @@ -204,6 +204,27 @@ func FileContractElement(ctx context.Context, tx sql.Tx, fcid types.FileContract }, nil } +func IsKnownContract(ctx context.Context, tx sql.Tx, fcid types.FileContractID) (known bool, _ error) { + err := tx.QueryRow(ctx, "SELECT EXISTS (SELECT 1 FROM contracts WHERE fcid = ?)", FileContractID(fcid)). + Scan(&known) + if err != nil { + return false, err + } + return known, nil +} + +func RecordContractRenewal(ctx context.Context, tx sql.Tx, oldFCID, newFCID types.FileContractID) error { + _, err := tx.Exec(ctx, "UPDATE contracts SET renewed_to = ? WHERE fcid = ?", FileContractID(newFCID), FileContractID(oldFCID)) + if err != nil { + return fmt.Errorf("failed to update renewed_to of old contract: %w", err) + } + _, err = tx.Exec(ctx, "UPDATE contracts SET renewed_from = ? WHERE fcid = ?", FileContractID(oldFCID), FileContractID(newFCID)) + if err != nil { + return fmt.Errorf("failed to update renewed_from of new contract: %w", err) + } + return nil +} + func PruneFileContractElements(ctx context.Context, tx sql.Tx, threshold uint64) error { _, err := tx.Exec(ctx, ` DELETE FROM contract_elements diff --git a/stores/sql/database.go b/stores/sql/database.go index 35bf0618d..1c5d5935a 100644 --- a/stores/sql/database.go +++ b/stores/sql/database.go @@ -21,7 +21,9 @@ type ( ContractState(fcid types.FileContractID) (api.ContractState, error) ExpiredFileContractElements(bh uint64) ([]types.V2FileContractElement, error) FileContractElement(fcid types.FileContractID) (types.V2FileContractElement, error) + IsKnownContract(fcid types.FileContractID) (bool, error) PruneFileContractElements(threshold uint64) error + RecordContractRenewal(old, new types.FileContractID) error UpdateFileContractElements([]types.V2FileContractElement) error UpdateChainIndex(index types.ChainIndex) error UpdateFileContractElementProofs(updater wallet.ProofUpdater) error diff --git a/stores/sql/mysql/chain.go b/stores/sql/mysql/chain.go index bed8ae9b2..8d854fb27 100644 --- a/stores/sql/mysql/chain.go +++ b/stores/sql/mysql/chain.go @@ -24,9 +24,10 @@ var ( ) type chainUpdateTx struct { - ctx context.Context - tx isql.Tx - l *zap.SugaredLogger + ctx context.Context + tx isql.Tx + l *zap.SugaredLogger + known map[types.FileContractID]bool // map to prevent rare duplicate selects } func (c chainUpdateTx) WalletApplyIndex(index types.ChainIndex, created, spent []types.SiacoinElement, events []wallet.Event, timestamp time.Time) error { @@ -210,10 +211,27 @@ func (c chainUpdateTx) FileContractElement(fcid types.FileContractID) (types.V2F return ssql.FileContractElement(c.ctx, c.tx, fcid) } +func (c chainUpdateTx) IsKnownContract(fcid types.FileContractID) (bool, error) { + if relevant, ok := c.known[fcid]; ok { + return relevant, nil + } + + known, err := ssql.IsKnownContract(c.ctx, c.tx, fcid) + if err != nil { + return false, err + } + c.known[fcid] = known + return known, nil +} + func (c chainUpdateTx) PruneFileContractElements(threshold uint64) error { return ssql.PruneFileContractElements(c.ctx, c.tx, threshold) } +func (c chainUpdateTx) RecordContractRenewal(oldFCID, newFCID types.FileContractID) error { + return ssql.RecordContractRenewal(c.ctx, c.tx, oldFCID, newFCID) +} + func (c chainUpdateTx) UpdateFileContractElements(fces []types.V2FileContractElement) error { contractIDStmt, err := c.tx.Prepare(c.ctx, "SELECT c.id FROM contracts c WHERE c.fcid = ?") if err != nil { diff --git a/stores/sql/mysql/main.go b/stores/sql/mysql/main.go index 8d856fe8c..7dba272ec 100644 --- a/stores/sql/mysql/main.go +++ b/stores/sql/mysql/main.go @@ -654,9 +654,10 @@ func (tx *MainDatabaseTx) Peers(ctx context.Context) ([]syncer.PeerInfo, error) func (tx *MainDatabaseTx) ProcessChainUpdate(ctx context.Context, fn func(ssql.ChainUpdateTx) error) error { return fn(&chainUpdateTx{ - ctx: ctx, - tx: tx, - l: tx.log.Named("ProcessChainUpdate"), + ctx: ctx, + known: make(map[types.FileContractID]bool), + tx: tx, + l: tx.log.Named("ProcessChainUpdate"), }) } diff --git a/stores/sql/sqlite/chain.go b/stores/sql/sqlite/chain.go index e806b8a27..cf642e8fe 100644 --- a/stores/sql/sqlite/chain.go +++ b/stores/sql/sqlite/chain.go @@ -25,9 +25,10 @@ var ( ) type chainUpdateTx struct { - ctx context.Context - tx isql.Tx - l *zap.SugaredLogger + ctx context.Context + tx isql.Tx + l *zap.SugaredLogger + known map[types.FileContractID]bool // map to prevent rare duplicate selects } func (c chainUpdateTx) WalletApplyIndex(index types.ChainIndex, created, spent []types.SiacoinElement, events []wallet.Event, timestamp time.Time) error { @@ -211,10 +212,27 @@ func (c chainUpdateTx) FileContractElement(fcid types.FileContractID) (types.V2F return ssql.FileContractElement(c.ctx, c.tx, fcid) } +func (c chainUpdateTx) IsKnownContract(fcid types.FileContractID) (bool, error) { + if relevant, ok := c.known[fcid]; ok { + return relevant, nil + } + + known, err := ssql.IsKnownContract(c.ctx, c.tx, fcid) + if err != nil { + return false, err + } + c.known[fcid] = known + return known, nil +} + func (c chainUpdateTx) PruneFileContractElements(threshold uint64) error { return ssql.PruneFileContractElements(c.ctx, c.tx, threshold) } +func (c chainUpdateTx) RecordContractRenewal(oldFCID, newFCID types.FileContractID) error { + return ssql.RecordContractRenewal(c.ctx, c.tx, oldFCID, newFCID) +} + func (c chainUpdateTx) UpdateFileContractElements(fces []types.V2FileContractElement) error { contractIDStmt, err := c.tx.Prepare(c.ctx, "SELECT c.id FROM contracts c WHERE c.fcid = ?") if err != nil { diff --git a/stores/sql/sqlite/main.go b/stores/sql/sqlite/main.go index 51fcd5592..0e488c6ed 100644 --- a/stores/sql/sqlite/main.go +++ b/stores/sql/sqlite/main.go @@ -650,9 +650,10 @@ func (tx *MainDatabaseTx) Peers(ctx context.Context) ([]syncer.PeerInfo, error) func (tx *MainDatabaseTx) ProcessChainUpdate(ctx context.Context, fn func(ssql.ChainUpdateTx) error) (err error) { return fn(&chainUpdateTx{ - ctx: ctx, - tx: tx, - l: tx.log.Named("ProcessChainUpdate"), + ctx: ctx, + known: make(map[types.FileContractID]bool), + tx: tx, + l: tx.log.Named("ProcessChainUpdate"), }) }