Skip to content

Commit

Permalink
contracts,sqlite: expose resolution height
Browse files Browse the repository at this point in the history
  • Loading branch information
n8maninger committed Apr 13, 2023
1 parent 446ceb6 commit afd54c6
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 32 deletions.
14 changes: 13 additions & 1 deletion host/contracts/actions.go
Expand Up @@ -76,7 +76,7 @@ func (cm *ContractManager) processActions() {
}

// handleContractAction performs a lifecycle action on a contract.
func (cm *ContractManager) handleContractAction(id types.FileContractID, action string) {
func (cm *ContractManager) handleContractAction(id types.FileContractID, height uint64, action string) {
log := cm.log.Named("lifecycle")
contract, err := cm.store.Contract(id)
if err != nil {
Expand All @@ -90,6 +90,10 @@ func (cm *ContractManager) handleContractAction(id types.FileContractID, action

switch action {
case ActionBroadcastFormation:
if (height-contract.NegotiationHeight)%3 != 0 {
// debounce formation broadcasts to prevent spamming
return
}
formationSet, err := cm.store.ContractFormationSet(id)
if err != nil {
log.Error("failed to get formation set", zap.String("contract", id.String()), zap.Error(err))
Expand All @@ -100,6 +104,10 @@ func (cm *ContractManager) handleContractAction(id types.FileContractID, action
}
log.Info("broadcast formation transaction", zap.String("contract", id.String()), zap.String("transactionID", formationSet[len(formationSet)-1].ID().String()))
case ActionBroadcastFinalRevision:
if (contract.Revision.WindowStart-height)%3 != 0 {
// debounce final revision broadcasts to prevent spamming
return
}
revisionTxn := types.Transaction{
FileContractRevisions: []types.FileContractRevision{contract.Revision},
Signatures: []types.TransactionSignature{
Expand Down Expand Up @@ -134,6 +142,10 @@ func (cm *ContractManager) handleContractAction(id types.FileContractID, action
}
log.Info("broadcast revision transaction", zap.String("contract", id.String()), zap.Uint64("revisionNumber", contract.Revision.RevisionNumber), zap.String("transactionID", revisionTxn.ID().String()))
case ActionBroadcastResolution:
if (height-contract.Revision.WindowStart)%3 != 0 {
// debounce resolution broadcasts to prevent spamming
return
}
validPayout, missedPayout := contract.Revision.ValidHostPayout(), contract.Revision.MissedHostPayout()
if missedPayout.Cmp(validPayout) >= 0 {
log.Info("skipping storage proof, no benefit to host", zap.String("contract", id.String()), zap.String("validPayout", validPayout.ExactString()), zap.String("missedPayout", missedPayout.ExactString()))
Expand Down
7 changes: 4 additions & 3 deletions host/contracts/contracts.go
Expand Up @@ -99,9 +99,10 @@ type (
// RevisionConfirmed is true if the contract revision transaction has
// been confirmed on the blockchain.
RevisionConfirmed bool `json:"revisionConfirmed"`
// ResolutionConfirmed is true if the contract's resolution has been
// confirmed on the blockchain.
ResolutionConfirmed bool `json:"resolutionConfirmed"`
// ResolutionHeight is the height the storage proof was confirmed
// at. If the contract has not been resolved, the field is the zero
// value.
ResolutionHeight uint64 `json:"resolutionHeight"`
// RenewedTo is the ID of the contract that renewed this contract. If
// this contract was not renewed, this field is the zero value.
RenewedTo types.FileContractID `json:"renewedTo"`
Expand Down
22 changes: 16 additions & 6 deletions host/contracts/manager.go
Expand Up @@ -217,8 +217,14 @@ func (cm *ContractManager) ProcessConsensusChange(cc modules.ConsensusChange) {
}
}

var appliedFormations, appliedResolutions []types.FileContractID
var appliedFormations []types.FileContractID
var appliedResolutions []struct {
id types.FileContractID
height uint64
}
appliedRevisions := make(map[types.FileContractID]types.FileContractRevision)
// calculate the block height of the first applied diff
blockHeight := uint64(cc.BlockHeight) - uint64(len(cc.AppliedBlocks))
for _, applied := range cc.AppliedBlocks {
for _, transaction := range applied.Transactions {
for i := range transaction.FileContracts {
Expand All @@ -235,9 +241,13 @@ func (cm *ContractManager) ProcessConsensusChange(cc modules.ConsensusChange) {

for _, proof := range transaction.StorageProofs {
contractID := types.FileContractID(proof.ParentID)
appliedResolutions = append(appliedResolutions, contractID)
appliedResolutions = append(appliedResolutions, struct {
id types.FileContractID
height uint64
}{contractID, blockHeight})
}
}
blockHeight++
}

err = cm.store.UpdateContractState(cc.ID, uint64(cc.BlockHeight), func(tx UpdateStateTransaction) error {
Expand Down Expand Up @@ -303,16 +313,16 @@ func (cm *ContractManager) ProcessConsensusChange(cc modules.ConsensusChange) {
}

for _, applied := range appliedResolutions {
if relevant, err := tx.ContractRelevant(applied); err != nil {
if relevant, err := tx.ContractRelevant(applied.id); err != nil {
return fmt.Errorf("failed to check if contract %v is relevant: %w", applied, err)
} else if !relevant {
continue
} else if err := tx.ConfirmResolution(applied); err != nil {
} else if err := tx.ConfirmResolution(applied.id, applied.height); err != nil {
return fmt.Errorf("failed to apply proof: %w", err)
} else if err := tx.SetStatus(applied, ContractStatusSuccessful); err != nil {
} else if err := tx.SetStatus(applied.id, ContractStatusSuccessful); err != nil {
return fmt.Errorf("failed to set status: %w", err)
}
log.Debug("contract resolution applied", zap.String("contract", applied.String()))
log.Debug("contract resolution applied", zap.String("contract", applied.id.String()), zap.Uint64("height", applied.height))
}

return nil
Expand Down
5 changes: 3 additions & 2 deletions host/contracts/manager_test.go
Expand Up @@ -303,14 +303,15 @@ func TestContractLifecycle(t *testing.T) {
t.Fatal(err)
}
time.Sleep(time.Second) // sync time
proofHeight := rev.Revision.WindowStart

contract, err = c.Contract(rev.Revision.ParentID)
if err != nil {
t.Fatal(err)
} else if contract.Status != contracts.ContractStatusSuccessful {
t.Fatal("expected contract to be successful")
} else if !contract.ResolutionConfirmed {
t.Fatal("expected resolution to be confirmed")
} else if contract.ResolutionHeight != proofHeight {
t.Fatalf("expected resolution height %v, got %v", proofHeight, contract.ResolutionHeight)
} else if m, err := node.Store().Metrics(time.Now()); err != nil {
t.Fatal(err)
} else if m.Contracts.Active != 0 {
Expand Down
4 changes: 2 additions & 2 deletions host/contracts/persist.go
Expand Up @@ -31,7 +31,7 @@ type (
SetStatus(types.FileContractID, ContractStatus) error
ConfirmFormation(types.FileContractID) error
ConfirmRevision(types.FileContractRevision) error
ConfirmResolution(types.FileContractID) error
ConfirmResolution(id types.FileContractID, height uint64) error

RevertFormation(types.FileContractID) error
RevertRevision(types.FileContractID) error
Expand Down Expand Up @@ -62,7 +62,7 @@ type (
SectorRoots(id types.FileContractID, limit, offset uint64) ([]types.Hash256, error)
// ContractAction calls contractFn on every contract in the store that
// needs a lifecycle action performed.
ContractAction(height uint64, contractFn func(types.FileContractID, string)) error
ContractAction(height uint64, contractFn func(types.FileContractID, uint64, string)) error
// UpdateContract atomically updates a contract and its sector roots.
UpdateContract(types.FileContractID, func(UpdateContractTransaction) error) error
// UpdateContractState atomically updates the contract manager's state.
Expand Down
32 changes: 17 additions & 15 deletions persist/sqlite/contracts.go
Expand Up @@ -182,11 +182,11 @@ func (u *updateContractsTxn) ConfirmRevision(revision types.FileContractRevision
return u.tx.QueryRow(query, sqlUint64(revision.RevisionNumber), sqlHash256(revision.ParentID)).Scan(&dbID)
}

// ConfirmResolution sets the resolution_confirmed flag to true.
func (u *updateContractsTxn) ConfirmResolution(id types.FileContractID) error {
const query = `UPDATE contracts SET resolution_confirmed=true WHERE contract_id=$1 RETURNING id;`
// ConfirmResolution sets the resolution height.
func (u *updateContractsTxn) ConfirmResolution(id types.FileContractID, height uint64) error {
const query = `UPDATE contracts SET resolution_height=$1 WHERE contract_id=$2 RETURNING id;`
var dbID int64
if err := u.tx.QueryRow(query, sqlHash256(id)).Scan(&dbID); err != nil {
if err := u.tx.QueryRow(query, height, sqlHash256(id)).Scan(&dbID); err != nil {
return fmt.Errorf("failed to confirm resolution: %w", err)
}
return nil
Expand All @@ -209,9 +209,9 @@ func (u *updateContractsTxn) RevertRevision(id types.FileContractID) error {
return u.tx.QueryRow(query, sqlUint64(0), sqlHash256(id)).Scan(&dbID)
}

// RevertResolution sets the resolution_confirmed flag to false.
// RevertResolution sets the resolution height to null
func (u *updateContractsTxn) RevertResolution(id types.FileContractID) error {
const query = `UPDATE contracts SET resolution_confirmed=false WHERE contract_id=$1 RETURNING id;`
const query = `UPDATE contracts SET resolution_height=NULL WHERE contract_id=$1 RETURNING id;`
var dbID int64
if err := u.tx.QueryRow(query, sqlHash256(id)).Scan(&dbID); err != nil {
return fmt.Errorf("failed to revert resolution: %w", err)
Expand Down Expand Up @@ -244,7 +244,7 @@ func (s *Store) Contracts(filter contracts.ContractFilter) ([]contracts.Contract
s.log.Debug("querying contracts", zap.String("clause", whereClause), zap.Any("params", append(whereParams, filter.Limit, filter.Offset)))

query := fmt.Sprintf(`SELECT c.contract_id, rt.contract_id AS renewed_to, rf.contract_id AS renewed_from, c.contract_status, c.negotiation_height, c.formation_confirmed,
c.revision_number=c.confirmed_revision_number AS revision_confirmed, c.resolution_confirmed, c.locked_collateral, c.rpc_revenue,
c.revision_number=c.confirmed_revision_number AS revision_confirmed, c.resolution_height, c.locked_collateral, c.rpc_revenue,
c.storage_revenue, c.ingress_revenue, c.egress_revenue, c.account_funding, c.risked_collateral, c.raw_revision, c.host_sig, c.renter_sig
FROM contracts c
INNER JOIN contract_renters r ON (c.renter_id=r.id)
Expand All @@ -270,7 +270,7 @@ LEFT JOIN contracts rf ON (c.renewed_from=rf.id) %s %s LIMIT ? OFFSET ?`, whereC
// Contract returns the contract with the given ID.
func (s *Store) Contract(id types.FileContractID) (contracts.Contract, error) {
const query = `SELECT c.contract_id, rt.contract_id AS renewed_to, rf.contract_id AS renewed_from, c.contract_status, c.negotiation_height, c.formation_confirmed,
c.revision_number=c.confirmed_revision_number AS revision_confirmed, c.resolution_confirmed, c.locked_collateral, c.rpc_revenue,
c.revision_number=c.confirmed_revision_number AS revision_confirmed, c.resolution_height, c.locked_collateral, c.rpc_revenue,
c.storage_revenue, c.ingress_revenue, c.egress_revenue, c.account_funding, c.risked_collateral, c.raw_revision, c.host_sig, c.renter_sig
FROM contracts c
LEFT JOIN contracts rt ON (c.renewed_to = rt.id)
Expand Down Expand Up @@ -386,14 +386,14 @@ func (s *Store) SectorRoots(contractID types.FileContractID, offset, limit uint6

// ContractAction calls contractFn on every contract in the store that
// needs a lifecycle action performed.
func (s *Store) ContractAction(height uint64, contractFn func(types.FileContractID, string)) error {
func (s *Store) ContractAction(height uint64, contractFn func(types.FileContractID, uint64, string)) error {
actions, err := contractsForAction(&dbTxn{s}, height)
if err != nil {
return fmt.Errorf("failed to get contracts for action: %w", err)
}

for _, action := range actions {
contractFn(action.ID, action.Action)
contractFn(action.ID, height, action.Action)
}
return nil
}
Expand Down Expand Up @@ -504,7 +504,7 @@ UNION
SELECT contract_id, 'revision' AS action FROM contracts WHERE formation_confirmed=true AND confirmed_revision_number != revision_number AND window_start BETWEEN $2 AND $3
UNION
-- formation confirmed, resolution not confirmed, status active, in proof window (broadcast storage proof)
SELECT contract_id, 'resolve' AS action FROM contracts WHERE formation_confirmed=true AND resolution_confirmed=false AND window_start <= $4 AND window_end > $4 AND contract_status=$5
SELECT contract_id, 'resolve' AS action FROM contracts WHERE formation_confirmed=true AND resolution_height IS NULL AND window_start <= $4 AND window_end > $4 AND contract_status=$5
UNION
-- formation confirmed, status active, outside proof window (mark as failed)
SELECT contract_id, 'expire' AS action FROM contracts WHERE formation_confirmed=true AND window_end < $4 AND contract_status=$5;`
Expand Down Expand Up @@ -546,8 +546,8 @@ func renterDBID(tx txn, renterKey types.PublicKey) (int64, error) {
func insertContract(tx txn, revision contracts.SignedRevision, formationSet []types.Transaction, lockedCollateral types.Currency, initialUsage contracts.Usage, negotationHeight uint64) (dbID int64, err error) {
const query = `INSERT INTO contracts (contract_id, renter_id, locked_collateral, rpc_revenue, storage_revenue, ingress_revenue,
egress_revenue, account_funding, risked_collateral, revision_number, negotiation_height, window_start, window_end, formation_txn_set,
raw_revision, host_sig, renter_sig, confirmed_revision_number, formation_confirmed, resolution_confirmed, contract_status) VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id;`
raw_revision, host_sig, renter_sig, confirmed_revision_number, formation_confirmed, contract_status) VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING id;`
renterID, err := renterDBID(tx, revision.RenterKey())
if err != nil {
return 0, fmt.Errorf("failed to get renter id: %w", err)
Expand All @@ -572,7 +572,6 @@ raw_revision, host_sig, renter_sig, confirmed_revision_number, formation_confirm
sqlHash512(revision.RenterSignature),
sqlUint64(0), // confirmed_revision_number
false, // formation_confirmed
false, // resolution_confirmed
contracts.ContractStatusPending,
).Scan(&dbID)
if err != nil {
Expand Down Expand Up @@ -705,14 +704,15 @@ func buildOrderBy(filter contracts.ContractFilter) string {
func scanContract(row scanner) (c contracts.Contract, err error) {
var revisionBuf []byte
var contractID types.FileContractID
var resolutionHeight sql.NullInt64
err = row.Scan((*sqlHash256)(&contractID),
nullable((*sqlHash256)(&c.RenewedTo)),
nullable((*sqlHash256)(&c.RenewedFrom)),
&c.Status,
&c.NegotiationHeight,
&c.FormationConfirmed,
&c.RevisionConfirmed,
&c.ResolutionConfirmed,
&resolutionHeight,
(*sqlCurrency)(&c.LockedCollateral),
(*sqlCurrency)(&c.Usage.RPCRevenue),
(*sqlCurrency)(&c.Usage.StorageRevenue),
Expand All @@ -730,6 +730,8 @@ func scanContract(row scanner) (c contracts.Contract, err error) {
return contracts.Contract{}, fmt.Errorf("failed to decode revision: %w", err)
} else if c.Revision.ParentID != contractID {
panic("contract id mismatch")
} else if resolutionHeight.Valid {
c.ResolutionHeight = uint64(resolutionHeight.Int64)
}
return
}
Expand Down
6 changes: 3 additions & 3 deletions persist/sqlite/init.sql
Expand Up @@ -87,7 +87,7 @@ CREATE TABLE contracts (
renter_sig BLOB NOT NULL,
raw_revision BLOB NOT NULL, -- binary serialized contract revision
formation_confirmed BOOLEAN NOT NULL, -- true if the contract has been confirmed on the blockchain
resolution_confirmed BOOLEAN NOT NULL, -- true if the storage proof/resolution has been confirmed on the blockchain
resolution_height INTEGER, -- null if the storage proof/resolution has not been confirmed on the blockchain, otherwise the height of the block containing the storage proof/resolution
negotiation_height INTEGER NOT NULL, -- determines if the formation txn should be rebroadcast or if the contract should be deleted
window_start INTEGER NOT NULL,
window_end INTEGER NOT NULL,
Expand All @@ -100,8 +100,8 @@ CREATE INDEX contracts_renewed_from ON contracts(renewed_from);
CREATE INDEX contracts_negotiation_height ON contracts(negotiation_height);
CREATE INDEX contracts_window_start ON contracts(window_start);
CREATE INDEX contracts_contract_status ON contracts(contract_status);
CREATE INDEX contracts_formation_confirmed_resolution_confirmed_window_start ON contracts(formation_confirmed, resolution_confirmed, window_start);
CREATE INDEX contracts_formation_confirmed_resolution_confirmed_window_end ON contracts(formation_confirmed, resolution_confirmed, window_end);
CREATE INDEX contracts_formation_confirmed_resolution_height_window_start ON contracts(formation_confirmed, resolution_height, window_start);
CREATE INDEX contracts_formation_confirmed_resolution_height_window_end ON contracts(formation_confirmed, resolution_height, window_end);
CREATE INDEX contracts_formation_confirmed_window_start ON contracts(formation_confirmed, window_start);
CREATE INDEX contracts_formation_confirmed_negotation_height ON contracts(formation_confirmed, negotiation_height);

Expand Down

0 comments on commit afd54c6

Please sign in to comment.