Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RPCCost (RHPv2) #123

Merged
merged 3 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions rhp/v2/merkle.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,29 @@ func VerifyDiffProof(actions []RPCWriteAction, numLeaves uint64, treeHashes, lea
return verifyMulti(newProofIndices, treeHashes, newLeafHashes, numLeaves, newRoot)
}

// DiffProofSize returns the size of a Merkle diff proof for the specified
// actions within a tree containing numLeaves leaves.
func DiffProofSize(actions []RPCWriteAction, numLeaves uint64) (numHashes uint64) {
indices := sectorsChanged(actions, numLeaves)
numHashes += uint64(len(indices))

buildRange := func(i, j uint64) {
for i < j {
subtreeSize := nextSubtreeSize(i, j)
numHashes++
i += subtreeSize
}
}

var start uint64
for _, end := range indices {
buildRange(start, end)
start = end + 1
}
buildRange(start, numLeaves)
return
}

func sectorsChanged(actions []RPCWriteAction, numSectors uint64) []uint64 {
newNumSectors := numSectors
sectorsChanged := make(map[uint64]struct{})
Expand Down
160 changes: 127 additions & 33 deletions rhp/v2/rhp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package rhp

import (
"encoding/json"
"errors"
"fmt"
"math/bits"
"net"
"strings"
"time"
Expand Down Expand Up @@ -141,6 +141,29 @@ var (
RPCReadStop = types.NewSpecifier("ReadStop")
)

// RPC read/write errors
var (
// ErrOffsetOutOfBounds is returned when the offset exceeds and length
// exceed the sector size.
ErrOffsetOutOfBounds = errors.New("update section is out of bounds")

// ErrInvalidSectorLength is returned when a sector is not the correct
// length.
ErrInvalidSectorLength = errors.New("length of sector data must be exactly 4MiB")
// ErrSwapOutOfBounds is returned when one of the swap indices exceeds the
// total number of sectors
ErrSwapOutOfBounds = errors.New("swap index is out of bounds")
// ErrTrimOutOfBounds is returned when a trim operation exceeds the total
// number of sectors
ErrTrimOutOfBounds = errors.New("trim size exceeds number of sectors")
// ErrUpdateOutOfBounds is returned when the update index exceeds the total
// number of sectors
ErrUpdateOutOfBounds = errors.New("update index is out of bounds")
// ErrUpdateProofSize is returned when a proof is requested for an update
// operation that is not a multiple of 64 bytes.
ErrUpdateProofSize = errors.New("update section is not a multiple of the segment size")
)

// RPC request/response objects
type (
// RPCFormContractRequest contains the request parameters for the
Expand Down Expand Up @@ -280,45 +303,116 @@ type (
}
)

// RPCSectorRootsCost returns the price of a SectorRoots RPC.
func RPCSectorRootsCost(settings HostSettings, n uint64) types.Currency {
return settings.BaseRPCPrice.
Add(settings.DownloadBandwidthPrice.Mul64(n * 32)). // roots
Add(settings.DownloadBandwidthPrice.Mul64(128 * 32)) // proof
type RPCCost struct {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionally this type matches the ResourceCost in RHPv3, we could move this into the types package maybe and use the one type. I didn't want to do that yet mostly because of the documentation in ResourceCost which is tailored to the MDM... Maybe it's fine to just have both, for the same reason hostd had its own internal rpcCost type.

Base types.Currency
Storage types.Currency
Ingress types.Currency
Egress types.Currency
Collateral types.Currency
}

func (c RPCCost) Add(o RPCCost) RPCCost {
return RPCCost{
Base: c.Base.Add(o.Base),
Storage: c.Storage.Add(o.Storage),
Ingress: c.Ingress.Add(o.Ingress),
Egress: c.Egress.Add(o.Egress),
Collateral: c.Collateral.Add(o.Collateral),
}
}

func (c RPCCost) Total() (cost, collateral types.Currency) {
return c.Base.Add(c.Storage).Add(c.Ingress).Add(c.Egress), c.Collateral
}

// RPCReadCost returns the price of a Read RPC.
func RPCReadCost(settings HostSettings, sections []RPCReadRequestSection) types.Currency {
sectorAccessPrice := settings.SectorAccessPrice.Mul64(uint64(len(sections)))
// RPCReadCost returns the cost of a Read RPC.
func (hs *HostSettings) RPCReadCost(sections []RPCReadRequestSection, proof bool) (RPCCost, error) {
// validate the request sections and calculate the cost
var bandwidth uint64
for _, sec := range sections {
bandwidth += sec.Length
bandwidth += 2 * uint64(bits.Len64(LeavesPerSector)) * 32 // proof
switch {
case uint64(sec.Offset)+uint64(sec.Length) > SectorSize:
return RPCCost{}, ErrOffsetOutOfBounds
case sec.Length == 0:
return RPCCost{}, errors.New("length cannot be zero")
case proof && (sec.Offset%LeafSize != 0 || sec.Length%LeafSize != 0):
return RPCCost{}, errors.New("offset and length must be multiples of SegmentSize when requesting a Merkle proof")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*LeafSize

}

bandwidth += uint64(sec.Length)
if proof {
start := sec.Offset / LeafSize
end := (sec.Offset + sec.Length) / LeafSize
proofSize := RangeProofSize(LeavesPerSector, start, end)
bandwidth += proofSize * 32
}
}
if bandwidth < minMessageSize {
bandwidth = minMessageSize
}
bandwidthPrice := settings.DownloadBandwidthPrice.Mul64(bandwidth)
return settings.BaseRPCPrice.Add(sectorAccessPrice).Add(bandwidthPrice)

return RPCCost{
Base: hs.BaseRPCPrice.Add(hs.SectorAccessPrice.Mul64(uint64(len(sections)))),
Egress: hs.DownloadBandwidthPrice.Mul64(bandwidth),
}, nil
}

// RPCAppendCost returns the price and collateral of a Write RPC with a single
// append operation.
func RPCAppendCost(settings HostSettings, storageDuration uint64) (price, collateral types.Currency) {
price = settings.BaseRPCPrice.
Add(settings.StoragePrice.Mul64(SectorSize).Mul64(storageDuration)).
Add(settings.UploadBandwidthPrice.Mul64(SectorSize)).
Add(settings.DownloadBandwidthPrice.Mul64(128 * 32)) // proof
collateral = settings.Collateral.Mul64(SectorSize).Mul64(storageDuration)
// add some leeway to reduce chance of host rejecting
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@n8maninger hostd stopped doing it so I copied that behaviour, just wanted to draw attention to it to ensure it's safe to do so

price = price.Mul64(125).Div64(100)
collateral = collateral.Mul64(95).Div64(100)
return
// RPCSectorRootsCost returns the cost of a SectorRoots RPC.
func (hs *HostSettings) RPCSectorRootsCost(rootOffset, numRoots uint64) RPCCost {
proofSize := RangeProofSize(LeavesPerSector, rootOffset, rootOffset+numRoots)
return RPCCost{
Base: hs.BaseRPCPrice,
Egress: hs.DownloadBandwidthPrice.Mul64((numRoots + proofSize) * 32),
}
}

// RPCDeleteCost returns the price of a Write RPC that deletes n sectors.
func RPCDeleteCost(settings HostSettings, n int) types.Currency {
price := settings.BaseRPCPrice.
Add(settings.DownloadBandwidthPrice.Mul64(128 * 32)) // proof
return price.Mul64(105).Div64(100)
// RPCWriteCost returns the cost of a Write RPC.
func (hs *HostSettings) RPCWriteCost(actions []RPCWriteAction, oldSectors, remainingDuration uint64, proof bool) (RPCCost, error) {
var uploadBytes uint64
newSectors := oldSectors
for _, action := range actions {
switch action.Type {
case RPCWriteActionAppend:
if len(action.Data) != SectorSize {
return RPCCost{}, fmt.Errorf("invalid sector size: %v: %w", len(action.Data), ErrInvalidSectorLength)
}
newSectors++
uploadBytes += SectorSize
case RPCWriteActionTrim:
if action.A > newSectors {
return RPCCost{}, ErrTrimOutOfBounds
}
newSectors -= action.A
case RPCWriteActionSwap:
if action.A >= newSectors || action.B >= newSectors {
return RPCCost{}, ErrSwapOutOfBounds
}
case RPCWriteActionUpdate:
idx, offset := action.A, action.B
if idx >= newSectors {
return RPCCost{}, ErrUpdateOutOfBounds
} else if offset+uint64(len(action.Data)) > SectorSize {
return RPCCost{}, ErrOffsetOutOfBounds
} else if proof && (offset%LeafSize != 0) || len(action.Data)%LeafSize != 0 {
return RPCCost{}, ErrUpdateProofSize
}
default:
return RPCCost{}, fmt.Errorf("unknown write action type '%v'", action.Type)
}
}

cost := RPCCost{
Base: hs.BaseRPCPrice, // base cost of the RPC
Ingress: hs.UploadBandwidthPrice.Mul64(uploadBytes), // cost of uploading the new sectors
}

if newSectors > oldSectors {
additionalSectors := (newSectors - oldSectors)
cost.Storage = hs.StoragePrice.Mul64(SectorSize * additionalSectors * remainingDuration) // cost of storing the new sectors
cost.Collateral = hs.Collateral.Mul64(SectorSize * additionalSectors * remainingDuration) // collateral for the new sectors
}

if proof {
// estimate cost of Merkle proof
proofSize := DiffProofSize(actions, oldSectors)
cost.Egress = hs.DownloadBandwidthPrice.Mul64(proofSize * 32)
}
return cost, nil
}