diff --git a/consensus/update_test.go b/consensus/update_test.go index 097ad351..62aedac1 100644 --- a/consensus/update_test.go +++ b/consensus/update_test.go @@ -1084,6 +1084,7 @@ func TestApplyRevertBlockV2(t *testing.T) { v1FC.Filesize = 65 v1FC.FileMerkleRoot = blake2b.SumPair((State{}).StorageProofLeafHash([]byte{1}), (State{}).StorageProofLeafHash([]byte{2})) v2FC := types.V2FileContract{ + Capacity: v1FC.Filesize, Filesize: v1FC.Filesize, FileMerkleRoot: v1FC.FileMerkleRoot, ProofHeight: 20, diff --git a/consensus/validation.go b/consensus/validation.go index 7a41ff95..fa939deb 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -715,6 +715,8 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { validateContract := func(fc types.V2FileContract, renewal bool) error { switch { + case fc.Filesize > fc.Capacity: + return fmt.Errorf("has filesize (%v) exceeding capacity (%v)", fc.Filesize, fc.Capacity) case fc.ProofHeight < ms.base.childHeight(): return fmt.Errorf("has proof height (%v) that has already passed", fc.ProofHeight) case fc.ExpirationHeight <= fc.ProofHeight: @@ -737,6 +739,10 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { curOutputSum := cur.RenterOutput.Value.Add(cur.HostOutput.Value) revOutputSum := rev.RenterOutput.Value.Add(rev.HostOutput.Value) switch { + case rev.Capacity < cur.Capacity: + return fmt.Errorf("decreases capacity") + case rev.Filesize > rev.Capacity: + return fmt.Errorf("has filesize (%v) exceeding capacity (%v)", rev.Filesize, rev.Capacity) case cur.ProofHeight < ms.base.childHeight(): return fmt.Errorf("revises contract after its proof window has opened") case rev.RevisionNumber <= cur.RevisionNumber: diff --git a/consensus/validation_test.go b/consensus/validation_test.go index dd13993d..96bf38c0 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -861,6 +861,7 @@ func TestValidateV2Block(t *testing.T) { v1GiftFC.Filesize = 65 v1GiftFC.FileMerkleRoot = blake2b.SumPair((State{}).StorageProofLeafHash([]byte{1}), (State{}).StorageProofLeafHash([]byte{2})) v2GiftFC := types.V2FileContract{ + Capacity: v1GiftFC.Filesize, Filesize: v1GiftFC.Filesize, FileMerkleRoot: v1GiftFC.FileMerkleRoot, ProofHeight: 20, diff --git a/rhp/v4/encoding.go b/rhp/v4/encoding.go index 35278e21..71650515 100644 --- a/rhp/v4/encoding.go +++ b/rhp/v4/encoding.go @@ -2,7 +2,6 @@ package rhp import ( "bytes" - "fmt" "go.sia.tech/core/types" ) @@ -26,7 +25,7 @@ func (hp HostPrices) EncodeTo(e *types.Encoder) { types.V2Currency(hp.StoragePrice).EncodeTo(e) types.V2Currency(hp.IngressPrice).EncodeTo(e) types.V2Currency(hp.EgressPrice).EncodeTo(e) - types.V2Currency(hp.ModifySectorActionPrice).EncodeTo(e) + types.V2Currency(hp.FreeSectorPrice).EncodeTo(e) e.WriteUint64(hp.TipHeight) e.WriteTime(hp.ValidUntil) hp.Signature.EncodeTo(e) @@ -39,7 +38,7 @@ func (hp *HostPrices) DecodeFrom(d *types.Decoder) { (*types.V2Currency)(&hp.StoragePrice).DecodeFrom(d) (*types.V2Currency)(&hp.IngressPrice).DecodeFrom(d) (*types.V2Currency)(&hp.EgressPrice).DecodeFrom(d) - (*types.V2Currency)(&hp.ModifySectorActionPrice).DecodeFrom(d) + (*types.V2Currency)(&hp.FreeSectorPrice).DecodeFrom(d) hp.TipHeight = d.ReadUint64() hp.ValidUntil = d.ReadTime() hp.Signature.DecodeFrom(d) @@ -54,7 +53,7 @@ func (hs HostSettings) EncodeTo(e *types.Encoder) { types.V2Currency(hs.MaxCollateral).EncodeTo(e) e.WriteUint64(hs.MaxContractDuration) e.WriteUint64(hs.MaxSectorDuration) - e.WriteUint64(hs.MaxModifyActions) + e.WriteUint64(hs.MaxSectorBatchSize) e.WriteUint64(hs.RemainingStorage) e.WriteUint64(hs.TotalStorage) hs.Prices.EncodeTo(e) @@ -69,46 +68,12 @@ func (hs *HostSettings) DecodeFrom(d *types.Decoder) { (*types.V2Currency)(&hs.MaxCollateral).DecodeFrom(d) hs.MaxContractDuration = d.ReadUint64() hs.MaxSectorDuration = d.ReadUint64() - hs.MaxModifyActions = d.ReadUint64() + hs.MaxSectorBatchSize = d.ReadUint64() hs.RemainingStorage = d.ReadUint64() hs.TotalStorage = d.ReadUint64() hs.Prices.DecodeFrom(d) } -// EncodeTo implements types.EncoderTo. -func (m ModifyAction) EncodeTo(e *types.Encoder) { - e.WriteUint8(m.Type) - switch m.Type { - case ActionSwap: - e.WriteUint64(m.A) - e.WriteUint64(m.B) - case ActionTrim: - e.WriteUint64(m.N) - case ActionUpdate: - m.Root.EncodeTo(e) - e.WriteUint64(m.A) - default: - panic("invalid action type") - } -} - -// DecodeFrom implements types.DecoderFrom. -func (m *ModifyAction) DecodeFrom(d *types.Decoder) { - m.Type = d.ReadUint8() - switch m.Type { - case ActionSwap: - m.A = d.ReadUint64() - m.B = d.ReadUint64() - case ActionTrim: - m.N = d.ReadUint64() - case ActionUpdate: - m.Root.DecodeFrom(d) - m.A = d.ReadUint64() - default: - d.SetErr(fmt.Errorf("invalid action type (%v)", m.Type)) - } -} - // EncodeTo implements types.EncoderTo. func (a Account) EncodeTo(e *types.Encoder) { e.Write(a[:]) } @@ -389,53 +354,57 @@ func (r *RPCRefreshContractThirdResponse) maxLen() int { return reasonableObjectSize } -func (r *RPCModifySectorsRequest) encodeTo(e *types.Encoder) { +func (r *RPCFreeSectorsRequest) encodeTo(e *types.Encoder) { r.ContractID.EncodeTo(e) r.Prices.EncodeTo(e) - types.EncodeSlice(e, r.Actions) + types.EncodeSliceFn(e, r.Indices, func(e *types.Encoder, v uint64) { + e.WriteUint64(v) + }) r.ChallengeSignature.EncodeTo(e) } -func (r *RPCModifySectorsRequest) decodeFrom(d *types.Decoder) { +func (r *RPCFreeSectorsRequest) decodeFrom(d *types.Decoder) { r.ContractID.DecodeFrom(d) r.Prices.DecodeFrom(d) - types.DecodeSlice(d, &r.Actions) + types.DecodeSliceFn(d, &r.Indices, func(d *types.Decoder) uint64 { + return d.ReadUint64() + }) r.ChallengeSignature.DecodeFrom(d) } -func (r *RPCModifySectorsRequest) maxLen() int { +func (r *RPCFreeSectorsRequest) maxLen() int { return reasonableObjectSize } -func (r *RPCModifySectorsResponse) encodeTo(e *types.Encoder) { +func (r *RPCFreeSectorsResponse) encodeTo(e *types.Encoder) { types.EncodeSlice(e, r.OldSubtreeHashes) types.EncodeSlice(e, r.OldLeafHashes) r.NewMerkleRoot.EncodeTo(e) } -func (r *RPCModifySectorsResponse) decodeFrom(d *types.Decoder) { +func (r *RPCFreeSectorsResponse) decodeFrom(d *types.Decoder) { types.DecodeSlice(d, &r.OldSubtreeHashes) types.DecodeSlice(d, &r.OldLeafHashes) r.NewMerkleRoot.DecodeFrom(d) } -func (r *RPCModifySectorsResponse) maxLen() int { +func (r *RPCFreeSectorsResponse) maxLen() int { return reasonableObjectSize } -func (r *RPCModifySectorsSecondResponse) encodeTo(e *types.Encoder) { +func (r *RPCFreeSectorsSecondResponse) encodeTo(e *types.Encoder) { r.RenterSignature.EncodeTo(e) } -func (r *RPCModifySectorsSecondResponse) decodeFrom(d *types.Decoder) { +func (r *RPCFreeSectorsSecondResponse) decodeFrom(d *types.Decoder) { r.RenterSignature.DecodeFrom(d) } -func (r *RPCModifySectorsSecondResponse) maxLen() int { +func (r *RPCFreeSectorsSecondResponse) maxLen() int { return sizeofSignature } -func (r *RPCModifySectorsThirdResponse) encodeTo(e *types.Encoder) { +func (r *RPCFreeSectorsThirdResponse) encodeTo(e *types.Encoder) { r.HostSignature.EncodeTo(e) } -func (r *RPCModifySectorsThirdResponse) decodeFrom(d *types.Decoder) { +func (r *RPCFreeSectorsThirdResponse) decodeFrom(d *types.Decoder) { r.HostSignature.DecodeFrom(d) } -func (r *RPCModifySectorsThirdResponse) maxLen() int { +func (r *RPCFreeSectorsThirdResponse) maxLen() int { return sizeofSignature } diff --git a/rhp/v4/merkle.go b/rhp/v4/merkle.go index ca381815..97201083 100644 --- a/rhp/v4/merkle.go +++ b/rhp/v4/merkle.go @@ -76,7 +76,7 @@ func BuildAppendProof(sectorRoots, appended []types.Hash256) ([]types.Hash256, t return subtreeRoots, acc.Root() } -// VerifyAppendProof verifies a Merkle proof produced by BuildAppendProof. +// VerifyAppendSectorsProof verifies a Merkle proof produced by BuildAppendProof. func VerifyAppendSectorsProof(numSectors uint64, subtreeRoots []types.Hash256, appended []types.Hash256, oldRoot, newRoot types.Hash256) bool { acc := blake2b.Accumulator{NumLeaves: numSectors} for i := 0; i < bits.Len64(numSectors); i++ { @@ -106,6 +106,8 @@ func VerifySectorRootsProof(proof, sectorRoots []types.Hash256, numSectors, star return rhp2.VerifySectorRangeProof(proof, sectorRoots, start, end, numSectors, root) } +/* +TODO: implement for RPC remove func convertActions(actions []ModifyAction) []rhp2.RPCWriteAction { rhp2Actions := make([]rhp2.RPCWriteAction, len(actions)) for i, a := range actions { @@ -142,3 +144,4 @@ func BuildModifySectorsProof(actions []ModifyAction, sectorRoots []types.Hash256 func VerifyModifySectorsProof(actions []ModifyAction, numSectors uint64, treeHashes, leafHashes []types.Hash256, oldRoot types.Hash256, newRoot types.Hash256) bool { return rhp2.VerifyDiffProof(convertActions(actions), numSectors, treeHashes, leafHashes, oldRoot, newRoot, nil) } +*/ diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index 6d4a80eb..5bc2240a 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -25,7 +25,7 @@ var ( RPCFundAccountsID = types.NewSpecifier("FundAccounts") RPCLatestRevisionID = types.NewSpecifier("LatestRevision") RPCAppendSectorsID = types.NewSpecifier("AppendSectors") - RPCModifySectorsID = types.NewSpecifier("ModifySectors") + RPCFreeSectorsID = types.NewSpecifier("FreeSectors") RPCReadSectorID = types.NewSpecifier("ReadSector") RPCRenewContractID = types.NewSpecifier("RenewContract") RPCRefreshContractID = types.NewSpecifier("RefreshContract") @@ -39,74 +39,106 @@ func round4KiB(n uint64) uint64 { return (n + (1<<12 - 1)) &^ (1<<12 - 1) } +// Usage contains the cost breakdown and collateral of executing an RPC. +type Usage struct { + RPC types.Currency `json:"rpc"` + Storage types.Currency `json:"storage"` + Egress types.Currency `json:"egress"` + Ingress types.Currency `json:"ingress"` + AccountFunding types.Currency `json:"accountFunding"` + RiskedCollateral types.Currency `json:"collateral"` +} + +// RenterCost returns the total cost of executing the RPC. +func (u Usage) RenterCost() types.Currency { + return u.RPC.Add(u.Storage).Add(u.Egress).Add(u.Ingress).Add(u.AccountFunding) +} + +// HostRiskedCollateral returns the amount of collateral the host must risk +func (u Usage) HostRiskedCollateral() types.Currency { + return u.RiskedCollateral +} + +// Add returns the sum of two Usages. +func (u Usage) Add(b Usage) Usage { + return Usage{ + RPC: u.RPC.Add(b.RPC), + Storage: u.Storage.Add(b.Storage), + Egress: u.Egress.Add(b.Egress), + Ingress: u.Ingress.Add(b.Ingress), + AccountFunding: u.AccountFunding.Add(b.AccountFunding), + RiskedCollateral: u.RiskedCollateral.Add(b.RiskedCollateral), + } +} + // HostPrices specify a time-bound set of parameters used to calculate the cost // of various RPCs. type HostPrices struct { - ContractPrice types.Currency `json:"contractPrice"` - Collateral types.Currency `json:"collateral"` - StoragePrice types.Currency `json:"storagePrice"` - IngressPrice types.Currency `json:"ingressPrice"` - EgressPrice types.Currency `json:"egressPrice"` - ModifySectorActionPrice types.Currency `json:"modifySectorActionPrice"` - TipHeight uint64 `json:"tipHeight"` - ValidUntil time.Time `json:"validUntil"` + ContractPrice types.Currency `json:"contractPrice"` + Collateral types.Currency `json:"collateral"` + StoragePrice types.Currency `json:"storagePrice"` + IngressPrice types.Currency `json:"ingressPrice"` + EgressPrice types.Currency `json:"egressPrice"` + FreeSectorPrice types.Currency `json:"freeSectorPrice"` + TipHeight uint64 `json:"tipHeight"` + ValidUntil time.Time `json:"validUntil"` // covers above fields Signature types.Signature `json:"signature"` } // RPCReadSectorCost returns the cost of reading a sector of the given length. -func (hp HostPrices) RPCReadSectorCost(length uint64) types.Currency { - return hp.EgressPrice.Mul64(round4KiB(length)) +func (hp HostPrices) RPCReadSectorCost(length uint64) Usage { + return Usage{ + Egress: hp.EgressPrice.Mul64(round4KiB(length)), + } } // RPCWriteSectorCost returns the cost of executing the WriteSector RPC with the // given sector length and duration. -func (hp HostPrices) RPCWriteSectorCost(sectorLength uint64, duration uint64) types.Currency { - storage, _ := hp.StoreSectorCost(duration) - return hp.IngressPrice.Mul64(round4KiB(sectorLength)).Add(storage) +func (hp HostPrices) RPCWriteSectorCost(sectorLength uint64, duration uint64) Usage { + return hp.StoreSectorCost(duration).Add(Usage{ + Ingress: hp.IngressPrice.Mul64(round4KiB(sectorLength)), + }) } // StoreSectorCost returns the cost of storing a sector for the given duration. -func (hp HostPrices) StoreSectorCost(duration uint64) (storage types.Currency, collateral types.Currency) { - storage = hp.StoragePrice.Mul64(SectorSize).Mul64(duration) - collateral = hp.Collateral.Mul64(SectorSize).Mul64(duration) - return +func (hp HostPrices) StoreSectorCost(duration uint64) Usage { + return Usage{ + Storage: hp.StoragePrice.Mul64(SectorSize).Mul64(duration), + RiskedCollateral: hp.Collateral.Mul64(SectorSize).Mul64(duration), + } } // RPCSectorRootsCost returns the cost of fetching sector roots for the given length. -func (hp HostPrices) RPCSectorRootsCost(length uint64) types.Currency { - return hp.EgressPrice.Mul64(round4KiB(32 * length)) +func (hp HostPrices) RPCSectorRootsCost(length uint64) Usage { + return Usage{ + Egress: hp.EgressPrice.Mul64(round4KiB(32 * length)), + } } // RPCVerifySectorCost returns the cost of building a proof for the specified // sector. -func (hp HostPrices) RPCVerifySectorCost() types.Currency { - return hp.EgressPrice.Mul64(SectorSize) -} - -// RPCModifySectorsCost returns the cost of modifying a contract's sectors with the -// given actions. The duration parameter is the number of blocks until the -// contract's expiration height. -func (hp HostPrices) RPCModifySectorsCost(actions []ModifyAction) (cost types.Currency) { - var n uint64 - for _, action := range actions { - if action.Type == ActionTrim { - n += action.N - } else { - n++ - } +func (hp HostPrices) RPCVerifySectorCost() Usage { + return Usage{ + Egress: hp.EgressPrice.Mul64(SectorSize), + } +} + +// RPCFreeSectorsCost returns the cost of removing sectors from a contract. +func (hp HostPrices) RPCFreeSectorsCost(sectors int) Usage { + return Usage{ + RPC: hp.FreeSectorPrice.Mul64(uint64(sectors)), } - return hp.ModifySectorActionPrice.Mul64(n) } // RPCAppendSectorsCost returns the cost of appending sectors to a contract. The duration // parameter is the number of blocks until the contract's expiration height. -func (hp HostPrices) RPCAppendSectorsCost(sectors, duration uint64) (cost, collateral types.Currency) { - storage, collateral := hp.StoreSectorCost(duration) - storage = storage.Mul64(sectors) - collateral = collateral.Mul64(sectors) - return storage, collateral +func (hp HostPrices) RPCAppendSectorsCost(sectors, duration uint64) Usage { + usage := hp.StoreSectorCost(duration) + usage.Storage = usage.Storage.Mul64(sectors) + usage.RiskedCollateral = usage.RiskedCollateral.Mul64(sectors) + return usage } // SigHash returns the hash of the host settings used for signing. @@ -117,7 +149,7 @@ func (hp HostPrices) SigHash() types.Hash256 { types.V2Currency(hp.StoragePrice).EncodeTo(h.E) types.V2Currency(hp.IngressPrice).EncodeTo(h.E) types.V2Currency(hp.EgressPrice).EncodeTo(h.E) - types.V2Currency(hp.ModifySectorActionPrice).EncodeTo(h.E) + types.V2Currency(hp.FreeSectorPrice).EncodeTo(h.E) h.E.WriteUint64(hp.TipHeight) h.E.WriteTime(hp.ValidUntil) return h.Sum() @@ -144,28 +176,12 @@ type HostSettings struct { MaxCollateral types.Currency `json:"maxCollateral"` MaxContractDuration uint64 `json:"maxContractDuration"` MaxSectorDuration uint64 `json:"maxSectorDuration"` - MaxModifyActions uint64 `json:"maxModifyActions"` + MaxSectorBatchSize uint64 `json:"maxSectorBatchSize"` RemainingStorage uint64 `json:"remainingStorage"` TotalStorage uint64 `json:"totalStorage"` Prices HostPrices `json:"prices"` } -// A ModifyAction adds or modifies sectors within a contract. -type ModifyAction struct { - Type uint8 `json:"type"` - Root types.Hash256 `json:"root,omitempty"` // Update - A uint64 `json:"a,omitempty"` // Swap - B uint64 `json:"b,omitempty"` // Swap - N uint64 `json:"n,omitempty"` // Trim, Update -} - -// ModifyAction types. -const ( - ActionSwap = iota + 1 - ActionTrim - ActionUpdate -) - // An Account represents an ephemeral balance that can be funded via contract // revision and spent to pay for RPCs. type Account types.PublicKey @@ -333,26 +349,26 @@ type ( TransactionSet []types.V2Transaction `json:"transactionSet"` } - // RPCModifySectorsRequest implements Object. - RPCModifySectorsRequest struct { + // RPCFreeSectorsRequest implements Object. + RPCFreeSectorsRequest struct { ContractID types.FileContractID `json:"contractID"` Prices HostPrices `json:"prices"` - Actions []ModifyAction `json:"actions"` + Indices []uint64 `json:"indices"` // A ChallengeSignature proves the renter can modify the contract. ChallengeSignature types.Signature `json:"challengeSignature"` } - // RPCModifySectorsResponse implements Object. - RPCModifySectorsResponse struct { + // RPCFreeSectorsResponse implements Object. + RPCFreeSectorsResponse struct { OldSubtreeHashes []types.Hash256 `json:"oldSubtreeHashes"` OldLeafHashes []types.Hash256 `json:"oldLeafHashes"` NewMerkleRoot types.Hash256 `json:"newMerkleRoot"` } - // RPCModifySectorsSecondResponse implements Object. - RPCModifySectorsSecondResponse struct { + // RPCFreeSectorsSecondResponse implements Object. + RPCFreeSectorsSecondResponse struct { RenterSignature types.Signature `json:"renterSignature"` } - // RPCModifySectorsThirdResponse implements Object. - RPCModifySectorsThirdResponse struct { + // RPCFreeSectorsThirdResponse implements Object. + RPCFreeSectorsThirdResponse struct { HostSignature types.Signature `json:"hostSignature"` } @@ -488,7 +504,7 @@ type ( // ChallengeSigHash returns the hash of the challenge signature used for // signing. -func (r *RPCModifySectorsRequest) ChallengeSigHash(revisionNumber uint64) types.Hash256 { +func (r *RPCFreeSectorsRequest) ChallengeSigHash(revisionNumber uint64) types.Hash256 { h := types.NewHasher() r.ContractID.EncodeTo(h.E) h.E.WriteUint64(revisionNumber) @@ -496,7 +512,7 @@ func (r *RPCModifySectorsRequest) ChallengeSigHash(revisionNumber uint64) types. } // ValidChallengeSignature checks the challenge signature for validity. -func (r *RPCModifySectorsRequest) ValidChallengeSignature(fc types.V2FileContract) bool { +func (r *RPCFreeSectorsRequest) ValidChallengeSignature(fc types.V2FileContract) bool { return fc.RenterPublicKey.VerifyHash(r.ChallengeSigHash(fc.RevisionNumber+1), r.ChallengeSignature) } @@ -542,47 +558,29 @@ func (r *RPCRefreshContractRequest) ValidChallengeSignature(existing types.V2Fil return existing.RenterPublicKey.VerifyHash(r.ChallengeSigHash(existing.RevisionNumber), r.ChallengeSignature) } -// ValidateModifyActions checks the given actions for validity. It returns an -// error if the actions are invalid. -func ValidateModifyActions(actions []ModifyAction, maxActions uint64) error { - var actionCount uint64 - for _, action := range actions { - switch action.Type { - case ActionSwap, ActionUpdate: - actionCount++ - case ActionTrim: - actionCount += action.N - default: - return fmt.Errorf("invalid action type: %v", action.Type) - } - } - if actionCount > maxActions { - return fmt.Errorf("too many actions: %v > %v", actionCount, maxActions) - } - return nil -} - // NewContract creates a new file contract with the given settings. -func NewContract(p HostPrices, cp RPCFormContractParams, hostKey types.PublicKey, hostAddress types.Address) types.V2FileContract { +func NewContract(p HostPrices, cp RPCFormContractParams, hostKey types.PublicKey, hostAddress types.Address) (types.V2FileContract, Usage) { return types.V2FileContract{ - Filesize: 0, - FileMerkleRoot: types.Hash256{}, - ProofHeight: cp.ProofHeight, - ExpirationHeight: cp.ProofHeight + proofWindow, - RenterOutput: types.SiacoinOutput{ - Value: cp.Allowance, - Address: cp.RenterAddress, - }, - HostOutput: types.SiacoinOutput{ - Value: cp.Collateral.Add(p.ContractPrice), - Address: hostAddress, - }, - MissedHostValue: cp.Collateral, - TotalCollateral: cp.Collateral, - RenterPublicKey: cp.RenterPublicKey, - HostPublicKey: hostKey, - RevisionNumber: 0, - } + Filesize: 0, + FileMerkleRoot: types.Hash256{}, + ProofHeight: cp.ProofHeight, + ExpirationHeight: cp.ProofHeight + proofWindow, + RenterOutput: types.SiacoinOutput{ + Value: cp.Allowance, + Address: cp.RenterAddress, + }, + HostOutput: types.SiacoinOutput{ + Value: cp.Collateral.Add(p.ContractPrice), + Address: hostAddress, + }, + MissedHostValue: cp.Collateral, + TotalCollateral: cp.Collateral, + RenterPublicKey: cp.RenterPublicKey, + HostPublicKey: hostKey, + RevisionNumber: 0, + }, Usage{ + RPC: p.ContractPrice, + } } // ContractCost calculates the cost to the host and renter for forming a contract. @@ -609,7 +607,8 @@ func RefreshCost(cs consensus.State, p HostPrices, r types.V2FileContractRenewal // PayWithContract modifies a contract to transfer the amount from the renter and // deduct collateral from the host. It returns an RPC error if the contract does not // have sufficient funds. -func PayWithContract(fc *types.V2FileContract, amount, collateral types.Currency) error { +func PayWithContract(fc *types.V2FileContract, usage Usage) error { + amount, collateral := usage.RenterCost(), usage.HostRiskedCollateral() // verify the contract can pay the amount before modifying if fc.RenterOutput.Value.Cmp(amount) < 0 { return NewRPCError(ErrorCodePayment, fmt.Sprintf("insufficient renter funds: %v < %v", fc.RenterOutput.Value, amount)) @@ -626,42 +625,43 @@ func PayWithContract(fc *types.V2FileContract, amount, collateral types.Currency return nil } -// ReviseForModifySectors creates a contract revision from a modify sectors request -// and response. -func ReviseForModifySectors(fc types.V2FileContract, prices HostPrices, root types.Hash256, actions []ModifyAction) (types.V2FileContract, error) { - for _, action := range actions { - if action.Type == ActionTrim { - fc.Filesize -= SectorSize * action.N - } - } - if err := PayWithContract(&fc, prices.RPCModifySectorsCost(actions), types.ZeroCurrency); err != nil { - return fc, err +// ReviseForFreeSectors creates a contract revision for the free sectors RPC +func ReviseForFreeSectors(fc types.V2FileContract, prices HostPrices, newRoot types.Hash256, deletions int) (types.V2FileContract, Usage, error) { + fc.Filesize -= SectorSize * uint64(deletions) + usage := prices.RPCFreeSectorsCost(deletions) + if err := PayWithContract(&fc, usage); err != nil { + return fc, Usage{}, err } - fc.FileMerkleRoot = root - return fc, nil + fc.FileMerkleRoot = newRoot + return fc, usage, nil } -// ReviseForAppendSectors creates a contract revision from an append sectors request -func ReviseForAppendSectors(fc types.V2FileContract, prices HostPrices, root types.Hash256, appended uint64) (types.V2FileContract, error) { - cost, collateral := prices.RPCAppendSectorsCost(appended, fc.ExpirationHeight-prices.TipHeight) - if err := PayWithContract(&fc, cost, collateral); err != nil { - return fc, err +// ReviseForAppendSectors creates a contract revision for the append sectors RPC +func ReviseForAppendSectors(fc types.V2FileContract, prices HostPrices, root types.Hash256, appended uint64) (types.V2FileContract, Usage, error) { + sectors := fc.Filesize / SectorSize + capacity := fc.Capacity / SectorSize + appended -= capacity - sectors // capacity will always be >= sectors + usage := prices.RPCAppendSectorsCost(appended, fc.ExpirationHeight-prices.TipHeight) + if err := PayWithContract(&fc, usage); err != nil { + return fc, Usage{}, err } fc.Filesize += SectorSize * appended fc.FileMerkleRoot = root - return fc, nil + return fc, usage, nil } -// ReviseForSectorRoots creates a contract revision from a sector roots request -func ReviseForSectorRoots(fc types.V2FileContract, prices HostPrices, numRoots uint64) (types.V2FileContract, error) { - err := PayWithContract(&fc, prices.EgressPrice.Mul64(round4KiB(32*numRoots)), types.ZeroCurrency) - return fc, err +// ReviseForSectorRoots creates a contract revision for the sector roots RPC +func ReviseForSectorRoots(fc types.V2FileContract, prices HostPrices, numRoots uint64) (types.V2FileContract, Usage, error) { + usage := prices.RPCSectorRootsCost(numRoots) + err := PayWithContract(&fc, usage) + return fc, usage, err } -// ReviseForFundAccount creates a contract revision from a fund account request. -func ReviseForFundAccount(fc types.V2FileContract, amount types.Currency) (types.V2FileContract, error) { - err := PayWithContract(&fc, amount, types.ZeroCurrency) - return fc, err +// ReviseForFundAccounts creates a contract revision for the fund accounts RPC +func ReviseForFundAccounts(fc types.V2FileContract, amount types.Currency) (types.V2FileContract, Usage, error) { + usage := Usage{AccountFunding: amount} + err := PayWithContract(&fc, usage) + return fc, usage, err } // MinRenterAllowance returns the minimum allowance required to justify the given @@ -671,8 +671,9 @@ func MinRenterAllowance(hp HostPrices, duration uint64, collateral types.Currenc return hp.StoragePrice.Mul64(duration).Mul(maxCollateralBytes) } -// RenewContract creates a contract renewal from an existing contract revision -func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContractParams) (renewal types.V2FileContractRenewal) { +// RenewContract creates a contract renewal for the renew RPC +func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContractParams) (types.V2FileContractRenewal, Usage) { + var renewal types.V2FileContractRenewal // clear the old contract renewal.FinalRevision = fc renewal.FinalRevision.RevisionNumber = types.MaxRevisionNumber @@ -702,8 +703,9 @@ func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContra // missed host value should only include the new collateral value renewal.NewContract.MissedHostValue = rp.Collateral - // storage cost is the difference between the new and old contract since the old contract - // already paid for the storage up to the current expiration height. + // storage cost is the difference between the new and old contract since the + // old contract already paid for the storage up to the current expiration + // height. storageCost := prices.StoragePrice.Mul64(fc.Filesize).Mul64(renewal.NewContract.ExpirationHeight - fc.ExpirationHeight) // host output value includes the locked + risked collateral, the additional @@ -728,11 +730,17 @@ func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContra } else { renewal.RenterRollover = fc.RenterOutput.Value } - return + return renewal, Usage{ + RPC: prices.ContractPrice, + Storage: renewal.NewContract.HostOutput.Value.Sub(renewal.NewContract.TotalCollateral).Sub(prices.ContractPrice), + RiskedCollateral: renewal.NewContract.TotalCollateral.Sub(renewal.NewContract.MissedHostValue), + } } -// RefreshContract creates a new contract renewal from an existing contract revision -func RefreshContract(fc types.V2FileContract, prices HostPrices, rp RPCRefreshContractParams) (renewal types.V2FileContractRenewal) { +// RefreshContract creates a contract renewal for the refresh RPC. +func RefreshContract(fc types.V2FileContract, prices HostPrices, rp RPCRefreshContractParams) (types.V2FileContractRenewal, Usage) { + var renewal types.V2FileContractRenewal + // clear the old contract renewal.FinalRevision = fc renewal.FinalRevision.RevisionNumber = types.MaxRevisionNumber @@ -753,5 +761,9 @@ func RefreshContract(fc types.V2FileContract, prices HostPrices, rp RPCRefreshCo // roll over everything from the existing contract renewal.HostRollover = fc.HostOutput.Value renewal.RenterRollover = fc.RenterOutput.Value - return + return renewal, Usage{ + RPC: prices.ContractPrice, + Storage: renewal.NewContract.HostOutput.Value.Sub(renewal.NewContract.TotalCollateral).Sub(prices.ContractPrice), + RiskedCollateral: renewal.NewContract.TotalCollateral.Sub(renewal.NewContract.MissedHostValue), + } } diff --git a/rhp/v4/validation.go b/rhp/v4/validation.go index 58aab0fd..41fcfcbd 100644 --- a/rhp/v4/validation.go +++ b/rhp/v4/validation.go @@ -48,15 +48,27 @@ func (req *RPCWriteSectorStreamingRequest) Validate(pk types.PublicKey, maxDurat } // Validate validates a modify sectors request. Signatures are not validated. -func (req *RPCModifySectorsRequest) Validate(pk types.PublicKey, maxActions uint64) error { +func (req *RPCFreeSectorsRequest) Validate(pk types.PublicKey, fc types.V2FileContract, maxActions uint64) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) + } else if uint64(len(req.Indices)) > maxActions { + return fmt.Errorf("removing too many sectors at once: %d > %d", len(req.Indices), maxActions) + } + seen := make(map[uint64]bool) + sectors := fc.Filesize / SectorSize + for _, index := range req.Indices { + if index >= sectors { + return fmt.Errorf("sector index %d exceeds contract sectors %d", index, sectors) + } else if seen[index] { + return fmt.Errorf("duplicate sector index %d", index) + } + seen[index] = true } - return ValidateModifyActions(req.Actions, maxActions) + return nil } // Validate validates a sector roots request. Signatures are not validated. -func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileContract) error { +func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileContract, maxSectors uint64) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) } @@ -67,6 +79,8 @@ func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileCo return errors.New("length must be greater than 0") case req.Length+req.Offset > contractSectors: return fmt.Errorf("read request range exceeds contract sectors: %d > %d", req.Length+req.Offset, contractSectors) + case req.Length > maxSectors: + return fmt.Errorf("read request range exceeds maximum sectors: %d > %d", req.Length, maxSectors) } return nil } diff --git a/types/encoding.go b/types/encoding.go index d1a8477c..2586abcf 100644 --- a/types/encoding.go +++ b/types/encoding.go @@ -651,6 +651,7 @@ func (sfe SiafundElement) EncodeTo(e *Encoder) { // EncodeTo implements types.EncoderTo. func (fc V2FileContract) EncodeTo(e *Encoder) { + e.WriteUint64(fc.Capacity) e.WriteUint64(fc.Filesize) fc.FileMerkleRoot.EncodeTo(e) e.WriteUint64(fc.ProofHeight) @@ -1246,6 +1247,7 @@ func (sfe *SiafundElement) DecodeFrom(d *Decoder) { // DecodeFrom implements types.DecoderFrom. func (fc *V2FileContract) DecodeFrom(d *Decoder) { + fc.Capacity = d.ReadUint64() fc.Filesize = d.ReadUint64() fc.FileMerkleRoot.DecodeFrom(d) fc.ProofHeight = d.ReadUint64() diff --git a/types/types.go b/types/types.go index 2b58f9e4..bdee4ed5 100644 --- a/types/types.go +++ b/types/types.go @@ -455,6 +455,7 @@ func (txn *Transaction) TotalFees() Currency { // or "missed" depending on whether a valid StorageProof is submitted for the // contract. type V2FileContract struct { + Capacity uint64 `json:"capacity"` Filesize uint64 `json:"filesize"` FileMerkleRoot Hash256 `json:"fileMerkleRoot"` ProofHeight uint64 `json:"proofHeight"`