diff --git a/modules/manager.go b/modules/manager.go index df11de7..2572520 100644 --- a/modules/manager.go +++ b/modules/manager.go @@ -281,6 +281,10 @@ type HostDB interface { type Manager interface { Alerter + // AcceptContracts accepts a set of contracts from the renter + // and adds them to the contract set. + AcceptContracts(types.PublicKey, []ContractMetadata) + // ActiveHosts returns an array of HostDB's active hosts. ActiveHosts() ([]HostDBEntry, error) diff --git a/modules/manager/contractor/contractor.go b/modules/manager/contractor/contractor.go index 41c2ffd..fe88556 100644 --- a/modules/manager/contractor/contractor.go +++ b/modules/manager/contractor/contractor.go @@ -557,3 +557,94 @@ func (c *Contractor) UpdateSlab(slab modules.Slab) error { } return err } + +// AcceptContracts accepts a set of contracts from the renter +// and adds them to the contract set. +func (c *Contractor) AcceptContracts(rpk types.PublicKey, contracts []modules.ContractMetadata) { + // Create a map of existing contracts. + existingContracts := c.staticContracts.ByRenter(rpk) + existing := make(map[types.FileContractID]struct{}) + for _, contract := range existingContracts { + existing[contract.ID] = struct{}{} + } + + // Iterate through the set and add only missing contracts. + for _, contract := range contracts { + if _, exists := existing[contract.ID]; exists { + continue + } + + // Find the contract txn. + block, ok := c.cs.BlockAtHeight(contract.StartHeight) + if !ok { + c.log.Println("ERROR: couldn't find block at height", contract.StartHeight) + continue + } + + var transaction types.Transaction + var found bool + for _, txn := range block.Transactions { + if len(txn.FileContractRevisions) > 0 && txn.FileContractRevisions[0].ParentID == contract.ID { + transaction = modules.CopyTransaction(txn) + found = true + break + } + } + if !found { + c.log.Println("ERROR: couldn't find transaction", contract.ID) + continue + } + + // We have no way to get some information from the data + // provided, so we have to speculate a bit. + txnFee := transaction.MinerFees[0] + rev := transaction.FileContractRevisions[0] + payout := rev.ValidRenterPayout().Add(rev.ValidHostPayout()) + tax := modules.Tax(contract.StartHeight, payout) + host, ok, err := c.hdb.Host(contract.HostKey) + if err != nil || !ok { + c.log.Println("ERROR: couldn't find host for the contract", contract.ID) + continue + } + contractFee := host.Settings.ContractPrice + maxContractFee := contract.TotalCost.Sub(txnFee).Sub(tax).Sub(contract.UploadSpending).Sub(contract.DownloadSpending).Sub(contract.FundAccountSpending) + if contractFee.Cmp(maxContractFee) > 0 { + contractFee = maxContractFee + } + + // Insert the contract. + rc, err := c.staticContracts.InsertContract(transaction, contract.StartHeight, contract.TotalCost, contractFee, txnFee, tax, rpk) + if err != nil { + c.log.Printf("ERROR: couldn't accept contract %s: %v\n", contract.ID, err) + continue + } + + // Add a mapping from the contract's id to the public keys of the host + // and the renter. + c.mu.Lock() + c.pubKeysToContractID[rc.RenterPublicKey.String()+rc.HostPublicKey.String()] = contract.ID + c.mu.Unlock() + + // Update the spendings. + // NOTE: `renterd` doesn't store contract signatures, so we have to + // pass a nil. + err = c.UpdateContract(rev, nil, contract.UploadSpending, contract.DownloadSpending, contract.FundAccountSpending) + if err != nil { + c.log.Println("ERROR: couldn't update contract spendings", err) + } + + if contract.RenewedFrom != (types.FileContractID{}) { + c.renewedFrom[contract.ID] = contract.RenewedFrom + err = c.updateRenewedContract(contract.RenewedFrom, contract.ID) + if err != nil { + c.log.Println("ERROR: couldn't update renewal history:", err) + } + } + } + + // Update the hostdb to include the new contracts. + err := c.hdb.UpdateContracts(c.staticContracts.ViewAll()) + if err != nil { + c.log.Println("ERROR: unable to update hostdb contracts:", err) + } +} diff --git a/modules/manager/manager.go b/modules/manager/manager.go index 65562cd..6369272 100644 --- a/modules/manager/manager.go +++ b/modules/manager/manager.go @@ -38,6 +38,10 @@ type hostContractor interface { // Allowance returns the current allowance of the renter. Allowance(types.PublicKey) modules.Allowance + // AcceptContracts accepts a set of contracts from the renter + // and adds them to the contract set. + AcceptContracts(types.PublicKey, []modules.ContractMetadata) + // Close closes the hostContractor. Close() error @@ -1079,3 +1083,9 @@ func (m *Manager) UpdateSlab(pk types.PublicKey, slab modules.Slab) error { return m.UpdateSpendings(renter.Email, us) } + +// AcceptContracts accepts a set of contracts from the renter +// and adds them to the contract set. +func (m *Manager) AcceptContracts(rpk types.PublicKey, contracts []modules.ContractMetadata) { + m.hostContractor.AcceptContracts(rpk, contracts) +} diff --git a/modules/provider.go b/modules/provider.go index 4d3fee5..47c2371 100644 --- a/modules/provider.go +++ b/modules/provider.go @@ -1,6 +1,7 @@ package modules import ( + rhpv2 "go.sia.tech/core/rhp/v2" "go.sia.tech/core/types" ) @@ -18,3 +19,89 @@ type Provider interface { // SecretKey returns the provider's secret key. SecretKey() types.PrivateKey } + +// ExtendedContract contains the contract and its metadata. +type ExtendedContract struct { + Contract rhpv2.ContractRevision + StartHeight uint64 + TotalCost types.Currency + UploadSpending types.Currency + DownloadSpending types.Currency + FundAccountSpending types.Currency + RenewedFrom types.FileContractID +} + +// EncodeTo implements requestBody. +func (ec ExtendedContract) EncodeTo(e *types.Encoder) { + ec.Contract.Revision.EncodeTo(e) + ec.Contract.Signatures[0].EncodeTo(e) + ec.Contract.Signatures[1].EncodeTo(e) + e.WriteUint64(ec.StartHeight) + ec.TotalCost.EncodeTo(e) + ec.UploadSpending.EncodeTo(e) + ec.DownloadSpending.EncodeTo(e) + ec.FundAccountSpending.EncodeTo(e) + ec.RenewedFrom.EncodeTo(e) +} + +// DecodeFrom implements requestBody. +func (ec ExtendedContract) DecodeFrom(d *types.Decoder) { + // Nothing to do here. +} + +// ExtendedContractSet is a collection of extendedContracts. +type ExtendedContractSet struct { + Contracts []ExtendedContract +} + +// EncodeTo implements requestBody. +func (ecs ExtendedContractSet) EncodeTo(e *types.Encoder) { + e.WritePrefix(len(ecs.Contracts)) + for _, ec := range ecs.Contracts { + ec.EncodeTo(e) + } +} + +// DecodeFrom implements requestBody. +func (ecs ExtendedContractSet) DecodeFrom(d *types.Decoder) { + // Nothing to do here. +} + +// ContractMetadata contains all metadata needed to re-create +// a contract. +type ContractMetadata struct { + ID types.FileContractID + HostKey types.PublicKey + + StartHeight uint64 + RenewedFrom types.FileContractID + + UploadSpending types.Currency + DownloadSpending types.Currency + FundAccountSpending types.Currency + TotalCost types.Currency +} + +// EncodeTo implements requestBody. +func (cm ContractMetadata) EncodeTo(e *types.Encoder) { + e.Write(cm.ID[:]) + e.Write(cm.HostKey[:]) + e.WriteUint64(cm.StartHeight) + e.Write(cm.RenewedFrom[:]) + cm.UploadSpending.EncodeTo(e) + cm.DownloadSpending.EncodeTo(e) + cm.FundAccountSpending.EncodeTo(e) + cm.TotalCost.EncodeTo(e) +} + +// DecodeFrom implements requestBody. +func (cm ContractMetadata) DecodeFrom(d *types.Decoder) { + d.Read(cm.ID[:]) + d.Read(cm.HostKey[:]) + cm.StartHeight = d.ReadUint64() + d.Read(cm.RenewedFrom[:]) + cm.UploadSpending.DecodeFrom(d) + cm.DownloadSpending.DecodeFrom(d) + cm.FundAccountSpending.DecodeFrom(d) + cm.TotalCost.DecodeFrom(d) +} diff --git a/modules/provider/consts.go b/modules/provider/consts.go index 037add1..61970ee 100644 --- a/modules/provider/consts.go +++ b/modules/provider/consts.go @@ -47,6 +47,10 @@ const ( // update a single slab. updateSlabTime = 15 * time.Second + // shareContractsTime defines the amount of time that the provider has to + // accept a set of contracts from the renter. + shareContractsTime = 1 * time.Minute + // defaultConnectionDeadline is the default read and write deadline which is set // on a connection. This ensures it times out if I/O exceeds this deadline. defaultConnectionDeadline = 5 * time.Minute @@ -94,4 +98,7 @@ var ( // updateSlabSpecifier is used to update a single slab. updateSlabSpecifier = types.NewSpecifier("UpdateSlab") + + // shareContractsSpecifier is used when a renter shares their contract set. + shareContractsSpecifier = types.NewSpecifier("ShareContracts") ) diff --git a/modules/provider/encoding.go b/modules/provider/encoding.go index 6193fb8..865a8ff 100644 --- a/modules/provider/encoding.go +++ b/modules/provider/encoding.go @@ -281,53 +281,6 @@ func (ur *updateRequest) EncodeTo(e *types.Encoder) { ur.FundAccount.EncodeTo(e) } -// extendedContract contains the contract and its metadata. -type extendedContract struct { - contract rhpv2.ContractRevision - startHeight uint64 - totalCost types.Currency - uploadSpending types.Currency - downloadSpending types.Currency - fundAccountSpending types.Currency - renewedFrom types.FileContractID -} - -// EncodeTo implements requestBody. -func (ec extendedContract) EncodeTo(e *types.Encoder) { - ec.contract.Revision.EncodeTo(e) - ec.contract.Signatures[0].EncodeTo(e) - ec.contract.Signatures[1].EncodeTo(e) - e.WriteUint64(ec.startHeight) - ec.totalCost.EncodeTo(e) - ec.uploadSpending.EncodeTo(e) - ec.downloadSpending.EncodeTo(e) - ec.fundAccountSpending.EncodeTo(e) - ec.renewedFrom.EncodeTo(e) -} - -// DecodeFrom implements requestBody. -func (ec extendedContract) DecodeFrom(d *types.Decoder) { - // Nothing to do here. -} - -// extendedContractSet is a collection of extendedContracts. -type extendedContractSet struct { - contracts []extendedContract -} - -// EncodeTo implements requestBody. -func (ecs extendedContractSet) EncodeTo(e *types.Encoder) { - e.WritePrefix(len(ecs.contracts)) - for _, ec := range ecs.contracts { - ec.EncodeTo(e) - } -} - -// DecodeFrom implements requestBody. -func (ecs extendedContractSet) DecodeFrom(d *types.Decoder) { - // Nothing to do here. -} - // formContractRequest is used when forming a contract with a single // host using the new Renter-Satellite protocol. type formContractRequest struct { @@ -635,3 +588,30 @@ func (usr *updateSlabRequest) EncodeTo(e *types.Encoder) { e.Write(usr.PubKey[:]) usr.Slab.EncodeTo(e) } + +// shareRequest is used when the renter submits a set of contracts. +type shareRequest struct { + PubKey types.PublicKey + Contracts []modules.ContractMetadata + + Signature types.Signature +} + +// DecodeFrom implements requestBody. +func (sr *shareRequest) DecodeFrom(d *types.Decoder) { + d.Read(sr.PubKey[:]) + sr.Contracts = make([]modules.ContractMetadata, d.ReadPrefix()) + for i := 0; i < len(sr.Contracts); i++ { + sr.Contracts[i].DecodeFrom(d) + } + sr.Signature.DecodeFrom(d) +} + +// EncodeTo implements requestBody. +func (sr *shareRequest) EncodeTo(e *types.Encoder) { + e.Write(sr.PubKey[:]) + e.WritePrefix(len(sr.Contracts)) + for _, contract := range sr.Contracts { + contract.EncodeTo(e) + } +} diff --git a/modules/provider/network.go b/modules/provider/network.go index f6bd7c6..56b120d 100644 --- a/modules/provider/network.go +++ b/modules/provider/network.go @@ -316,6 +316,11 @@ func (p *Provider) threadedHandleConn(conn net.Conn) { if err != nil { err = modules.AddContext(err, "incoming RPCUpdateSlab failed") } + case shareContractsSpecifier: + err = p.managedAcceptContracts(s) + if err != nil { + err = modules.AddContext(err, "incoming RPCShareContracts failed") + } default: p.log.Println("INFO: inbound connection from:", conn.RemoteAddr()) //TODO } diff --git a/modules/provider/rpc.go b/modules/provider/rpc.go index 204a930..a40f7f5 100644 --- a/modules/provider/rpc.go +++ b/modules/provider/rpc.go @@ -43,20 +43,20 @@ func (p *Provider) managedRequestContracts(s *modules.RPCSession) error { // Get the contracts. contracts := p.m.ContractsByRenter(rr.PubKey) - ecs := extendedContractSet{ - contracts: make([]extendedContract, 0, len(contracts)), + ecs := modules.ExtendedContractSet{ + Contracts: make([]modules.ExtendedContract, 0, len(contracts)), } for _, contract := range contracts { cr := convertContract(contract) - ecs.contracts = append(ecs.contracts, extendedContract{ - contract: cr, - startHeight: contract.StartHeight, - totalCost: contract.TotalCost, - uploadSpending: contract.UploadSpending, - downloadSpending: contract.DownloadSpending, - fundAccountSpending: contract.FundAccountSpending, - renewedFrom: p.m.RenewedFrom(contract.ID), + ecs.Contracts = append(ecs.Contracts, modules.ExtendedContract{ + Contract: cr, + StartHeight: contract.StartHeight, + TotalCost: contract.TotalCost, + UploadSpending: contract.UploadSpending, + DownloadSpending: contract.DownloadSpending, + FundAccountSpending: contract.FundAccountSpending, + RenewedFrom: p.m.RenewedFrom(contract.ID), }) } @@ -120,8 +120,8 @@ func (p *Provider) managedFormContracts(s *modules.RPCSession) error { return err } - ecs := extendedContractSet{ - contracts: make([]extendedContract, 0, fr.Hosts), + ecs := modules.ExtendedContractSet{ + Contracts: make([]modules.ExtendedContract, 0, fr.Hosts), } // Create an allowance. @@ -156,10 +156,10 @@ func (p *Provider) managedFormContracts(s *modules.RPCSession) error { for _, contract := range contracts { cr := convertContract(contract) - ecs.contracts = append(ecs.contracts, extendedContract{ - contract: cr, - startHeight: contract.StartHeight, - totalCost: contract.TotalCost, + ecs.Contracts = append(ecs.Contracts, modules.ExtendedContract{ + Contract: cr, + StartHeight: contract.StartHeight, + TotalCost: contract.TotalCost, }) } @@ -222,8 +222,8 @@ func (p *Provider) managedRenewContracts(s *modules.RPCSession) error { return err } - ecs := extendedContractSet{ - contracts: make([]extendedContract, 0, len(rr.Contracts)), + ecs := modules.ExtendedContractSet{ + Contracts: make([]modules.ExtendedContract, 0, len(rr.Contracts)), } // Create an allowance. @@ -258,10 +258,10 @@ func (p *Provider) managedRenewContracts(s *modules.RPCSession) error { for _, contract := range contracts { cr := convertContract(contract) - ecs.contracts = append(ecs.contracts, extendedContract{ - contract: cr, - startHeight: contract.StartHeight, - totalCost: contract.TotalCost, + ecs.Contracts = append(ecs.Contracts, modules.ExtendedContract{ + Contract: cr, + StartHeight: contract.StartHeight, + TotalCost: contract.TotalCost, }) } @@ -392,10 +392,10 @@ func (p *Provider) managedFormContract(s *modules.RPCSession) error { return err } - ec := extendedContract{ - contract: convertContract(contract), - startHeight: contract.StartHeight, - totalCost: contract.TotalCost, + ec := modules.ExtendedContract{ + Contract: convertContract(contract), + StartHeight: contract.StartHeight, + TotalCost: contract.TotalCost, } return s.WriteResponse(&ec) @@ -461,10 +461,10 @@ func (p *Provider) managedRenewContract(s *modules.RPCSession) error { return err } - ec := extendedContract{ - contract: convertContract(contract), - startHeight: contract.StartHeight, - totalCost: contract.TotalCost, + ec := modules.ExtendedContract{ + Contract: convertContract(contract), + StartHeight: contract.StartHeight, + TotalCost: contract.TotalCost, } return s.WriteResponse(&ec) @@ -782,3 +782,38 @@ func (p *Provider) managedUpdateSlab(s *modules.RPCSession) error { return s.WriteResponse(nil) } + +// managedAcceptContracts accepts a set of contracts from the renter. +func (p *Provider) managedAcceptContracts(s *modules.RPCSession) error { + // Extend the deadline. + s.Conn.SetDeadline(time.Now().Add(shareContractsTime)) + + // Read the request. + var sr shareRequest + hash, err := s.ReadRequest(&sr, 65536) + if err != nil { + err = fmt.Errorf("could not read renter request: %v", err) + s.WriteError(err) + return err + } + + // Verify the signature. + if ok := sr.PubKey.VerifyHash(hash, sr.Signature); !ok { + err = errors.New("could not verify renter signature") + s.WriteError(err) + return err + } + + // Check if we know this renter. + _, err = p.m.GetRenter(sr.PubKey) + if err != nil { + err = fmt.Errorf("could not find renter in the database: %v", err) + s.WriteError(err) + return err + } + + // Accept the contracts. + p.m.AcceptContracts(sr.PubKey, sr.Contracts) + + return s.WriteResponse(nil) +}